Skip to content

Commit bbe54d3

Browse files
authored
Error Help: AI Disclaimer and Feedback (#10594)
This change adds a footer to both AI Error Help locations (tour for blocks, text-based for js/py). The footer contains simple thumbs up/down feedback and a disclaimer, whose text is modeled after similar products like copilot chat and the Teams EDU AI feature.
1 parent 3c87fa3 commit bbe54d3

File tree

15 files changed

+246
-18
lines changed

15 files changed

+246
-18
lines changed

localtypings/pxtarget.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,6 +1354,7 @@ declare namespace pxt.tour {
13541354
steps: BubbleStep[];
13551355
showConfetti?: boolean;
13561356
numberFinalStep?: boolean; // The last step will only be included in the step count if this is true.
1357+
footer?: string | JSX.Element;
13571358
}
13581359
const enum BubbleLocation {
13591360
Above,
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { classList } from "../util";
2+
import { ThumbsFeedback } from "./Feedback/ThumbsFeedback";
3+
4+
interface AIFooterProps {
5+
className?: string; // Optional class name to add to the footer
6+
onFeedbackSelected: (positive: boolean | undefined) => void; // Callback function to handle feedback selection
7+
}
8+
9+
/**
10+
* A component containing a standard AI disclaimer and feedback buttons.
11+
*/
12+
export const AIFooter = (props: AIFooterProps) => {
13+
const {
14+
className,
15+
onFeedbackSelected
16+
} = props;
17+
18+
return (
19+
<div className={classList("ai-footer", className)}>
20+
<div className="ai-footer-text">{lf("AI generated content may be incorrect.")}</div>
21+
<ThumbsFeedback lockOnSelect={true} onFeedbackSelected={onFeedbackSelected} />
22+
</div>
23+
);
24+
};
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import * as React from "react";
2+
import { classList } from "../../util";
3+
import { Button } from "../Button";
4+
5+
interface ThumbsFeedbackProps {
6+
onFeedbackSelected: (positive: boolean | undefined) => void; // Callback function to handle feedback selection
7+
lockOnSelect?: boolean; // If true, the user cannot change their selection once made
8+
positiveFeedbackText?: string; // Tooltip text for the thumbs up button (not displayed)
9+
negativeFeedbackText?: string; // Tooltip text for the thumbs down button (not displayed)
10+
rootClassName?: string; // Optional class name to add to the root element
11+
positiveClassName?: string; // Optional class name to add to the thumbs up button
12+
negativeClassName?: string; // Optional class name to add to the thumbs down button
13+
}
14+
15+
/**
16+
* A component for gathering simple thumbs up/down feedback.
17+
*/
18+
export const ThumbsFeedback = (props: ThumbsFeedbackProps) => {
19+
const {
20+
lockOnSelect,
21+
onFeedbackSelected,
22+
positiveFeedbackText,
23+
negativeFeedbackText,
24+
rootClassName,
25+
positiveClassName,
26+
negativeClassName,
27+
} = props;
28+
const [selectedFeedback, setSelectedFeedback] = React.useState<boolean | undefined>(undefined);
29+
30+
const handleFeedbackSelected = (positive: boolean) => {
31+
if (positive === selectedFeedback) {
32+
// If the user clicks the same feedback button again, reset it
33+
setSelectedFeedback(undefined);
34+
onFeedbackSelected(undefined);
35+
} else {
36+
setSelectedFeedback(positive);
37+
onFeedbackSelected(positive);
38+
}
39+
};
40+
41+
const positiveText = positiveFeedbackText || lf("Helpful");
42+
const negativeText = negativeFeedbackText || lf("Not Helpful");
43+
const lockButtons = lockOnSelect && selectedFeedback !== undefined;
44+
return (
45+
<div className={classList("feedback-buttons", rootClassName)}>
46+
<Button
47+
className={classList("feedback-button", positiveClassName, selectedFeedback ? "selected" : undefined)}
48+
onClick={() => handleFeedbackSelected(true)}
49+
title={positiveText}
50+
ariaLabel={positiveText}
51+
leftIcon={selectedFeedback ? "fas fa-thumbs-up" : "far fa-thumbs-up"}
52+
disabled={lockButtons}
53+
/>
54+
<Button
55+
className={classList(
56+
"feedback-button",
57+
negativeClassName,
58+
selectedFeedback === false ? "selected" : undefined
59+
)}
60+
onClick={() => handleFeedbackSelected(false)}
61+
title={negativeText}
62+
ariaLabel={negativeText}
63+
leftIcon={selectedFeedback === false ? "fas fa-thumbs-down" : "far fa-thumbs-down"}
64+
disabled={lockButtons}
65+
/>
66+
</div>
67+
);
68+
};

react-common/components/controls/TeachingBubble.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface TeachingBubbleProps extends ContainerProps {
3030
onNext: () => void;
3131
onBack: () => void;
3232
onFinish: () => void;
33+
footer?: string | JSX.Element;
3334
}
3435

3536
export const TeachingBubble = (props: TeachingBubbleProps) => {
@@ -45,6 +46,7 @@ export const TeachingBubble = (props: TeachingBubbleProps) => {
4546
onNext,
4647
onBack,
4748
onFinish,
49+
footer,
4850
stepNumber,
4951
totalSteps,
5052
parentElement,
@@ -379,14 +381,14 @@ export const TeachingBubble = (props: TeachingBubbleProps) => {
379381
ariaLabel={closeLabel}
380382
rightIcon="fas fa-times-circle"
381383
/>
382-
<div className="teaching-bubble-content">
384+
<div className="teaching-bubble-body">
383385
<strong aria-live="polite">{targetContent.title}</strong>
384386
<p aria-live="polite">{targetContent.description}</p>
385-
<div className={`teaching-bubble-footer ${!hasSteps ? "no-steps" : ""}`}>
387+
<div className={`teaching-bubble-navigation ${!hasSteps ? "no-steps" : ""}`}>
386388
{hasSteps && <div className={classList("teaching-bubble-steps", forceHideSteps && "hidden")} aria-live="polite">
387389
{stepNumber} of {totalSteps}
388390
</div>}
389-
<div className="teaching-bubble-navigation">
391+
<div className="teaching-bubble-navigation-buttons">
390392
{hasPrevious && <Button
391393
className="tertiary tour-button"
392394
onClick={onBack}
@@ -411,6 +413,9 @@ export const TeachingBubble = (props: TeachingBubbleProps) => {
411413
</div>
412414
</div>
413415
</div>
416+
{footer && <div className="teaching-bubble-footer">
417+
{footer}
418+
</div>}
414419
</div>
415420
</FocusTrap>, parentElement || document.getElementById("root") || document.body)
416421
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.ai-footer {
2+
display: flex;
3+
flex-direction: row;
4+
justify-content: space-between;
5+
align-items: center;
6+
width: 100%;
7+
height: fit-content;
8+
font-size: 14px;
9+
line-height: 14px;
10+
11+
.feedback-button i {
12+
font-size: 14px;
13+
}
14+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.feedback-buttons {
2+
display: flex;
3+
flex-direction: row;
4+
align-items: center;
5+
justify-content: center;
6+
7+
.common-button.feedback-button {
8+
background: none;
9+
padding: 0.1rem 0;
10+
margin: 0 0.2rem 0 0;
11+
12+
i {
13+
margin: 0;
14+
}
15+
16+
&:hover:not(.disabled) {
17+
filter: opacity(0.7);
18+
}
19+
}
20+
}

react-common/styles/onboarding/TeachingBubble.less

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
color: var(--teaching-bubble-foreground);
4545
box-shadow: 0 0 0 0.1rem;
4646
border-radius: 0.5rem;
47-
padding: 1rem;
4847
z-index: @modalDimmerZIndex;
4948

5049
.common-button {
@@ -74,20 +73,25 @@
7473
color: var(--teaching-bubble-foreground);
7574
}
7675

77-
.teaching-bubble-content {
76+
.teaching-bubble-body {
77+
padding: 1rem;
7878
font-size: 1.1rem;
7979

8080
p {
81-
margin: .25rem 0 .5rem;
81+
margin: 0.5rem 0;
8282
}
8383
}
8484

85-
.teaching-bubble-footer {
85+
.teaching-bubble-navigation {
8686
display: flex;
8787
flex-direction: row;
8888
align-items: flex-end;
8989
justify-content: space-between;
9090

91+
.common-button.feedback-button {
92+
color: var(--teaching-bubble-foreground);
93+
}
94+
9195
.teaching-bubble-steps {
9296
font-size: .9rem;
9397
color: var(--teaching-bubble-foreground);
@@ -121,6 +125,19 @@
121125
}
122126
}
123127

128+
.teaching-bubble-footer {
129+
color: var(--pxt-neutral-alpha80);
130+
margin-top: 0.5rem;
131+
margin: 0; // negative margins compensate for the padding on the bubble
132+
padding: 0.5rem 1rem;
133+
border-top: 1px solid var(--pxt-neutral-alpha50);
134+
135+
.ai-footer .feedback-button.disabled {
136+
// Override the default disabled coloring
137+
color: var(--pxt-neutral-alpha80);
138+
}
139+
}
140+
124141
.teaching-bubble-close.common-button {
125142
position: absolute;
126143
right: 0.5rem;
@@ -148,7 +165,7 @@
148165
color: @highContrastTextColor;
149166
border: solid @highContrastTextColor;
150167

151-
.teaching-bubble-navigation>.common-button {
168+
.teaching-bubble-navigation-buttons>.common-button {
152169
color: @highContrastTextColor;
153170
border: solid @highContrastTextColor;
154171
}

react-common/styles/react-common.less

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
@import "controls/Accordion.less";
2929
@import "controls/CarouselNav.less";
3030
@import "controls/Feedback.less";
31+
@import "controls/ThumbsFeedback.less";
32+
@import "controls/AIFooter.less";
3133
@import "theming/base-theme.less";
3234
@import "./react-common-variables.less";
3335
@import "./semantic-ui-overrides.less";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.ai-explanation-container {
2+
display: flex;
3+
flex-direction: column;
4+
justify-content: flex-start;
5+
align-items: flex-start;
6+
width: 100%;
7+
height: 100%;
8+
white-space: pre-line; // Preserve new lines
9+
10+
.ai-footer {
11+
margin-top: 1rem;
12+
13+
.feedback-button.disabled {
14+
// Override the default disabled coloring
15+
color: var(--pxt-neutral1-foreground);
16+
}
17+
}
18+
}

theme/pxt.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
@import 'errorList';
2828
@import 'asset-editor';
2929
@import 'semantic-ui-overrides';
30+
@import 'ai-error-explanation-text';
3031

3132
@import 'light';
3233
@import 'accessibility';

0 commit comments

Comments
 (0)