diff --git a/frontend/src/shell/WidgetHost.tsx b/frontend/src/shell/WidgetHost.tsx
index 068b6d96..0436e357 100644
--- a/frontend/src/shell/WidgetHost.tsx
+++ b/frontend/src/shell/WidgetHost.tsx
@@ -6,10 +6,25 @@ import type { UiResponse } from "../api/types";
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);
if (!pending) return null;
const Renderer = resolve(pending.widget);
const onRespond = async (r: UiResponse) => {
- if (sessionId) await postResponse(sessionId, r);
+ 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
+ ? `Invio risposta fallito: ${err.message}`
+ : "Invio risposta fallito.",
+ });
+ return;
+ }
+ }
clearPending();
};
return ;
diff --git a/frontend/src/shell/WorkflowBar.test.tsx b/frontend/src/shell/WorkflowBar.test.tsx
new file mode 100644
index 00000000..f32c0311
--- /dev/null
+++ b/frontend/src/shell/WorkflowBar.test.tsx
@@ -0,0 +1,22 @@
+// frontend/src/shell/WorkflowBar.test.tsx
+import { render, screen } from "@testing-library/react";
+import { WorkflowBar } from "./WorkflowBar";
+import { useSessionStore } from "../store/sessionStore";
+
+beforeEach(() => useSessionStore.getState().resetSession());
+
+test("highlights the phase matching currentPhase", () => {
+ useSessionStore.getState().setPhase("F4");
+ render();
+ expect(screen.getByTestId("phase-F4")).toHaveAttribute("data-active", "true");
+ // Other phases are not highlighted.
+ expect(screen.getByTestId("phase-F1")).toHaveAttribute("data-active", "false");
+ expect(screen.getByTestId("phase-F8")).toHaveAttribute("data-active", "false");
+});
+
+test("no phase highlighted when currentPhase is null", () => {
+ render();
+ for (const id of ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8"]) {
+ expect(screen.getByTestId(`phase-${id}`)).toHaveAttribute("data-active", "false");
+ }
+});
diff --git a/frontend/src/store/sessionStore.test.ts b/frontend/src/store/sessionStore.test.ts
index 00d43a9c..1a531a2c 100644
--- a/frontend/src/store/sessionStore.test.ts
+++ b/frontend/src/store/sessionStore.test.ts
@@ -24,3 +24,21 @@ test("clearPending removes the widget", () => {
useSessionStore.getState().clearPending();
expect(useSessionStore.getState().pendingWidget).toBeNull();
});
+
+test("ui_request sets currentPhase normalized to the short id", () => {
+ useSessionStore
+ .getState()
+ .applyEvent({ type: "ui_request", ui_request: { id: "u", widget: "select", phase: "F4_schema_linking" } });
+ expect(useSessionStore.getState().currentPhase).toBe("F4");
+});
+
+test("ui_request without a phase keeps the existing currentPhase", () => {
+ useSessionStore.getState().setPhase("F2");
+ useSessionStore.getState().applyEvent({ type: "ui_request", ui_request: { id: "u", widget: "select" } });
+ expect(useSessionStore.getState().currentPhase).toBe("F2");
+});
+
+test("pushToast appends an error toast", () => {
+ useSessionStore.getState().pushToast({ level: "error", text: "boom" });
+ expect(useSessionStore.getState().toasts.at(-1)).toEqual({ level: "error", text: "boom" });
+});
diff --git a/frontend/src/store/sessionStore.ts b/frontend/src/store/sessionStore.ts
index 51223ced..2a916ac4 100644
--- a/frontend/src/store/sessionStore.ts
+++ b/frontend/src/store/sessionStore.ts
@@ -16,6 +16,7 @@ interface SessionState {
clearPending: () => void;
resetSession: () => void;
setPhase: (phase: string | null) => void;
+ pushToast: (toast: { level: string; text: string }) => void;
}
const empty = {
@@ -30,7 +31,13 @@ export const useSessionStore = create((set) => ({
...empty,
applyEvent: (e) =>
set((st) => {
- if (e.type === "ui_request") return { pendingWidget: e.ui_request };
+ if (e.type === "ui_request")
+ return {
+ pendingWidget: e.ui_request,
+ currentPhase: e.ui_request.phase
+ ? e.ui_request.phase.split("_")[0]
+ : st.currentPhase,
+ };
if (e.type === "text_delta") {
const t = [...st.transcript];
const last = t.at(-1);
@@ -45,4 +52,5 @@ export const useSessionStore = create((set) => ({
clearPending: () => set({ pendingWidget: null }),
resetSession: () => set({ ...empty }),
setPhase: (phase) => set({ currentPhase: phase }),
+ pushToast: (toast) => set((st) => ({ toasts: [...st.toasts, toast] })),
}));