import { useMemo, useState, type ReactNode } from "react"; import { QuestionnaireEngine, type FieldDefinition, type QuestionDefinition, type QuestionnaireDefinition, type Answers } from "../lib/questionnaire/questionnaire-engine"; import { QuestionnaireRenderer } from "../lib/react/QuestionnaireRenderer"; import { mockSave, mockUpload } from "./mock-persistence"; import { JsonViewer } from "./JsonViewer"; import { SchemaDiagram, type DiagramSelection } from "./SchemaDiagram"; import { ValidationEditor } from "./ValidationEditor"; import starter from "./editor-questionnaire.json"; const basic = starter as QuestionnaireDefinition; const types: QuestionDefinition["type"][] = ["text", "number", "date", "boolean", "select", "multiselect", "files"]; const json = (value: unknown) => JSON.stringify(value, null, 2); function Input({ label, value, onChange }: { label: string; value: string; onChange: (value: string) => void }) { return ; } function MoveButtons({ index, count, move, remove, name }: { index: number; count: number; move: (offset: number) => void; remove: () => void; name: string }) { return
; } function reordered(items: T[], index: number, offset: number) { const next = [...items]; [next[index], next[index + offset]] = [next[index + offset], next[index]]; return next; } function FieldsEditor({ fields, update, newId, selectedIndex }: { fields: FieldDefinition[]; update: (fields: FieldDefinition[]) => void; newId: () => string; selectedIndex?: number }) { function replace(index: number, field: FieldDefinition) { update(fields.map((item, i) => i === index ? field : item)); } return
{fields.map((field, index) => selectedIndex !== undefined && index !== selectedIndex ? null :
{field.type === "group" ? "Group" : `Question ${index + 1}`} update(reordered(fields, index, offset))} remove={() => update(fields.filter((_, i) => i !== index))} />
{field.type === "group" ? <> replace(index, { ...field, title })} /> replace(index, { ...field, when: when || undefined })} /> replace(index, { ...field, fields: children })} newId={newId} /> : <>
replace(index, { ...field, id })} />
replace(index, { ...field, label })} /> replace(index, { ...field, description: description || undefined })} />
replace(index, { ...field, when: when || undefined })} /> replace(index, { ...field, requiredWhen: requiredWhen || undefined })} />
replace(index, next)} /> {(field.type === "select" || field.type === "multiselect") &&
Options {field.options.map((option, optionIndex) =>
replace(index, { ...field, options: field.options.map((item, i) => i === optionIndex ? { ...item, value } : item) })} /> replace(index, { ...field, options: field.options.map((item, i) => i === optionIndex ? { ...item, label } : item) })} />
)}
} }
)} {selectedIndex === undefined &&
}
; } export function SchemaEditorPage() { const [definition, setDefinition] = useState(() => structuredClone(basic)); const [source, setSource] = useState(() => json(basic)); const [tab, setTab] = useState<"Visual" | "JSON">("Visual"); const [selection, setSelection] = useState(); const [playing, setPlaying] = useState(); const [run, setRun] = useState(0); const [result, setResult] = useState<{ answers: Answers; outcomes: Record }>(); const validation = useMemo(() => { try { return { definition: new QuestionnaireEngine(JSON.parse(source)).definition, error: "" }; } catch (cause) { return { definition: undefined, error: cause instanceof Error ? cause.message : String(cause) }; } }, [source]); function update(next: QuestionnaireDefinition) { setDefinition(next); setSource(json(next)); } function selectTab(next: "Visual" | "JSON") { if (next === "Visual") { if (!validation.definition) return; setDefinition(validation.definition); setSelection(undefined); } setTab(next); } function newId() { const ids = new Set(); function collect(fields: FieldDefinition[]) { fields.forEach(field => field.type === "group" ? collect(field.fields) : ids.add(field.id)); } definition.pages.forEach(page => collect(page.fields)); let index = 1; while (ids.has(`question${index}`)) index++; return `question${index}`; } let content: ReactNode; if (playing) { content =
Preview · {playing.title}
{result ?

Questionnaire completed

Your preview answers and outcomes:

:
setResult({ answers, outcomes })} />
}
; } else content =
{(["Visual", "JSON"] as const).map(mode => )}
{validation.error ? <>Fix the schema to play{tab === "JSON" ? " or open Visual" : ""}.
{validation.error}
: ✓ Ready to play · {validation.definition!.pages.length} page{validation.definition!.pages.length === 1 ? "" : "s"}}
{tab === "JSON" ?

Edit the full schema, including validation rules, routes, derived values, and outcomes.