67 lines
2.4 KiB
TypeScript
67 lines
2.4 KiB
TypeScript
import { useRef, useState } from "react";
|
|
import { useSessionStore } from "../store/sessionStore";
|
|
import { resolve } from "../widgets";
|
|
import { postResponse } from "../api/sessions";
|
|
import type { UiResponse } from "../api/types";
|
|
import { ErrorBoundary } from "../components/ErrorBoundary";
|
|
|
|
export function WidgetHost({ sessionId }: { sessionId: string | null }) {
|
|
const pending = useSessionStore((s) => s.pendingWidget);
|
|
const clearPending = useSessionStore((s) => s.clearPending);
|
|
const pushToast = useSessionStore((s) => s.pushToast);
|
|
const setLastUserEntry = useSessionStore((s) => s.setLastUserEntry);
|
|
const [responding, setResponding] = useState(false);
|
|
const responseInFlight = useRef(false);
|
|
if (!pending) return null;
|
|
const Renderer = resolve(pending.widget);
|
|
const onRespond = async (r: UiResponse) => {
|
|
if (responseInFlight.current) return;
|
|
responseInFlight.current = true;
|
|
setResponding(true);
|
|
|
|
if (sessionId) {
|
|
try {
|
|
await postResponse(sessionId, r);
|
|
} catch (err) {
|
|
// Surface feedback and keep the widget pending so the user can retry.
|
|
pushToast({
|
|
level: "error",
|
|
text:
|
|
err instanceof Error
|
|
? `Failed to send response: ${err.message}`
|
|
: "Failed to send response.",
|
|
});
|
|
responseInFlight.current = false;
|
|
setResponding(false);
|
|
return;
|
|
}
|
|
}
|
|
responseInFlight.current = false;
|
|
setResponding(false);
|
|
setLastUserEntry({
|
|
kind: "choice",
|
|
text:
|
|
r.kind === "schema-linking"
|
|
? `${r.tables?.filter((t) => t.enacted).length ?? 0} tables curated`
|
|
: r.text ?? r.choices?.join(", ") ?? r.decision?.type ?? r.control ?? "(choice)",
|
|
});
|
|
clearPending();
|
|
};
|
|
// A malformed gate payload must not white-screen the whole app: isolate the
|
|
// widget render. resetKeys on the descriptor id so the next gate starts clean.
|
|
return (
|
|
<ErrorBoundary resetKeys={[pending.id]} label="step">
|
|
<div className="space-y-2">
|
|
<fieldset disabled={responding} className="min-w-0 border-0 p-0">
|
|
<Renderer descriptor={pending} onRespond={onRespond} sessionId={sessionId ?? undefined} />
|
|
</fieldset>
|
|
{responding && (
|
|
<p role="status" aria-live="polite" className="text-sm text-muted-foreground">
|
|
Sending response…
|
|
</p>
|
|
)}
|
|
</div>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|