@alexkroman1/aai-cli 6.11.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{_agent-DpH2pBJd.mjs → _agent-BTwZJu4P.mjs} +2 -2
- package/dist/{_bundler-C2BDwU7r.mjs → _bundler-CDuYl5Gb.mjs} +2 -2
- package/dist/_dev-env.d.ts +22 -2
- package/dist/{_dev-server-LLLWnFBs.mjs → _dev-server-Me06wgeF.mjs} +38 -47
- package/dist/_dev-server.d.ts +0 -17
- package/dist/{_init-C57bWAQ8.mjs → _init-BhEfQ6Yi.mjs} +3 -2
- package/dist/{_preflight-BtfaYtbE.mjs → _preflight-Co1G9Jww.mjs} +1 -1
- package/dist/_preflight.d.ts +1 -1
- package/dist/_resource-commands.d.ts +23 -0
- package/dist/{_slug-api-BxrsJXVI.mjs → _slug-api-z-USxPtL.mjs} +16 -12
- package/dist/_slug-api.d.ts +5 -2
- package/dist/{_studio-U3jhKlA3.mjs → _studio-DR5yzZ1T.mjs} +2 -1
- package/dist/{_templates-Dxb_P2Wz.mjs → _templates-4WcKOjS5.mjs} +5 -6
- package/dist/_templates.d.ts +3 -4
- package/dist/_workflow-scan.d.ts +108 -0
- package/dist/{build-Dfza2pRI.mjs → build-BBxf8vom.mjs} +6 -3
- package/dist/cli.mjs +346 -349
- package/dist/{client-bundler-BEIqgOtd.mjs → client-bundler-CWnG42cU.mjs} +2 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-vbpjGzqs.mjs → delete-Dl_LSfvI.mjs} +3 -3
- package/dist/{deploy-B8lubiRT.mjs → deploy-DRC8jWr8.mjs} +5 -5
- package/dist/{dev-Uq5ujP8a.mjs → dev-BvcctCn2.mjs} +1 -1
- package/dist/eval-DfQ8cm5p.mjs +56 -0
- package/dist/eval.d.ts +43 -0
- package/dist/{init-l6xfU4xX.mjs → init-CUo87gcI.mjs} +3 -3
- package/dist/{login-C71-qz8F.mjs → login-BBuM1sxH.mjs} +3 -4
- package/dist/{logs-BWVGSjLR.mjs → logs-CQ2T46dr.mjs} +2 -2
- package/dist/project-config.mjs +1 -1
- package/dist/scaffold/CLAUDE.md +442 -80
- package/dist/scaffold/package.json +6 -3
- package/dist/scaffold/server.mjs +11 -4
- package/dist/scaffold/tsconfig.json +0 -1
- package/dist/{secret-BuMuFR4B.mjs → secret-Ct34kdgU.mjs} +2 -2
- package/dist/{storage-DzRZ-eCw.mjs → storage-i_79ZwAR.mjs} +2 -2
- package/dist/{studio-BCNUpDgP.mjs → studio-CzTzPqe1.mjs} +4 -4
- package/dist/templates/briefing-desk/agent.test.ts +350 -0
- package/dist/templates/briefing-desk/agent.ts +30 -0
- package/dist/templates/briefing-desk/shared.ts +200 -0
- package/dist/templates/briefing-desk/system-prompt.md +39 -0
- package/dist/templates/briefing-desk/tools/briefing_so_far.ts +35 -0
- package/dist/templates/briefing-desk/tools/research_topic.ts +88 -0
- package/dist/templates/briefing-desk/tools/verify_claim.ts +77 -0
- package/dist/templates/call-audit/agent.eval.test.ts +474 -0
- package/dist/templates/call-audit/agent.test.ts +57 -204
- package/dist/templates/call-audit/agent.ts +32 -19
- package/dist/templates/call-audit/client.tsx +17 -54
- package/dist/templates/call-audit/workflows/audit.ts +11 -18
- package/dist/templates/call-audit/workflows/ingest.ts +106 -114
- package/dist/templates/call-audit/workflows/media.ts +2 -12
- package/dist/templates/call-audit/workflows/summarize.ts +47 -52
- package/dist/templates/call-audit/workflows/sync-api.ts +7 -7
- package/dist/templates/code-interpreter/agent.eval.test.ts +165 -0
- package/dist/templates/dispatch-center/agent.eval.test.ts +349 -0
- package/dist/templates/dispatch-center/agent.test.ts +149 -25
- package/dist/templates/dispatch-center/client.tsx +239 -129
- package/dist/templates/dispatch-center/shared.ts +99 -1
- package/dist/templates/dispatch-center/system-prompt.md +3 -1
- package/dist/templates/dispatch-center/tools/incident_add_note.ts +16 -13
- package/dist/templates/dispatch-center/tools/incident_create.ts +19 -1
- package/dist/templates/dispatch-center/tools/incident_escalate.ts +68 -59
- package/dist/templates/dispatch-center/tools/incident_triage.ts +57 -43
- package/dist/templates/dispatch-center/tools/incident_update_status.ts +49 -40
- package/dist/templates/dispatch-center/tools/ops_run_scenario.ts +8 -2
- package/dist/templates/dispatch-center/tools/resources_dispatch.ts +69 -52
- package/dist/templates/dispatch-center/tools/resources_update_status.ts +45 -41
- package/dist/templates/embedded-assets/agent.eval.test.ts +138 -0
- package/dist/templates/embedded-assets/agent.test.ts +17 -14
- package/dist/templates/health-assistant/agent.eval.test.ts +167 -0
- package/dist/templates/health-assistant/agent.test.ts +22 -4
- package/dist/templates/infocom-adventure/agent.eval.test.ts +158 -0
- package/dist/templates/infocom-adventure/agent.test.ts +53 -24
- package/dist/templates/infocom-adventure/agent.ts +21 -0
- package/dist/templates/infocom-adventure/client.tsx +246 -164
- package/dist/templates/infocom-adventure/shared.ts +20 -9
- package/dist/templates/infocom-adventure/system-prompt.md +1 -1
- package/dist/templates/infocom-adventure/tools/game_state_move.ts +3 -2
- package/dist/templates/link-digest/agent.eval.test.ts +195 -0
- package/dist/templates/link-digest/agent.test.ts +24 -19
- package/dist/templates/link-digest/client.tsx +47 -61
- package/dist/templates/link-digest/workflows/digest.ts +19 -31
- package/dist/templates/math-buddy/agent.eval.test.ts +146 -0
- package/dist/templates/night-owl/agent.eval.test.ts +272 -0
- package/dist/templates/night-owl/agent.test.ts +70 -19
- package/dist/templates/night-owl/agent.ts +5 -0
- package/dist/templates/night-owl/client.tsx +56 -56
- package/dist/templates/night-owl/shared.ts +24 -0
- package/dist/templates/night-owl/tools/recommend.ts +18 -6
- package/dist/templates/personal-finance/agent.eval.test.ts +168 -0
- package/dist/templates/personal-finance/system-prompt.md +4 -0
- package/dist/templates/pipeline-simple/agent.eval.test.ts +78 -0
- package/dist/templates/pipeline-simple/agent.ts +2 -2
- package/dist/templates/pizza-ordering/agent.eval.test.ts +171 -0
- package/dist/templates/pizza-ordering/agent.test.ts +42 -27
- package/dist/templates/pizza-ordering/client.tsx +9 -26
- package/dist/templates/pizza-ordering/shared.ts +20 -1
- package/dist/templates/pizza-ordering/system-prompt.md +6 -0
- package/dist/templates/plan-and-execute/agent.eval.test.ts +209 -0
- package/dist/templates/plan-and-execute/agent.test.ts +99 -58
- package/dist/templates/plan-and-execute/agent.ts +1 -1
- package/dist/templates/plan-and-execute/client.tsx +12 -15
- package/dist/templates/plan-and-execute/shared.ts +71 -2
- package/dist/templates/plan-and-execute/tools/plan_status.ts +18 -6
- package/dist/templates/plan-and-execute/tools/revise_plan.ts +18 -10
- package/dist/templates/plan-and-execute/tools/start_plan.ts +13 -2
- package/dist/templates/plan-and-execute/tools/work_next_step.ts +70 -22
- package/dist/templates/podcast-digest/agent.eval.test.ts +411 -0
- package/dist/templates/podcast-digest/agent.test.ts +717 -0
- package/dist/templates/podcast-digest/agent.ts +139 -0
- package/dist/templates/podcast-digest/client.tsx +154 -0
- package/dist/templates/podcast-digest/workflows/digest.ts +444 -0
- package/dist/templates/podcast-digest/workflows/feeds.ts +507 -0
- package/dist/templates/podcast-digest/workflows/slack.ts +88 -0
- package/dist/templates/recap-workflow/agent.eval.test.ts +366 -0
- package/dist/templates/recap-workflow/agent.test.ts +97 -94
- package/dist/templates/recap-workflow/tools/recap_progress.ts +9 -12
- package/dist/templates/recap-workflow/tools/recap_status.ts +8 -9
- package/dist/templates/recap-workflow/tools/request_recap.ts +2 -1
- package/dist/templates/recap-workflow/workflows/recap.ts +36 -40
- package/dist/templates/redline/agent.eval.test.ts +209 -0
- package/dist/templates/redline/agent.test.ts +17 -15
- package/dist/templates/redline/client.tsx +12 -12
- package/dist/templates/redline/workflows/redline.ts +19 -31
- package/dist/templates/research-workflow/agent.eval.test.ts +329 -0
- package/dist/templates/research-workflow/agent.test.ts +60 -59
- package/dist/templates/research-workflow/tools/research_progress.ts +9 -12
- package/dist/templates/research-workflow/tools/research_status.ts +9 -11
- package/dist/templates/research-workflow/workflows/research.ts +44 -61
- package/dist/templates/retail/address.ts +87 -1
- package/dist/templates/retail/agent.eval.test.ts +314 -0
- package/dist/templates/retail/agent.test.ts +195 -112
- package/dist/templates/retail/agent.ts +35 -1
- package/dist/templates/retail/cancel.ts +116 -0
- package/dist/templates/retail/client.tsx +265 -117
- package/dist/templates/retail/payment.ts +107 -0
- package/dist/templates/retail/pending.test.ts +196 -0
- package/dist/templates/retail/pending.ts +137 -0
- package/dist/templates/retail/registry.test.ts +150 -7
- package/dist/templates/retail/returns.ts +102 -0
- package/dist/templates/retail/shared.test.ts +42 -0
- package/dist/templates/retail/shared.ts +35 -0
- package/dist/templates/retail/store.test.ts +84 -15
- package/dist/templates/retail/store.ts +231 -48
- package/dist/templates/retail/swap.test.ts +4 -4
- package/dist/templates/retail/swap.ts +248 -15
- package/dist/templates/retail/system-prompt.md +69 -28
- package/dist/templates/retail/tools/cancel_change.ts +33 -0
- package/dist/templates/retail/tools/cancel_pending_order.ts +16 -60
- package/dist/templates/retail/tools/confirm_change.ts +42 -0
- package/dist/templates/retail/tools/exchange_delivered_order_items.ts +22 -67
- package/dist/templates/retail/tools/find_user_id_by_email.ts +8 -5
- package/dist/templates/retail/tools/find_user_id_by_name_zip.ts +4 -5
- package/dist/templates/retail/tools/get_item_details.ts +3 -4
- package/dist/templates/retail/tools/get_order_details.ts +3 -4
- package/dist/templates/retail/tools/get_product_details.ts +3 -4
- package/dist/templates/retail/tools/get_user_details.ts +2 -2
- package/dist/templates/retail/tools/list_all_product_types.ts +5 -5
- package/dist/templates/retail/tools/modify_pending_order_address.ts +13 -25
- package/dist/templates/retail/tools/modify_pending_order_items.ts +23 -62
- package/dist/templates/retail/tools/modify_pending_order_payment.ts +16 -78
- package/dist/templates/retail/tools/modify_user_address.ts +13 -16
- package/dist/templates/retail/tools/return_delivered_order_items.ts +17 -72
- package/dist/templates/retail/tools/transfer_to_human_agents.ts +12 -6
- package/dist/templates/simple/agent.eval.test.ts +54 -0
- package/dist/templates/solo-rpg/agent.eval.test.ts +320 -0
- package/dist/templates/solo-rpg/agent.test.ts +227 -48
- package/dist/templates/solo-rpg/client.tsx +38 -37
- package/dist/templates/solo-rpg/shared.ts +145 -19
- package/dist/templates/solo-rpg/system-prompt.md +3 -2
- package/dist/templates/solo-rpg/tools/action_roll.ts +82 -63
- package/dist/templates/solo-rpg/tools/burn_momentum.ts +85 -50
- package/dist/templates/solo-rpg/tools/check_state.ts +24 -7
- package/dist/templates/solo-rpg/tools/load_game.ts +13 -1
- package/dist/templates/solo-rpg/tools/save_game.ts +16 -7
- package/dist/templates/solo-rpg/tools/setup_character.ts +22 -2
- package/dist/templates/solo-rpg/tools/update_state.ts +113 -100
- package/dist/templates/spoken-summary/agent.eval.test.ts +342 -0
- package/dist/templates/spoken-summary/agent.test.ts +68 -97
- package/dist/templates/spoken-summary/agent.ts +30 -17
- package/dist/templates/spoken-summary/client.tsx +10 -50
- package/dist/templates/spoken-summary/workflows/summarize.ts +17 -22
- package/dist/templates/spoken-summary/workflows/transcribe.ts +19 -26
- package/dist/templates/support-line/agent.eval.test.ts +273 -0
- package/dist/templates/support-line/agent.test.ts +11 -16
- package/dist/templates/support-line/agent.ts +1 -1
- package/dist/templates/support-line/client.tsx +9 -9
- package/dist/templates/support-line/nodes.ts +100 -0
- package/dist/templates/support-line/procedure.ts +407 -0
- package/dist/templates/support-line/system-prompt.md +2 -1
- package/dist/templates/support-line/tools/answer_question.ts +66 -4
- package/dist/templates/transcription-workflow/agent.eval.test.ts +234 -0
- package/dist/templates/transcription-workflow/agent.test.ts +94 -194
- package/dist/templates/transcription-workflow/agent.ts +1 -1
- package/dist/templates/transcription-workflow/client.tsx +17 -42
- package/dist/templates/transcription-workflow/workflows/batch.ts +19 -22
- package/dist/templates/transcription-workflow/workflows/normalize.ts +90 -186
- package/dist/templates/transcription-workflow/workflows/stitch.ts +0 -11
- package/dist/templates/transcription-workflow/workflows/stream.ts +4 -5
- package/dist/templates/transcription-workflow/workflows/sync-api.ts +6 -6
- package/dist/templates/transcription-workflow/workflows/transcribe.ts +32 -26
- package/dist/templates/transcription-workflow/workflows/wav.ts +0 -31
- package/dist/templates/travel-concierge/agent.eval.test.ts +323 -0
- package/dist/templates/travel-concierge/agent.test.ts +174 -43
- package/dist/templates/travel-concierge/agent.ts +18 -9
- package/dist/templates/travel-concierge/client.tsx +11 -23
- package/dist/templates/travel-concierge/routing.ts +34 -15
- package/dist/templates/travel-concierge/shared.ts +180 -12
- package/dist/templates/travel-concierge/system-prompt.md +7 -1
- package/dist/templates/travel-concierge/tools/book_car_rental.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_excursion.ts +8 -5
- package/dist/templates/travel-concierge/tools/book_hotel.ts +8 -5
- package/dist/templates/travel-concierge/tools/cancel_ticket.ts +8 -5
- package/dist/templates/travel-concierge/tools/search_car_rentals.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_excursions.ts +9 -5
- package/dist/templates/travel-concierge/tools/search_flights.ts +9 -6
- package/dist/templates/travel-concierge/tools/search_hotels.ts +9 -5
- package/dist/templates/travel-concierge/tools/update_ticket.ts +8 -5
- package/dist/templates/web-researcher/agent.eval.test.ts +121 -0
- package/dist/templates/web-researcher/system-prompt.md +15 -4
- package/dist/{test-9kPKJI-w.mjs → test-BvyT6sAc.mjs} +24 -10
- package/dist/test.d.ts +29 -5
- package/dist/{worker-bundler-CGD4r8Kc.mjs → worker-bundler-CCVEDjm1.mjs} +241 -3
- package/dist/worker-bundler.mjs +1 -1
- package/dist/{workflow-CFpxOFfQ.mjs → workflow-BflATMmU.mjs} +47 -12
- package/dist/workflow-bundler.d.ts +6 -1
- package/dist/workflow.d.ts +1 -1
- package/package.json +5 -4
- package/dist/eject-31gjtaHF.mjs +0 -116
- package/dist/eject.d.ts +0 -60
- package/dist/templates/call-audit/workflows/temp-media.ts +0 -138
- package/dist/templates/infocom-adventure/tools/game_state_history.ts +0 -14
- package/dist/templates/support-line/graph.ts +0 -224
- package/dist/{_config-D_s09e7g.mjs → _config-CmJOFsAP.mjs} +1 -1
- /package/dist/templates/plan-and-execute/{graph.ts → procedure.ts} +0 -0
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
DialogPosition,
|
|
3
|
+
InferToolOutput,
|
|
4
|
+
ToolContext,
|
|
5
|
+
ToolDef,
|
|
6
|
+
ToolInputSchema,
|
|
7
|
+
} from "@alexkroman1/aai";
|
|
8
|
+
import { isToolFailure } from "@alexkroman1/aai";
|
|
9
|
+
import { createToolContext, ok } from "@alexkroman1/aai/testing";
|
|
3
10
|
import { describe, expect, test } from "vitest";
|
|
4
|
-
import { dispatchSlot } from "./shared.ts";
|
|
11
|
+
import { callFlow, dispatchSlot } from "./shared.ts";
|
|
12
|
+
import incidentAddNote from "./tools/incident_add_note.ts";
|
|
5
13
|
import incidentCreate from "./tools/incident_create.ts";
|
|
6
14
|
import incidentEscalate from "./tools/incident_escalate.ts";
|
|
7
15
|
import incidentTriage from "./tools/incident_triage.ts";
|
|
8
16
|
import incidentUpdateStatus from "./tools/incident_update_status.ts";
|
|
17
|
+
import opsRunScenario from "./tools/ops_run_scenario.ts";
|
|
9
18
|
import resourcesDispatch from "./tools/resources_dispatch.ts";
|
|
10
19
|
import resourcesUpdateStatus from "./tools/resources_update_status.ts";
|
|
11
20
|
|
|
@@ -14,14 +23,31 @@ import resourcesUpdateStatus from "./tools/resources_update_status.ts";
|
|
|
14
23
|
* construction. */
|
|
15
24
|
const makeCtx = (): ToolContext => createToolContext();
|
|
16
25
|
|
|
26
|
+
/**
|
|
27
|
+
* What a gated tool's own `execute` returned, read off the tool itself.
|
|
28
|
+
*
|
|
29
|
+
* `callFlow.tool` threads its result type out now, so `InferToolOutput` answers
|
|
30
|
+
* `DialogToolResult<R> | ToolFailure` — this is that minus the envelope and the
|
|
31
|
+
* refusal arm. It replaces the inline `{ dispatched: { callsign: string }[] }`
|
|
32
|
+
* shapes the assertions below used to restate, which were a second copy of each
|
|
33
|
+
* tool's return type that could not go stale loudly.
|
|
34
|
+
*
|
|
35
|
+
* The unwrap itself is `ok` from `@alexkroman1/aai/testing`; the hand-rolled
|
|
36
|
+
* copy that used to sit here was byte-identical to three other templates'.
|
|
37
|
+
*/
|
|
38
|
+
type Result<T extends ToolDef<ToolInputSchema>> = Extract<
|
|
39
|
+
InferToolOutput<T>,
|
|
40
|
+
{ result: unknown }
|
|
41
|
+
>["result"];
|
|
42
|
+
|
|
43
|
+
/** Where the call is, without going through a tool. */
|
|
44
|
+
const at = (ctx: ToolContext): DialogPosition => callFlow.position(ctx);
|
|
45
|
+
|
|
17
46
|
async function createIncidentFor(
|
|
18
47
|
ctx: ToolContext,
|
|
19
48
|
description = "structure fire with heavy smoke",
|
|
20
49
|
): Promise<string> {
|
|
21
|
-
const result =
|
|
22
|
-
{ location: "400 Oak Street", description },
|
|
23
|
-
ctx,
|
|
24
|
-
)) as { incidentId: string };
|
|
50
|
+
const result = await incidentCreate.execute({ location: "400 Oak Street", description }, ctx);
|
|
25
51
|
return result.incidentId;
|
|
26
52
|
}
|
|
27
53
|
|
|
@@ -52,10 +78,9 @@ describe("dispatch-center template", () => {
|
|
|
52
78
|
const ctx = makeCtx();
|
|
53
79
|
const incidentId = await createIncidentFor(ctx, "cardiac arrest, patient not breathing");
|
|
54
80
|
|
|
55
|
-
const result =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
81
|
+
const result = ok<Result<typeof resourcesDispatch>>(
|
|
82
|
+
await resourcesDispatch.execute({ incidentId, callsigns: ["auto"] }, ctx),
|
|
83
|
+
);
|
|
59
84
|
|
|
60
85
|
expect(result.failed).toBeUndefined();
|
|
61
86
|
expect(result.dispatched.length).toBeGreaterThan(0);
|
|
@@ -68,10 +93,10 @@ describe("dispatch-center template", () => {
|
|
|
68
93
|
// mutex in updateState makes each one run against the previous one's
|
|
69
94
|
// finished state, so neither incident's changes are half-applied when
|
|
70
95
|
// the other's mutator runs.
|
|
71
|
-
const [a, b] =
|
|
96
|
+
const [a, b] = await Promise.all([
|
|
72
97
|
incidentCreate.execute({ location: "1 First St", description: "gas leak" }, ctx),
|
|
73
98
|
incidentCreate.execute({ location: "2 Second St", description: "vehicle crash" }, ctx),
|
|
74
|
-
])
|
|
99
|
+
]);
|
|
75
100
|
|
|
76
101
|
const state = dispatchSlot.get(ctx);
|
|
77
102
|
expect(a?.incidentId).not.toBe(b?.incidentId);
|
|
@@ -124,21 +149,120 @@ describe("dispatch-center template", () => {
|
|
|
124
149
|
const incidentId = await createIncidentFor(ctx);
|
|
125
150
|
await incidentUpdateStatus.execute({ incidentId, status: "resolved" }, ctx);
|
|
126
151
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
152
|
+
// Each is a refusal the BODY answered, so it arrives unwrapped as a
|
|
153
|
+
// `ToolFailure` rather than under the position envelope — which is what the
|
|
154
|
+
// narrowing says, where the old cast to `{ error?: string }` said nothing
|
|
155
|
+
// and would have read `undefined` off a success just as quietly.
|
|
156
|
+
const escalated = await incidentEscalate.execute({ incidentId, reason: "flare-up" }, ctx);
|
|
157
|
+
expect(isToolFailure(escalated) && escalated.error).toMatch(/resolved/);
|
|
131
158
|
|
|
132
|
-
const reResolved =
|
|
133
|
-
|
|
134
|
-
ctx,
|
|
135
|
-
)) as { error?: string };
|
|
136
|
-
expect(reResolved.error).toMatch(/resolved/);
|
|
159
|
+
const reResolved = await incidentUpdateStatus.execute({ incidentId, status: "resolved" }, ctx);
|
|
160
|
+
expect(isToolFailure(reResolved) && reResolved.error).toMatch(/resolved/);
|
|
137
161
|
|
|
138
|
-
const dispatchedTo =
|
|
162
|
+
const dispatchedTo = await resourcesDispatch.execute(
|
|
139
163
|
{ incidentId, callsigns: ["Medic-1"] },
|
|
140
164
|
ctx,
|
|
141
|
-
)
|
|
142
|
-
expect(dispatchedTo.error).toMatch(/resolved/);
|
|
165
|
+
);
|
|
166
|
+
expect(isToolFailure(dispatchedTo) && dispatchedTo.error).toMatch(/resolved/);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("the call flow", () => {
|
|
171
|
+
test("a fresh shift is in standby, and every incident tool refuses there", async () => {
|
|
172
|
+
const ctx = makeCtx();
|
|
173
|
+
expect(at(ctx).state).toBe("standby");
|
|
174
|
+
|
|
175
|
+
// Each of these used to run and answer `Incident INC-0001 not found` — a
|
|
176
|
+
// data answer to a positional question. The refusal now names where the
|
|
177
|
+
// shift is and quotes what to do about it.
|
|
178
|
+
for (const call of [
|
|
179
|
+
incidentTriage.execute({ incidentId: "INC-0001" }, ctx),
|
|
180
|
+
resourcesDispatch.execute({ incidentId: "INC-0001", callsigns: ["Medic-1"] }, ctx),
|
|
181
|
+
incidentUpdateStatus.execute({ incidentId: "INC-0001", status: "on_scene" }, ctx),
|
|
182
|
+
incidentEscalate.execute({ incidentId: "INC-0001", reason: "spreading" }, ctx),
|
|
183
|
+
incidentAddNote.execute({ incidentId: "INC-0001", note: "caller hung up" }, ctx),
|
|
184
|
+
resourcesUpdateStatus.execute({ callsign: "Medic-1", status: "en_route" }, ctx),
|
|
185
|
+
]) {
|
|
186
|
+
const refusal = await call;
|
|
187
|
+
expect(isToolFailure(refusal)).toBe(true);
|
|
188
|
+
expect(isToolFailure(refusal) && refusal.error).toMatch(/standby/);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// And nothing ran: a refusal must not have touched the board.
|
|
192
|
+
expect(dispatchSlot.get(ctx).incidentCounter).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("logging, triaging and dispatching walk the call through its three steps", async () => {
|
|
196
|
+
const ctx = makeCtx();
|
|
197
|
+
|
|
198
|
+
const created = await incidentCreate.execute(
|
|
199
|
+
{ location: "400 Oak Street", description: "structure fire with heavy smoke" },
|
|
200
|
+
ctx,
|
|
201
|
+
);
|
|
202
|
+
// `state`/`instruction`, not `at`/`next`: this ungated tool spreads the
|
|
203
|
+
// `DialogPosition` verbatim now, so it reports its position under the same
|
|
204
|
+
// keys every gated tool's result carries.
|
|
205
|
+
expect(created.state).toBe("working.triaging");
|
|
206
|
+
expect(created.instruction).toMatch(/incident_triage/);
|
|
207
|
+
|
|
208
|
+
const triaged = ok<Result<typeof incidentTriage>>(
|
|
209
|
+
await incidentTriage.execute({ incidentId: created.incidentId, severity: "critical" }, ctx),
|
|
210
|
+
);
|
|
211
|
+
expect(triaged.triageScore).toBeGreaterThan(0);
|
|
212
|
+
expect(at(ctx).state).toBe("working.dispatching");
|
|
213
|
+
|
|
214
|
+
ok(
|
|
215
|
+
await resourcesDispatch.execute({ incidentId: created.incidentId, autoDispatch: true }, ctx),
|
|
216
|
+
);
|
|
217
|
+
expect(at(ctx).state).toBe("working.monitoring");
|
|
218
|
+
expect(at(ctx).instruction).toMatch(/radio in/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("a dispatch that rolled nothing leaves the call where it was", async () => {
|
|
222
|
+
const ctx = makeCtx();
|
|
223
|
+
const incidentId = await createIncidentFor(ctx);
|
|
224
|
+
await incidentTriage.execute({ incidentId }, ctx);
|
|
225
|
+
expect(at(ctx).state).toBe("working.dispatching");
|
|
226
|
+
|
|
227
|
+
// Every requested callsign is unknown, so no unit moved — and the call has
|
|
228
|
+
// not moved on either.
|
|
229
|
+
const result = ok<Result<typeof resourcesDispatch>>(
|
|
230
|
+
await resourcesDispatch.execute({ incidentId, callsigns: ["Ghost-1"] }, ctx),
|
|
231
|
+
);
|
|
232
|
+
expect(result.dispatched).toHaveLength(0);
|
|
233
|
+
expect(at(ctx).state).toBe("working.dispatching");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("a new call is legal mid-incident and puts the flow back on triage", async () => {
|
|
237
|
+
const ctx = makeCtx();
|
|
238
|
+
const first = await createIncidentFor(ctx);
|
|
239
|
+
await incidentTriage.execute({ incidentId: first }, ctx);
|
|
240
|
+
await resourcesDispatch.execute({ incidentId: first, autoDispatch: true }, ctx);
|
|
241
|
+
expect(at(ctx).state).toBe("working.monitoring");
|
|
242
|
+
|
|
243
|
+
await createIncidentFor(ctx, "chemical spill spreading toward a storm drain");
|
|
244
|
+
expect(at(ctx).state).toBe("working.triaging");
|
|
245
|
+
|
|
246
|
+
// The first incident is still workable — the position tracks the call in
|
|
247
|
+
// hand, and the tools are addressed by id.
|
|
248
|
+
ok(await incidentAddNote.execute({ incidentId: first, note: "crews on scene" }, ctx));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("a failed tool does not advance the flow", async () => {
|
|
252
|
+
const ctx = makeCtx();
|
|
253
|
+
const incidentId = await createIncidentFor(ctx);
|
|
254
|
+
await incidentUpdateStatus.execute({ incidentId, status: "resolved" }, ctx);
|
|
255
|
+
const before = at(ctx).state;
|
|
256
|
+
|
|
257
|
+
const refused = await incidentTriage.execute({ incidentId, severity: "critical" }, ctx);
|
|
258
|
+
expect(isToolFailure(refused)).toBe(true);
|
|
259
|
+
expect(at(ctx).state).toBe(before);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("a training scenario logs incidents like a real call does", async () => {
|
|
263
|
+
const ctx = makeCtx();
|
|
264
|
+
const result = await opsRunScenario.execute({ scenario: "mass_casualty" }, ctx);
|
|
265
|
+
expect(result.incidentsCreated.length).toBeGreaterThan(1);
|
|
266
|
+
expect(result.state).toBe("working.triaging");
|
|
143
267
|
});
|
|
144
268
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import "@alexkroman1/aai-ui/styles.css";
|
|
2
|
-
import type { AgentState,
|
|
2
|
+
import type { AgentState, ConversationItem } from "@alexkroman1/aai-ui";
|
|
3
3
|
import {
|
|
4
4
|
AutoScroll,
|
|
5
5
|
client,
|
|
6
6
|
useAgentState,
|
|
7
|
+
useConversation,
|
|
7
8
|
useSession,
|
|
8
|
-
|
|
9
|
+
useSessionSelector,
|
|
9
10
|
} from "@alexkroman1/aai-ui";
|
|
10
11
|
import type { DispatchState, IncidentSummary, Severity, Status } from "./shared.ts";
|
|
11
12
|
import { dashboardProjection } from "./shared.ts";
|
|
@@ -139,15 +140,242 @@ function IncidentCard({ inc }: { inc: IncidentSummary }) {
|
|
|
139
140
|
);
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
/** One radio line: a message bubble, or the tool call that came after it. */
|
|
144
|
+
function Row({ item }: { item: ConversationItem }) {
|
|
145
|
+
if (item.kind === "tool") {
|
|
146
|
+
const { name, status } = item.toolCall;
|
|
147
|
+
return (
|
|
148
|
+
<div
|
|
149
|
+
className="self-start rounded-md px-2.5 py-1.5 text-[11px] font-mono flex items-center gap-2"
|
|
150
|
+
style={{
|
|
151
|
+
background: "#111827",
|
|
152
|
+
border: "1px solid #1e293b",
|
|
153
|
+
color: status === "pending" ? "#eab308" : "#64748b",
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<span
|
|
157
|
+
className="w-1.5 h-1.5 rounded-full inline-block"
|
|
158
|
+
style={{
|
|
159
|
+
background: status === "pending" ? "#eab308" : "#22c55e",
|
|
160
|
+
animation: status === "pending" ? "dc-pulse 1s ease-in-out infinite" : "none",
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
{name}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const { role, content } = item.message;
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5"
|
|
171
|
+
style={{
|
|
172
|
+
lineHeight: 1.6,
|
|
173
|
+
alignSelf: role === "assistant" ? "flex-start" : "flex-end",
|
|
174
|
+
background: role === "assistant" ? "#1e293b" : "#172554",
|
|
175
|
+
animation: "dc-slide-in 0.2s ease-out",
|
|
176
|
+
borderLeft: role === "assistant" ? "3px solid #3b82f6" : "none",
|
|
177
|
+
borderRight: role !== "assistant" ? "3px solid #22d3ee" : "none",
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: "#64748b" }}>
|
|
181
|
+
{role === "assistant" ? "DISPATCH" : "OPERATOR"}
|
|
182
|
+
</div>
|
|
183
|
+
{content}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The radio log.
|
|
190
|
+
*
|
|
191
|
+
* `useConversation()` rather than a `session.messages.map(...)`: the messages
|
|
192
|
+
* were only half of it. This board runs eight tools and rendered NONE of them,
|
|
193
|
+
* because tool calls live in a second array this page never read — and it also
|
|
194
|
+
* dropped the streaming reply and the thinking indicator. The hook owns the
|
|
195
|
+
* interleave (a tool row follows its anchor message), the `null`-vs-`""`
|
|
196
|
+
* transcript distinction and the thinking-suppression rule; what stays here is
|
|
197
|
+
* the markup, which is the part this template exists to show.
|
|
198
|
+
*
|
|
199
|
+
* It also subscribes per FIELD, so the conversation re-renders at the
|
|
200
|
+
* conversation's rate. The whole-page `useSession()` this replaced re-rendered
|
|
201
|
+
* the incident board on every STT partial.
|
|
202
|
+
*/
|
|
203
|
+
function Conversation() {
|
|
204
|
+
const { items, streaming, transcript, thinking } = useConversation();
|
|
205
|
+
return (
|
|
206
|
+
<>
|
|
207
|
+
<AutoScroll
|
|
208
|
+
scrollClassName="dc-messages overflow-y-auto"
|
|
209
|
+
contentClassName="p-4 flex flex-col gap-2"
|
|
210
|
+
>
|
|
211
|
+
{items.length === 0 && !streaming && (
|
|
212
|
+
<div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
|
|
213
|
+
Dispatch Command Center standing by. Click START to begin operations.
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
{items.map((item) => (
|
|
217
|
+
<Row
|
|
218
|
+
key={item.kind === "message" ? `m${item.message.id}` : item.toolCall.callId}
|
|
219
|
+
item={item}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
{streaming !== null && (
|
|
223
|
+
<div
|
|
224
|
+
className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5 self-start"
|
|
225
|
+
style={{ lineHeight: 1.6, background: "#1e293b", borderLeft: "3px solid #3b82f6" }}
|
|
226
|
+
>
|
|
227
|
+
<div className="text-[10px] uppercase tracking-wider mb-1" style={{ color: "#64748b" }}>
|
|
228
|
+
DISPATCH
|
|
229
|
+
</div>
|
|
230
|
+
{streaming}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
{thinking && (
|
|
234
|
+
<div className="self-start text-[11px] px-3.5" style={{ color: "#64748b" }}>
|
|
235
|
+
<span style={{ animation: "dc-pulse 1.2s ease-in-out infinite" }}>· · ·</span>
|
|
236
|
+
</div>
|
|
237
|
+
)}
|
|
238
|
+
</AutoScroll>
|
|
239
|
+
|
|
240
|
+
{transcript.speaking && (
|
|
241
|
+
<div
|
|
242
|
+
className="flex items-center px-4 py-2 text-xs italic min-h-8"
|
|
243
|
+
style={{ background: "#111827", borderTop: "1px solid #1e293b", color: "#64748b" }}
|
|
244
|
+
>
|
|
245
|
+
<span
|
|
246
|
+
className="w-2.5 h-2.5 rounded-full inline-block mr-2"
|
|
247
|
+
style={{ background: "#22c55e", animation: "dc-pulse 1.5s ease-in-out infinite" }}
|
|
248
|
+
/>
|
|
249
|
+
{transcript.text}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The live status readout, on its own subscription.
|
|
258
|
+
*
|
|
259
|
+
* `useSessionSelector` rather than a field off a whole-page `useSession()`: the
|
|
260
|
+
* header is the only thing here that cares about the session state, and reading
|
|
261
|
+
* it one level up dragged the incident board through every snapshot change.
|
|
262
|
+
*/
|
|
263
|
+
function StatusReadout() {
|
|
264
|
+
const state = useSessionSelector((s) => s.state);
|
|
265
|
+
return (
|
|
266
|
+
<>
|
|
267
|
+
<span
|
|
268
|
+
className="w-2.5 h-2.5 rounded-full inline-block"
|
|
269
|
+
style={{
|
|
270
|
+
background: STATE_COLORS[state],
|
|
271
|
+
animation:
|
|
272
|
+
state === "listening"
|
|
273
|
+
? "dc-pulse 1.5s ease-in-out infinite"
|
|
274
|
+
: state === "thinking"
|
|
275
|
+
? "dc-pulse 0.8s ease-in-out infinite"
|
|
276
|
+
: "none",
|
|
277
|
+
}}
|
|
278
|
+
title={state}
|
|
279
|
+
/>
|
|
280
|
+
<span className="text-[11px] font-normal normal-case" style={{ color: "#64748b" }}>
|
|
281
|
+
{state === "listening"
|
|
282
|
+
? "LISTENING"
|
|
283
|
+
: state === "thinking"
|
|
284
|
+
? "PROCESSING"
|
|
285
|
+
: state === "speaking"
|
|
286
|
+
? "TRANSMITTING"
|
|
287
|
+
: state.toUpperCase()}
|
|
288
|
+
</span>
|
|
289
|
+
</>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The fatal-error banner.
|
|
295
|
+
*
|
|
296
|
+
* `role="alert"` is the part that is easy to leave out and is load-bearing: per
|
|
297
|
+
* the `fatalError` latch in the session core this banner is the only remaining
|
|
298
|
+
* signal — the status eyebrow beside it goes back to reading like a live
|
|
299
|
+
* session — and a screen reader is never told an unannounced one appeared. It
|
|
300
|
+
* is what `ConsoleShell` carries, which this template's full-bleed two-pane
|
|
301
|
+
* board cannot use as its frame.
|
|
302
|
+
*/
|
|
303
|
+
function ErrorBanner() {
|
|
304
|
+
const error = useSessionSelector((s) => s.error);
|
|
305
|
+
if (!error) return null;
|
|
306
|
+
return (
|
|
307
|
+
<div
|
|
308
|
+
role="alert"
|
|
309
|
+
className="px-4 py-2 text-xs"
|
|
310
|
+
style={{ background: "#450a0a", color: "#fca5a5", borderTop: "1px solid #991b1b" }}
|
|
311
|
+
>
|
|
312
|
+
ERROR: {error.message} ({error.code})
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The shift controls. The one place a whole-session read is what is wanted:
|
|
318
|
+
* it needs `started`, `running` and three methods, and it is three buttons. */
|
|
319
|
+
function ShiftControls({ logged }: { logged: number }) {
|
|
143
320
|
const session = useSession();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
321
|
+
return (
|
|
322
|
+
<div
|
|
323
|
+
className="flex items-center gap-2.5 px-4 py-3"
|
|
324
|
+
style={{ background: "#111827", borderTop: "1px solid #1e293b" }}
|
|
325
|
+
>
|
|
326
|
+
{!session.started ? (
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
330
|
+
style={{ background: "#2563eb" }}
|
|
331
|
+
onClick={() => session.start()}
|
|
332
|
+
>
|
|
333
|
+
Start Dispatch
|
|
334
|
+
</button>
|
|
335
|
+
) : (
|
|
336
|
+
<>
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
|
|
340
|
+
style={{
|
|
341
|
+
background: session.running ? "#334155" : "#2563eb",
|
|
342
|
+
color: session.running ? "#e2e8f0" : "white",
|
|
343
|
+
}}
|
|
344
|
+
onClick={() => session.toggle()}
|
|
345
|
+
>
|
|
346
|
+
{session.running ? "Pause" : "Resume"}
|
|
347
|
+
</button>
|
|
348
|
+
{/* end() hangs up and flips `started` back, so the UI
|
|
349
|
+
returns to "Start Dispatch" and the next start is a
|
|
350
|
+
brand-new shift (fresh incident board, greeting
|
|
351
|
+
included). reset() would keep the call live — the
|
|
352
|
+
buttons never toggle back. */}
|
|
353
|
+
<button
|
|
354
|
+
type="button"
|
|
355
|
+
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
356
|
+
style={{ background: "#dc2626" }}
|
|
357
|
+
onClick={() => session.end()}
|
|
358
|
+
>
|
|
359
|
+
End Shift
|
|
360
|
+
</button>
|
|
361
|
+
</>
|
|
362
|
+
)}
|
|
363
|
+
<div className="flex-1" />
|
|
364
|
+
<span className="text-[10px]" style={{ color: "#475569" }}>
|
|
365
|
+
{logged} incident{logged !== 1 ? "s" : ""} logged
|
|
366
|
+
</span>
|
|
367
|
+
</div>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function App() {
|
|
148
372
|
// The agent's own board, projected by `syncState`. This replaced a
|
|
149
373
|
// useState mirror that merged incident deltas out of tool events — the
|
|
150
374
|
// projection is already the complete list, so there is nothing to merge.
|
|
375
|
+
//
|
|
376
|
+
// It is the ONLY subscription at this level: the session reads that used to
|
|
377
|
+
// sit beside it moved into the four components above, so a partial transcript
|
|
378
|
+
// no longer re-renders the incident cards.
|
|
151
379
|
const dash = useAgentState(dashboardProjection);
|
|
152
380
|
|
|
153
381
|
const incidentList = [...dash.incidents].reverse();
|
|
@@ -179,28 +407,7 @@ function App() {
|
|
|
179
407
|
>
|
|
180
408
|
<span style={{ color: "#3b82f6" }}>◆</span>
|
|
181
409
|
Dispatch Command Center
|
|
182
|
-
<
|
|
183
|
-
className="w-2.5 h-2.5 rounded-full inline-block"
|
|
184
|
-
style={{
|
|
185
|
-
background: STATE_COLORS[session.state],
|
|
186
|
-
animation:
|
|
187
|
-
session.state === "listening"
|
|
188
|
-
? "dc-pulse 1.5s ease-in-out infinite"
|
|
189
|
-
: session.state === "thinking"
|
|
190
|
-
? "dc-pulse 0.8s ease-in-out infinite"
|
|
191
|
-
: "none",
|
|
192
|
-
}}
|
|
193
|
-
title={session.state}
|
|
194
|
-
/>
|
|
195
|
-
<span className="text-[11px] font-normal normal-case" style={{ color: "#64748b" }}>
|
|
196
|
-
{session.state === "listening"
|
|
197
|
-
? "LISTENING"
|
|
198
|
-
: session.state === "thinking"
|
|
199
|
-
? "PROCESSING"
|
|
200
|
-
: session.state === "speaking"
|
|
201
|
-
? "TRANSMITTING"
|
|
202
|
-
: session.state.toUpperCase()}
|
|
203
|
-
</span>
|
|
410
|
+
<StatusReadout />
|
|
204
411
|
</div>
|
|
205
412
|
<div className="flex gap-2 items-center">
|
|
206
413
|
<span className="text-[10px] tracking-wider" style={{ color: "#64748b" }}>
|
|
@@ -229,106 +436,9 @@ function App() {
|
|
|
229
436
|
className="flex flex-col overflow-hidden"
|
|
230
437
|
style={{ borderRight: "1px solid #1e293b" }}
|
|
231
438
|
>
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
>
|
|
236
|
-
{session.messages.length === 0 && (
|
|
237
|
-
<div className="text-center p-10 text-[13px]" style={{ color: "#475569" }}>
|
|
238
|
-
Dispatch Command Center standing by. Click START to begin operations.
|
|
239
|
-
</div>
|
|
240
|
-
)}
|
|
241
|
-
{session.messages.map((m: ChatMessage, i: number) => (
|
|
242
|
-
<div
|
|
243
|
-
key={i}
|
|
244
|
-
className="rounded-lg text-[13px] max-w-[85%] px-3.5 py-2.5"
|
|
245
|
-
style={{
|
|
246
|
-
lineHeight: 1.6,
|
|
247
|
-
alignSelf: m.role === "assistant" ? "flex-start" : "flex-end",
|
|
248
|
-
background: m.role === "assistant" ? "#1e293b" : "#172554",
|
|
249
|
-
animation: "dc-slide-in 0.2s ease-out",
|
|
250
|
-
borderLeft: m.role === "assistant" ? "3px solid #3b82f6" : "none",
|
|
251
|
-
borderRight: m.role !== "assistant" ? "3px solid #22d3ee" : "none",
|
|
252
|
-
}}
|
|
253
|
-
>
|
|
254
|
-
<div
|
|
255
|
-
className="text-[10px] uppercase tracking-wider mb-1"
|
|
256
|
-
style={{ color: "#64748b" }}
|
|
257
|
-
>
|
|
258
|
-
{m.role === "assistant" ? "DISPATCH" : "OPERATOR"}
|
|
259
|
-
</div>
|
|
260
|
-
{m.content}
|
|
261
|
-
</div>
|
|
262
|
-
))}
|
|
263
|
-
</AutoScroll>
|
|
264
|
-
|
|
265
|
-
{speaking && (
|
|
266
|
-
<div
|
|
267
|
-
className="flex items-center px-4 py-2 text-xs italic min-h-8"
|
|
268
|
-
style={{ background: "#111827", borderTop: "1px solid #1e293b", color: "#64748b" }}
|
|
269
|
-
>
|
|
270
|
-
<span
|
|
271
|
-
className="w-2.5 h-2.5 rounded-full inline-block mr-2"
|
|
272
|
-
style={{ background: "#22c55e", animation: "dc-pulse 1.5s ease-in-out infinite" }}
|
|
273
|
-
/>
|
|
274
|
-
{userTranscript}
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
277
|
-
{session.error && (
|
|
278
|
-
<div
|
|
279
|
-
className="px-4 py-2 text-xs"
|
|
280
|
-
style={{ background: "#450a0a", color: "#fca5a5", borderTop: "1px solid #991b1b" }}
|
|
281
|
-
>
|
|
282
|
-
ERROR: {session.error.message} ({session.error.code})
|
|
283
|
-
</div>
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
<div
|
|
287
|
-
className="flex items-center gap-2.5 px-4 py-3"
|
|
288
|
-
style={{ background: "#111827", borderTop: "1px solid #1e293b" }}
|
|
289
|
-
>
|
|
290
|
-
{!session.started ? (
|
|
291
|
-
<button
|
|
292
|
-
type="button"
|
|
293
|
-
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
294
|
-
style={{ background: "#2563eb" }}
|
|
295
|
-
onClick={() => session.start()}
|
|
296
|
-
>
|
|
297
|
-
Start Dispatch
|
|
298
|
-
</button>
|
|
299
|
-
) : (
|
|
300
|
-
<>
|
|
301
|
-
<button
|
|
302
|
-
type="button"
|
|
303
|
-
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer"
|
|
304
|
-
style={{
|
|
305
|
-
background: session.running ? "#334155" : "#2563eb",
|
|
306
|
-
color: session.running ? "#e2e8f0" : "white",
|
|
307
|
-
}}
|
|
308
|
-
onClick={() => session.toggle()}
|
|
309
|
-
>
|
|
310
|
-
{session.running ? "Pause" : "Resume"}
|
|
311
|
-
</button>
|
|
312
|
-
{/* end() hangs up and flips `started` back, so the UI
|
|
313
|
-
returns to "Start Dispatch" and the next start is a
|
|
314
|
-
brand-new shift (fresh incident board, greeting
|
|
315
|
-
included). reset() would keep the call live — the
|
|
316
|
-
buttons never toggle back. */}
|
|
317
|
-
<button
|
|
318
|
-
type="button"
|
|
319
|
-
className="px-4 py-2 border-none rounded-md font-mono text-xs font-semibold uppercase tracking-wider cursor-pointer text-white"
|
|
320
|
-
style={{ background: "#dc2626" }}
|
|
321
|
-
onClick={() => session.end()}
|
|
322
|
-
>
|
|
323
|
-
End Shift
|
|
324
|
-
</button>
|
|
325
|
-
</>
|
|
326
|
-
)}
|
|
327
|
-
<div className="flex-1" />
|
|
328
|
-
<span className="text-[10px]" style={{ color: "#475569" }}>
|
|
329
|
-
{incidentList.length} incident{incidentList.length !== 1 ? "s" : ""} logged
|
|
330
|
-
</span>
|
|
331
|
-
</div>
|
|
439
|
+
<Conversation />
|
|
440
|
+
<ErrorBanner />
|
|
441
|
+
<ShiftControls logged={incidentList.length} />
|
|
332
442
|
</div>
|
|
333
443
|
|
|
334
444
|
{/* Right: sidebar dashboard */}
|