Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Teacher Tool: Copilot Criteria Support #9953

Merged
merged 32 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2372ed4
One approach to having a validator plan for AI questions, still sever…
thsparks Mar 28, 2024
eb7b97e
Merge branch 'master' of https://github.com/microsoft/pxt into thspar…
thsparks Apr 3, 2024
b210067
Move ai eval into teacher tool. (Does not remove from editor yet). No…
thsparks Apr 3, 2024
ed379e2
Remove ai eval from editor. Not sure if we really want to expose this…
thsparks Apr 3, 2024
fb952bb
Fix Updating Result Display
thsparks Apr 3, 2024
7f77388
Change "Not Evaluated" to "N/A"
thsparks Apr 3, 2024
189e26f
Capitalize
thsparks Apr 3, 2024
d78ce27
Resize text area automatically when autoResize is true.
thsparks Apr 4, 2024
e5dce83
(Untested) Backend Request Changes to askCopilotQuestion
thsparks Apr 4, 2024
3235655
Remove target parameter, change SHAREID to SHARE_ID
thsparks Apr 4, 2024
806a65c
Remove evaluating... from the dropdown.
thsparks Apr 4, 2024
ed5bc8c
Use a "key" field for system parameters instead of "default"
thsparks Apr 4, 2024
850989b
Do not print N/A results
thsparks Apr 4, 2024
b35f451
Vertical resize textarea when it's horizontally resized if autoresize…
thsparks Apr 5, 2024
3ed5832
Hide notes while evaluation is in progress.
thsparks Apr 5, 2024
84377d8
New loading indicator for results
thsparks Apr 5, 2024
7b6a85b
Merge branch 'master' of https://github.com/microsoft/pxt into thspar…
thsparks Apr 5, 2024
f09f969
Make test criteria appear at the top and put shared before specific.
thsparks Apr 8, 2024
f31bd65
Add copilot url parameter for setting the endpoint to use.
thsparks Apr 9, 2024
ffa278a
Rings instead of circles for loading
thsparks Apr 9, 2024
328b16d
Make copilot url param implicitly enable test criteria
thsparks Apr 9, 2024
ba54a94
Merge branch 'master' of https://github.com/microsoft/pxt into thspar…
thsparks Apr 9, 2024
59dd7de
Remove newline change
thsparks Apr 9, 2024
d2a430c
Clarify the different set result transforms
thsparks Apr 9, 2024
52dccca
Comment update
thsparks Apr 9, 2024
0d83f85
Prettier
thsparks Apr 9, 2024
38144f9
Less wordy description for AI question
thsparks Apr 9, 2024
4b5cc24
import useEffect
thsparks Apr 9, 2024
e5bba55
askCopilotQuestion -> askCopilotQuestionAsync
thsparks Apr 9, 2024
68ccfcf
Remove trailing slash assumption
thsparks Apr 9, 2024
b862442
Remove redundant "React."
thsparks Apr 9, 2024
476a8e1
Remove value state. I don't think this is necessary.
thsparks Apr 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions common-docs/teachertool/test/catalog-shared.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
{
"criteria": [
{
"id": "499F3572-E655-4DEE-953B-5F26BF0191D7",
"use": "ai_question",
"template": "Ask Copilot: ${question}",
"description": "Experimental: AI outputs may not be accurate. Use with caution and always review responses.",
"docPath": "/teachertool",
"params": [
{
"name": "question",
"type": "longString",
"paths": ["checks[0].question"]
},
{
"name": "shareid",
"type": "system",
"key": "SHARE_ID",
"paths": ["checks[0].shareId"]
}
]
},
{
"id": "7AE7EA2A-3AC8-42DC-89DB-65E3AE157156",
"use": "block_comment_used",
Expand Down Expand Up @@ -35,26 +55,6 @@
}
]
},
{
"id": "499F3572-E655-4DEE-953B-5F26BF0191D7",
"use": "ai_question",
"template": "Ask Copilot: ${question}",
"description": "Experimental: AI outputs are inherently nondeterministic and may not be accurate. Use with caution and always review responses.",
"docPath": "/teachertool",
"params": [
{
"name": "question",
"type": "longString",
"paths": ["checks[0].question"]
},
{
"name": "shareid",
"type": "system",
"key": "SHARE_ID",
"paths": ["checks[0].shareId"]
}
]
},
{
"id": "B8987394-1531-4C71-8661-BE4086CE0C6E",
"use": "n_loops",
Expand Down
9 changes: 8 additions & 1 deletion localtypings/validatorPlan.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ declare namespace pxt.blocks {
}

export interface EvaluationResult {
result: boolean;
result?: boolean;
notes?: string;
}

export interface BlockFieldValueExistsCheck extends ValidatorCheckBase {
Expand All @@ -50,4 +51,10 @@ declare namespace pxt.blocks {
fieldValue: string;
blockType: string;
}

export interface AiQuestionValidatorCheck extends ValidatorCheckBase {
validator: "aiQuestion";
question: string;
shareId: string;
}
}
41 changes: 38 additions & 3 deletions react-common/components/controls/Textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,47 @@ export const Textarea = (props: TextareaProps) => {

const [value, setValue] = React.useState(initialValue || "");
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const previousWidthRef = React.useRef<number>(0);

const fitVerticalSizeToContent = () => {
if (!textareaRef.current) {
return;
}

textareaRef.current.style.height = "1px";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
}

React.useEffect(() => {
setValue(initialValue)

if (autoResize && textareaRef.current) {
fitVerticalSizeToContent();
}
}, [initialValue])

React.useEffect(() => {
if (!autoResize) {
return () => {};
}

const observer = new ResizeObserver((entries) => {
// If the width has changed, we need to update the vertical height to account for it.
const width = entries[0].contentRect.width;
if (previousWidthRef.current != width) {
requestAnimationFrame(() => fitVerticalSizeToContent());
previousWidthRef.current = width;
}
});

if (textareaRef.current) {
observer.observe(textareaRef.current);
}

return () => {
observer.disconnect();
}
}, [autoResize]);

const changeHandler = (e: React.ChangeEvent<any>) => {
const newValue = (e.target as any).value;
Expand All @@ -61,8 +97,7 @@ export const Textarea = (props: TextareaProps) => {
onChange(newValue);
}
if (autoResize && textareaRef.current) {
textareaRef.current.style.height = "1px";
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
fitVerticalSizeToContent();
}
}

Expand Down Expand Up @@ -108,4 +143,4 @@ export const Textarea = (props: TextareaProps) => {
</div>
</div>
);
}
}
25 changes: 11 additions & 14 deletions teachertool/src/components/CriteriaEvalResultDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,48 @@ import { useMemo } from "react";
import { setEvalResultOutcome } from "../transforms/setEvalResultOutcome";
import { Dropdown, DropdownItem } from "react-common/components/controls/Dropdown";
import { EvaluationStatus } from "../types/criteria";
import css from "./styling/EvalResultDisplay.module.scss";
import { classList } from "react-common/components/util";
import css from "./styling/EvalResultDisplay.module.scss";

interface CriteriaEvalResultProps {
result: EvaluationStatus;
criteriaId: string;
}

const itemIdToCriteriaResult: pxt.Map<EvaluationStatus> = {
evaluating: EvaluationStatus.InProgress,
notevaluated: EvaluationStatus.CompleteWithNoResult,
fail: EvaluationStatus.Fail,
pass: EvaluationStatus.Pass,
pending: EvaluationStatus.Pending,
};

const criteriaResultToItemId: pxt.Map<string> = {
[EvaluationStatus.InProgress]: "evaluating",
[EvaluationStatus.CompleteWithNoResult]: "notevaluated",
[EvaluationStatus.Fail]: "fail",
[EvaluationStatus.Pass]: "pass",
[EvaluationStatus.Pending]: "pending",
};

const dropdownItems: DropdownItem[] = [
{
id: "evaluating",
title: lf("evaluating..."),
label: lf("evaluating..."),
},
{
id: "notevaluated",
title: lf("not evaluated"),
label: lf("not evaluated"),
title: lf("not applicable"),
label: lf("N/A"),
},
{
id: "fail",
title: lf("needs work"),
label: lf("needs work"),
label: lf("Needs work"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to ask about these. I don't really have a strong preference either way, but it was all lower case in the design which is why these were lowercase at the start. Do we know if there's a design pattern that we should follow for these?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not aware of any pattern, but I think it looks cleaner with capitalized first letters when shown in the page.

},
{
id: "pass",
title: lf("looks good!"),
label: lf("looks good!"),
label: lf("Looks good!"),
},
{
id: "pending",
title: lf("not started"),
label: lf("not started"),
label: lf("Not started"),
},
];

Expand All @@ -62,7 +55,11 @@ export const CriteriaEvalResultDropdown: React.FC<CriteriaEvalResultProps> = ({
<Dropdown
id="project-eval-result-dropdown"
selectedId={selectedResult}
className={classList("rounded", selectedResult)}
className={classList(
"rounded",
selectedResult,
selectedResult === "notevaluated" ? css["no-print"] : undefined
)}
items={dropdownItems}
onItemSelected={id => setEvalResultOutcome(criteriaId, itemIdToCriteriaResult[id])}
/>
Expand Down
49 changes: 32 additions & 17 deletions teachertool/src/components/CriteriaResultEntry.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { useState, useContext, useRef } from "react";
import { useState, useContext, useEffect } from "react";
import css from "./styling/EvalResultDisplay.module.scss";
import { AppStateContext } from "../state/appStateContext";
import { classList } from "react-common/components/util";
Expand All @@ -10,6 +10,8 @@ import { CriteriaEvalResultDropdown } from "./CriteriaEvalResultDropdown";
import { DebouncedTextarea } from "./DebouncedTextarea";
import { getCatalogCriteriaWithId, getCriteriaInstanceWithId } from "../state/helpers";
import { ReadOnlyCriteriaDisplay } from "./ReadonlyCriteriaDisplay";
import { EvaluationStatus } from "../types/criteria";
import { ThreeDotsLoadingDisplay } from "./ThreeDotsLoadingDisplay";

interface AddNotesButtonProps {
criteriaId: string;
Expand All @@ -36,11 +38,11 @@ const AddNotesButton: React.FC<AddNotesButtonProps> = ({ criteriaId, setShowInpu

interface CriteriaResultNotesProps {
criteriaId: string;
notes?: string;
}

const CriteriaResultNotes: React.FC<CriteriaResultNotesProps> = ({ criteriaId, notes }) => {
const CriteriaResultNotes: React.FC<CriteriaResultNotesProps> = ({ criteriaId }) => {
const { state: teacherTool } = useContext(AppStateContext);

const onTextChange = (str: string) => {
setEvalResultNotes(criteriaId, str);
};
Expand All @@ -52,7 +54,7 @@ const CriteriaResultNotes: React.FC<CriteriaResultNotesProps> = ({ criteriaId, n
ariaLabel={lf("Feedback regarding the criteria result")}
label={lf("Feedback")}
title={lf("Write your notes here")}
initialValue={teacherTool.evalResults[criteriaId]?.notes ?? undefined}
initialValue={teacherTool.evalResults[criteriaId]?.notes}
autoResize={true}
onChange={onTextChange}
autoComplete={false}
Expand All @@ -73,30 +75,43 @@ export const CriteriaResultEntry: React.FC<CriteriaResultEntryProps> = ({ criter
const criteriaInstance = getCriteriaInstanceWithId(teacherTool, criteriaId);
const catalogCriteria = criteriaInstance ? getCatalogCriteriaWithId(criteriaInstance.catalogCriteriaId) : undefined;

useEffect(() => {
if (!showInput && teacherTool.evalResults[criteriaId]?.notes) {
setShowInput(true);
}
}, [teacherTool.evalResults[criteriaId]?.notes]);

const isInProgress = teacherTool.evalResults[criteriaId].result === EvaluationStatus.InProgress;
return (
<>
{catalogCriteria && (
<div className={css["specific-criteria-result"]} key={criteriaId}>
<div className={css["specific-criteria-result"]} key={criteriaId} aria-busy={isInProgress}>
<div className={css["result-details"]}>
<ReadOnlyCriteriaDisplay
catalogCriteria={catalogCriteria}
criteriaInstance={criteriaInstance}
showDescription={false}
/>
<CriteriaEvalResultDropdown
result={teacherTool.evalResults[criteriaId].result}
criteriaId={criteriaId}
/>
</div>
<div
className={classList(
css["result-notes"],
teacherTool.evalResults[criteriaId]?.notes ? undefined : css["no-print"]
{!isInProgress && (
<CriteriaEvalResultDropdown
result={teacherTool.evalResults[criteriaId].result}
criteriaId={criteriaId}
/>
)}
>
{!showInput && <AddNotesButton criteriaId={criteriaId} setShowInput={setShowInput} />}
{showInput && <CriteriaResultNotes criteriaId={criteriaId} />}
</div>
{isInProgress ? (
<ThreeDotsLoadingDisplay className={css["loading-display"]} />
) : (
<div
className={classList(
css["result-notes"],
teacherTool.evalResults[criteriaId]?.notes ? undefined : css["no-print"]
)}
>
{!showInput && <AddNotesButton criteriaId={criteriaId} setShowInput={setShowInput} />}
{showInput && <CriteriaResultNotes criteriaId={criteriaId} />}
</div>
)}
</div>
)}
</>
Expand Down
17 changes: 17 additions & 0 deletions teachertool/src/components/ThreeDotsLoadingDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { classList } from "react-common/components/util";
import css from "./styling/ThreeDotsLoadingDisplay.module.scss";
import { Strings } from "../constants";

export interface ThreeDotsLoadingDisplayProps {
className?: string;
}
// Three dots that move up and down in a wave pattern.
export const ThreeDotsLoadingDisplay: React.FC<ThreeDotsLoadingDisplayProps> = ({ className }) => {
return (
<div className={classList(className, css["loading-ellipsis"])} aria-label={Strings.Loading}>
<i className={classList("far fa-circle", css["circle"], css["circle-1"])} aria-hidden={true} />
<i className={classList("far fa-circle", css["circle"], css["circle-2"])} aria-hidden={true} />
<i className={classList("far fa-circle", css["circle"], css["circle-3"])} aria-hidden={true} />
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
min-height: 9rem;
border-bottom: solid 1px var(--pxt-content-accent);
break-inside: avoid;

.loading-display {
padding-top: 0;
flex-grow: 1;
}
}

.result-details {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.loading-ellipsis {
display: flex;
flex-direction: row;
gap: 1rem;
padding-top: 1rem;
align-items: center;
color: var(--pxt-content-foreground);

.circle {
font-size: 1rem;

@keyframes bounce {
0% {
transform: translateY(0);
}

25% {
transform: translateY(-0.5rem);
}

50% {
transform: translateY(0);
}

100% {
transform: translateY(0);
}
}

animation: bounce 750ms infinite ease-in-out;
}

.circle-1 {
animation-delay: 0ms;
}

.circle-2 {
animation-delay: 125ms;
}

.circle-3 {
animation-delay: 250ms;
}
}
1 change: 1 addition & 0 deletions teachertool/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export namespace Strings {
export const ValueRequired = lf("Value Required");
export const AddSelected = lf("Add Selected");
export const Continue = lf("Continue");
export const Loading = lf("Loading...");
}

export namespace Ticks {
Expand Down
Loading