Spaces:
Runtime error
Runtime error
| import { useState } from "react"; | |
| import type { UiHint, UiHintField } from "../types"; | |
| type Props = { | |
| uiHint: UiHint | null; | |
| candidates?: string[]; | |
| availableJourneys?: string[]; | |
| onSubmit(text: string): void; | |
| }; | |
| /** | |
| * SmartControl renders the right interactive widget under the latest assistant bubble | |
| * based on the agent's ui_hint. We always offer the text composer below, so this is | |
| * a *shortcut* for the user, not a forced channel. | |
| * | |
| * - multi_slot_form -> full form with dropdowns / number / date inputs | |
| * - dropdown -> chip grid of vocabulary values | |
| * - date -> native date input | |
| * - confirm -> Yes / Not yet | |
| * - candidates -> journey-choice cards (ambiguity) | |
| * - text -> nothing (composer handles it) | |
| */ | |
| export function SmartControl({ uiHint, candidates = [], availableJourneys = [], onSubmit }: Props) { | |
| // Ambiguity / journey-choice always wins if present. | |
| if (candidates.length > 0) { | |
| return ( | |
| <div className="smart-control"> | |
| <div className="smart-label">Which would you like?</div> | |
| <div className="option-grid"> | |
| {candidates.map((c) => { | |
| const available = availableJourneys.includes(c); | |
| return ( | |
| <button | |
| key={c} | |
| className="option-chip option-chip--journey" | |
| onClick={() => onSubmit(c)} | |
| title={available ? "Click to choose" : "Recognised, but not yet implemented"} | |
| > | |
| <small>{available ? "available" : "coming soon"}</small> | |
| <span>{c}</span> | |
| </button> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (!uiHint || uiHint.kind === "text") return null; | |
| if (uiHint.kind === "multi_slot_form" && uiHint.fields && uiHint.fields.length > 0) { | |
| return <MultiSlotForm uiHint={uiHint} onSubmit={onSubmit} />; | |
| } | |
| if (uiHint.kind === "dropdown" && uiHint.options && uiHint.options.length > 0) { | |
| return ( | |
| <div className="smart-control"> | |
| <div className="smart-label"> | |
| {uiHint.label || uiHint.slot || "Pick one"} | |
| </div> | |
| <div className="option-grid"> | |
| {uiHint.options.map((opt) => ( | |
| <button key={opt} className="option-chip" onClick={() => onSubmit(prettify(opt))}> | |
| {humanLabelForOption(uiHint.slot, opt)} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| if (uiHint.kind === "date") { | |
| return <DateControl uiHint={uiHint} onSubmit={onSubmit} />; | |
| } | |
| if (uiHint.kind === "confirm") { | |
| return ( | |
| <div className="smart-control"> | |
| <div className="option-grid"> | |
| <button className="option-chip" onClick={() => onSubmit("yes, go ahead")}> | |
| Yes, create it | |
| </button> | |
| <button className="option-chip" onClick={() => onSubmit("not yet")}> | |
| Not yet | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return null; | |
| } | |
| function DateControl({ uiHint, onSubmit }: { uiHint: UiHint; onSubmit(text: string): void }) { | |
| const label = uiHint.label || uiHint.slot || "Pick a date"; | |
| return ( | |
| <div className="smart-control"> | |
| <div className="smart-label">{label}</div> | |
| <input | |
| type="date" | |
| className="option-chip" | |
| style={{ width: 200 }} | |
| onChange={(e) => { | |
| if (e.target.value) onSubmit(e.target.value); | |
| }} | |
| /> | |
| </div> | |
| ); | |
| } | |
| // ============================================================================ | |
| // Multi-slot form — beautiful in-theme card with dropdowns / number / date | |
| // ============================================================================ | |
| function MultiSlotForm({ uiHint, onSubmit }: { uiHint: UiHint; onSubmit(text: string): void }) { | |
| const fields = uiHint.fields ?? []; | |
| const [values, setValues] = useState<Record<string, string>>({}); | |
| const filledCount = Object.values(values).filter((v) => v && v.trim()).length; | |
| function buildMessage(): string { | |
| // Compose a natural-sounding sentence the slot extractor can parse. We use a templated | |
| // phrasing that maps slot → English fragment (the labels come from the form fields). | |
| const parts: string[] = []; | |
| for (const f of fields) { | |
| const v = (values[f.slot] || "").trim(); | |
| if (!v) continue; | |
| parts.push(`${f.label} ${v}`); | |
| } | |
| return parts.join(", ") + "."; | |
| } | |
| function handleSubmit() { | |
| if (filledCount === 0) return; | |
| onSubmit(buildMessage()); | |
| } | |
| return ( | |
| <div className="msf"> | |
| {uiHint.title && <div className="msf__title">{uiHint.title}</div>} | |
| <div className="msf__hint"> | |
| Fill in what you can — leave the rest blank. You can also just type your answer in | |
| the chat box below if you prefer. | |
| </div> | |
| <div className="msf__grid"> | |
| {fields.map((f) => ( | |
| <MsfField | |
| key={f.slot} | |
| field={f} | |
| value={values[f.slot] || ""} | |
| onChange={(v) => setValues((s) => ({ ...s, [f.slot]: v }))} | |
| /> | |
| ))} | |
| </div> | |
| <div className="msf__actions"> | |
| <span className="msf__counter"> | |
| {filledCount > 0 ? `${filledCount} field${filledCount === 1 ? "" : "s"} filled` : "No fields filled yet"} | |
| </span> | |
| <button | |
| className="msf__submit" | |
| onClick={handleSubmit} | |
| disabled={filledCount === 0} | |
| > | |
| Submit | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function MsfField({ | |
| field, value, onChange, | |
| }: { field: UiHintField; value: string; onChange(v: string): void }) { | |
| return ( | |
| <label className="msf__field"> | |
| <span className="msf__label">{field.label}</span> | |
| {field.kind === "dropdown" ? ( | |
| <select | |
| className="msf__input msf__input--select" | |
| value={value} | |
| onChange={(e) => onChange(e.target.value)} | |
| > | |
| <option value="">— select —</option> | |
| {(field.options || []).map((o) => ( | |
| <option key={o} value={o}>{o}</option> | |
| ))} | |
| </select> | |
| ) : field.kind === "date" ? ( | |
| <input | |
| className="msf__input" | |
| type="date" | |
| value={value} | |
| onChange={(e) => onChange(e.target.value)} | |
| /> | |
| ) : field.kind === "number" ? ( | |
| <input | |
| className="msf__input" | |
| type="number" | |
| inputMode="numeric" | |
| step="any" | |
| value={value} | |
| placeholder={field.placeholder || ""} | |
| onChange={(e) => onChange(e.target.value)} | |
| /> | |
| ) : ( | |
| <input | |
| className="msf__input" | |
| type="text" | |
| value={value} | |
| placeholder={field.placeholder || ""} | |
| onChange={(e) => onChange(e.target.value)} | |
| /> | |
| )} | |
| </label> | |
| ); | |
| } | |
| // Render a friendly label for known coded slots. potypeenum 1/2/3/4 -> General/Capital/Dropship/Consignment. | |
| function humanLabelForOption(slot: string | undefined, opt: string): string { | |
| if (!slot) return opt; | |
| if (slot.toLowerCase() === "potypeenum") { | |
| const m: Record<string, string> = { "1": "General", "2": "Capital", "3": "Dropship", "4": "Consignment" }; | |
| return m[opt] ? `${m[opt]}` : opt; | |
| } | |
| return opt; | |
| } | |
| function prettify(s: string): string { return s; } | |