diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx index 1effe55ef41..fce617b4c47 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx @@ -31,7 +31,7 @@ interface JsonEditorProps { validation: SystemOnePayloadValidation; } -function ValidationStatus({ validation }: { validation: SystemOnePayloadValidation }) { +export function ValidationStatus({ validation }: { validation: SystemOnePayloadValidation }) { const errorCount = validation.issues.filter((issue) => issue.severity === "error").length; if (errorCount > 0) { return ( @@ -43,7 +43,7 @@ function ValidationStatus({ validation }: { validation: SystemOnePayloadValidati return Valid payload; } -function IssueList({ id, validation }: { id: string; validation: SystemOnePayloadValidation }) { +export function IssueList({ id, validation }: { id: string; validation: SystemOnePayloadValidation }) { if (validation.issues.length === 0) { return (

diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneForm.tsx new file mode 100644 index 00000000000..d799afd13ea --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/SystemOneForm.tsx @@ -0,0 +1,337 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; +import { cn } from "@/lib/cva.config"; +import { Plus, Trash2 } from "lucide-react"; +import { useId, useMemo, useState, type ReactNode } from "react"; +import { IssueList, ValidationStatus } from "./JsonEditor"; +import { + blankQuestion, + freeName, + readForm, + removeAt, + renameKey, + replaceAt, + retype, + withNoulCriterion, + withoutKey, + type ChoiceQuestion, + type FormPayload, + type FormQuestion, + type NoulQuestion, + type QuestionType, + type ScoreQuestion, +} from "./lib/formPayload"; +import type { SystemOnePayloadValidation } from "./lib/validatePayload"; + +const QUESTION_TYPES: readonly QuestionType[] = ["choice", "noul", "score"]; +const TYPE_LABELS: Record = { choice: "Choice", noul: "Yes / no", score: "Score" }; + +const isQuestionType = (value: unknown): value is QuestionType => QUESTION_TYPES.some((type) => type === value); + +interface SystemOneFormProps { + value: string; + onChange: (value: string) => void; + validation: SystemOnePayloadValidation; + onOpenJson: () => void; +} + +interface QuestionEditorProps { + question: Q; + onChange: (question: FormQuestion) => void; +} + +interface KeyInputProps { + value: string; + taken: readonly string[]; + onCommit: (value: string) => void; + label: string; + placeholder?: string; +} + +function keyError(next: string, current: string, taken: readonly string[]): string | undefined { + if (next === "") { + return "Enter a name"; + } + if (next !== current && taken.includes(next)) { + return `"${next}" is already used`; + } + return undefined; +} + +function KeyInput({ value, taken, onCommit, label, placeholder }: KeyInputProps) { + const errorId = useId(); + const [draft, setDraft] = useState(value); + const next = draft.trim(); + const error = keyError(next, value, taken); + + function commit() { + if (error === undefined && next !== value) { + onCommit(next); + return; + } + setDraft(value); + } + + return ( +

+ setDraft(event.target.value)} + onBlur={commit} + /> + {error !== undefined && ( +

+ {error} +

+ )} +
+ ); +} + +function RemoveButton({ label, onClick }: { label: string; onClick: () => void }) { + return ( + + ); +} + +function AddButton({ children, onClick }: { children: ReactNode; onClick: () => void }) { + return ( + + ); +} + +function ChoiceOptions({ question, onChange }: QuestionEditorProps) { + const labels = Object.keys(question.criteria); + const writeCriteria = (criteria: Record) => onChange({ ...question, criteria }); + + return ( +
+ Options + {Object.entries(question.criteria).map(([label, description], index) => ( +
+ writeCriteria(renameKey(question.criteria, label, next))} + /> + writeCriteria({ ...question.criteria, [label]: event.target.value })} + /> + writeCriteria(withoutKey(question.criteria, label))} + /> +
+ ))} + writeCriteria({ ...question.criteria, [freeName(labels, "option")]: "" })}> + Add option + +
+ ); +} + +function NoulCriteria({ question, onChange }: QuestionEditorProps) { + const yesId = useId(); + const noId = useId(); + + return ( +
+
+ + onChange(withNoulCriterion(question, "true", event.target.value))} + /> +
+
+ + onChange(withNoulCriterion(question, "false", event.target.value))} + /> +
+
+ ); +} + +function ScoreLevels({ question, onChange }: QuestionEditorProps) { + const writeCriteria = (criteria: string[]) => onChange({ ...question, criteria }); + + return ( +
+ Levels, lowest first + {question.criteria.map((level, index) => ( +
+ {index} + writeCriteria(replaceAt(question.criteria, index, event.target.value))} + /> + writeCriteria(removeAt(question.criteria, index))} + /> +
+ ))} + writeCriteria([...question.criteria, ""])}>Add level +
+ ); +} + +function CriteriaEditor({ question, onChange }: QuestionEditorProps) { + switch (question.type) { + case "choice": + return ; + case "noul": + return ; + case "score": + return ; + } +} + +interface QuestionCardProps extends QuestionEditorProps { + name: string; + taken: readonly string[]; + onRename: (name: string) => void; + onRemove: () => void; +} + +function QuestionCard({ name, question, taken, onChange, onRename, onRemove }: QuestionCardProps) { + const instructionsId = useId(); + + return ( +
+
+ + + +
+
+ +