ramco-chat / frontend /src /components /SmartControl.tsx
VizuaraAI's picture
deploy: ramco-chat (FastAPI + React, Docker SDK)
8f36e11 verified
Raw
History Blame Contribute Delete
7.32 kB
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; }