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 (
Which would you like?
{candidates.map((c) => { const available = availableJourneys.includes(c); return ( ); })}
); } if (!uiHint || uiHint.kind === "text") return null; if (uiHint.kind === "multi_slot_form" && uiHint.fields && uiHint.fields.length > 0) { return ; } if (uiHint.kind === "dropdown" && uiHint.options && uiHint.options.length > 0) { return (
{uiHint.label || uiHint.slot || "Pick one"}
{uiHint.options.map((opt) => ( ))}
); } if (uiHint.kind === "date") { return ; } if (uiHint.kind === "confirm") { return (
); } return null; } function DateControl({ uiHint, onSubmit }: { uiHint: UiHint; onSubmit(text: string): void }) { const label = uiHint.label || uiHint.slot || "Pick a date"; return (
{label}
{ if (e.target.value) onSubmit(e.target.value); }} />
); } // ============================================================================ // 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>({}); 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 (
{uiHint.title &&
{uiHint.title}
}
Fill in what you can — leave the rest blank. You can also just type your answer in the chat box below if you prefer.
{fields.map((f) => ( setValues((s) => ({ ...s, [f.slot]: v }))} /> ))}
{filledCount > 0 ? `${filledCount} field${filledCount === 1 ? "" : "s"} filled` : "No fields filled yet"}
); } function MsfField({ field, value, onChange, }: { field: UiHintField; value: string; onChange(v: string): void }) { return ( ); } // 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 = { "1": "General", "2": "Capital", "3": "Dropship", "4": "Consignment" }; return m[opt] ? `${m[opt]}` : opt; } return opt; } function prettify(s: string): string { return s; }