Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 29 additions & 12 deletions teachertool/src/components/AskAIOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FocusTrap } from "react-common/components/controls/FocusTrap";
import { Accordion } from "react-common/components/controls/Accordion";
import { classList } from "react-common/components/util";

import { Strings } from "../constants";
import { Constants, Strings } from "../constants";
import { setAskAiOpen } from "../transforms/setAskAiOpen";
import { addAiQuestionCriteriaToChecklist } from "../transforms/addAiQuestionCriteriaToChecklist";
import { showToast } from "../transforms/showToast";
Expand Down Expand Up @@ -243,7 +243,11 @@ export const AskAIOverlay = () => {
<Accordion.Panel>
{customPrompts.length > 0 && (
<div className={css["custom-list"]}>
{customPrompts.map((p, index) => (
{customPrompts.map((p, index) => {
const trimmedText = p.text.trim();
const hasError = trimmedText && trimmedText.length < Constants.MinAIQuestionLength;

return (
<div key={p.id} className={css["custom-item"]}>
<div className={css["custom-header"]}>
<Checkbox
Expand All @@ -263,17 +267,30 @@ export const AskAIOverlay = () => {
/>
)}
</div>
<Textarea
id={`ask-ai-custom-text-${p.id}`}
className={css["textarea"]}
placeholder={Strings.CustomPromptPlaceholder}
initialValue={p.text}
maxLength={aiQuestionMaxLength}
showRemainingCharacterCount={100}
onChange={text => setCustomTextForId(p.id, text)}
/>
<div
className={classList(css["custom-textarea-wrapper"], hasError ? css["textarea-error"] : undefined)}
title={hasError ? Strings.QuestionTooShort : undefined}
>
<Textarea
id={`ask-ai-custom-text-${p.id}`}
className={css["textarea"]}
placeholder={Strings.CustomPromptPlaceholder}
initialValue={p.text}
maxLength={aiQuestionMaxLength}
showRemainingCharacterCount={100}
resize="vertical"
title={hasError ? Strings.QuestionTooShort : undefined}
onChange={text => setCustomTextForId(p.id, text)}
/>
{hasError && (
<i
className={classList(css["validation-icon"], "fas", "fa-exclamation-triangle")}
aria-hidden="true"
/>
)}
</div>
</div>
))}
)})}
</div>
)}

Expand Down
18 changes: 17 additions & 1 deletion teachertool/src/components/CriteriaInstanceDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useContext, useEffect, useMemo, useState } from "react";
import { Input } from "react-common/components/controls/Input";
import { Button } from "react-common/components/controls/Button";
import { AppStateContext } from "../state/appStateContext";
import { Strings, Ticks } from "../constants";
import { Constants, Strings, Ticks } from "../constants";
import { showModal } from "../transforms/showModal";
import { BlockPickerOptions } from "../types/modalOptions";
import { validateParameterValue } from "../utils/validateParameterValue";
Expand All @@ -21,13 +21,15 @@ interface InlineInputSegmentProps {
param: CriteriaParameterValue;
shouldExpand: boolean;
numeric: boolean;
minLength?: number;
}
const InlineInputSegment: React.FC<InlineInputSegmentProps> = ({
initialValue,
instance,
param,
shouldExpand,
numeric,
minLength,
}) => {
const [errorMessage, setErrorMessage] = useState(initialValue ? "" : Strings.ValueRequired);
const paramDefinition = useMemo(() => getParameterDefinition(instance.catalogCriteriaId, param.name), [param]);
Expand Down Expand Up @@ -58,6 +60,11 @@ const InlineInputSegment: React.FC<InlineInputSegmentProps> = ({
return;
}

if (minLength && initialValue.trim().length < minLength) {
setErrorMessage(Strings.QuestionTooShort);
return;
}

// We still allow some invalid values to be set on the parameter so the user can see what they typed
// and the associated error.
// Without this, we risk erroring too soon (i.e. typing in first digit of number with min > 10),
Expand All @@ -76,6 +83,10 @@ const InlineInputSegment: React.FC<InlineInputSegmentProps> = ({
setErrorMessage(Strings.ValueRequired);
}

if (minLength && newValue.trim().length < minLength) {
setErrorMessage(Strings.QuestionTooShort);
}

setParameterValue(instance.instanceId, param.name, newValue);
}

Expand Down Expand Up @@ -234,13 +245,18 @@ export const CriteriaInstanceDisplay: React.FC<CriteriaInstanceDisplayProps> = (
if (paramDef.type === "block") {
return <BlockInputSegment param={paramInstance} instance={criteriaInstance} />;
} else {
const minLength =
catalogCriteria?.use === "ai_question" && paramDef.name === "question"
? Constants.MinAIQuestionLength
: undefined;
return (
<InlineInputSegment
initialValue={paramInstance.value}
param={paramInstance}
instance={criteriaInstance}
shouldExpand={paramDef.type === "longString"}
numeric={paramDef.type === "number"}
minLength={minLength}
/>
);
}
Expand Down
34 changes: 34 additions & 0 deletions teachertool/src/components/styling/AskAIOverlay.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,40 @@

textarea[class*="common-textarea"] {
min-height: 6rem;
resize: vertical;
}

.custom-textarea-wrapper {
position: relative;
&.textarea-error {
:global(.common-textarea-group) {
border: solid 1px var(--pxt-colors-red-background);
background-color: var(--pxt-colors-red-alpha10);
transition: background-color 0.3s ease-in-out, border 0.3s ease-in-out;

&:focus::after,
&:focus-within::after {
border: solid 2px var(--pxt-colors-red-background);
}
}

textarea[class*="common-textarea"] {
padding-right: 2rem;
}

.validation-icon {
color: var(--pxt-colors-red-background) !important;
}
}
}

.validation-icon {
position: absolute;
top: 0.7rem;
right: 0.75rem;
font-size: 0.9rem;
pointer-events: none;
color: var(--pxt-colors-red-background);
}
}

Expand Down
14 changes: 8 additions & 6 deletions teachertool/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
namespace Misc {
export const LearnMoreLink = "https://makecode.microbit.org/teachertool"; // TODO: Replace with golink or aka.ms link
export const ExperienceId = "code-eval";
export const MinAIQuestionLength = 10;
}

export namespace Strings {
export const AppTitle = lf("Code Evaluation");
export const AppTitleShort = lf("Code Eval");
Expand All @@ -18,7 +24,7 @@ export namespace Strings {
export const AddCriteria = lf("Add Criteria");
export const AskAI = lf("Ask AI");
export const Custom = lf("Custom");
export const CustomPromptPlaceholder = lf("Write your own question here...");
export const CustomPromptPlaceholder = lf("Write your own question here (minimum {0} characters)...", Misc.MinAIQuestionLength);
export const Actions = lf("Actions");
export const RunOnLoad = lf("Evaluate on load");
export const RunOnLoadDescription = lf("Automatically evaluate a project when it is loaded");
Expand Down Expand Up @@ -52,6 +58,7 @@ export namespace Strings {
export const UnableToReachAI = lf("Unable to reach the AI service");
export const UnexpectedError = lf("An unexpected error occurred");
export const Dismiss = lf("Dismiss");
export const QuestionTooShort = lf("Question must be at least {0} characters", Misc.MinAIQuestionLength);
export const EvaluationComplete = lf("Evaluation complete");
export const UnableToEvaluatePartial = lf("Unable to evaluate some criteria");
export const GiveFeedback = lf("Give Feedback");
Expand Down Expand Up @@ -104,9 +111,4 @@ export namespace Ticks {
export const TermsOfUseClicked = "teachertool.termsofuse.clicked";
}

namespace Misc {
export const LearnMoreLink = "https://makecode.microbit.org/teachertool"; // TODO: Replace with golink or aka.ms link
export const ExperienceId = "code-eval";
}

export const Constants = Object.assign(Misc, { Strings, Ticks });
Loading