@alexkroman1/aai-ui 15.0.0 → 16.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/dist/agent-state-labels.d.ts +9 -4
- package/dist/{chat-view-C_3T7Ln8.js → chat-view-DsylXJsw.js} +154 -8
- package/dist/components/audio-result.d.ts +94 -0
- package/dist/components/chat-view.js +1 -1
- package/dist/components/conversation-view.d.ts +108 -0
- package/dist/components/message-list.js +1 -1
- package/dist/components/session-controls.d.ts +125 -0
- package/dist/components/session-state-dot.d.ts +111 -0
- package/dist/components/workflow-pending-note.d.ts +81 -0
- package/dist/components/workflow-run-error.d.ts +48 -0
- package/dist/components/workflow-run-panel.d.ts +79 -0
- package/dist/default-client/assets/index-CMh4E-Ci.css +2 -0
- package/dist/default-client/assets/index-eOmPq2sS.js +284 -0
- package/dist/default-client/index.html +2 -2
- package/dist/define-client.js +1 -1
- package/dist/hooks.d.ts +10 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +476 -63
- package/dist/{message-list-CdOnSh5m.js → message-list-BLyPRS1Q.js} +153 -37
- package/dist/use-session-controls.d.ts +69 -0
- package/package.json +2 -2
- package/styles.css +30 -0
- package/dist/default-client/assets/index-BTv30Z4F.css +0 -2
- package/dist/default-client/assets/index-BowRD-xq.js +0 -284
|
@@ -18,10 +18,15 @@
|
|
|
18
18
|
* page overriding one member keeps that: spreading a complete record cannot
|
|
19
19
|
* drop a key.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* that is genuinely shared (the words)
|
|
21
|
+
* `SessionStateDot` (`components/session-state-dot.tsx`) is the dot beside it,
|
|
22
|
+
* and this paragraph used to argue against one: the dot's palette is what each
|
|
23
|
+
* chrome exists to look like — a CRT's glow, a dispatch board's alert colours —
|
|
24
|
+
* and a component would take the one part that is genuinely shared (the words)
|
|
25
|
+
* hostage to the part that is not. It takes the palette as a PROP, so it does
|
|
26
|
+
* not: what it shares is the exhaustive colour lookup, this record as the label
|
|
27
|
+
* fallback, and the pulse rule, and the three chromes that had each written
|
|
28
|
+
* those fourteen lines around their own `satisfies Record<AgentState, string>`
|
|
29
|
+
* keep the record and lose the lines.
|
|
25
30
|
*/
|
|
26
31
|
import type { AgentState } from "./types.ts";
|
|
27
32
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as MessageList } from "./message-list-
|
|
1
|
+
import { t as MessageList } from "./message-list-BLyPRS1Q.js";
|
|
2
2
|
import { useSessionError, useSessionStatus, useTheme } from "./context.js";
|
|
3
3
|
import { a as inkTint, r as THINKING_COLOR, t as ERROR_COLOR } from "./_colors-CpZO-88A.js";
|
|
4
4
|
import { t as AaiLogo } from "./aai-logo-CFlomZlS.js";
|
|
@@ -6,6 +6,50 @@ import { t as Eyebrow } from "./eyebrow-UfmSz9yy.js";
|
|
|
6
6
|
import { Controls } from "./components/controls.js";
|
|
7
7
|
import clsx from "clsx";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
//#region src/agent-state-labels.ts
|
|
10
|
+
/**
|
|
11
|
+
* The default label per {@link AgentState}.
|
|
12
|
+
*
|
|
13
|
+
* Override the ones your page has a better word for and keep the rest:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import type { AgentState } from "@alexkroman1/aai-ui";
|
|
17
|
+
* import { AGENT_STATE_LABELS } from "@alexkroman1/aai-ui";
|
|
18
|
+
*
|
|
19
|
+
* // A dispatch board that shouts, and renames one state.
|
|
20
|
+
* const STATE_LABEL = { ...AGENT_STATE_LABELS, thinking: "Processing" };
|
|
21
|
+
* const shout = (s: AgentState) => STATE_LABEL[s].toUpperCase();
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* **Sentence case, deliberately.** A template that wants caps applies its own
|
|
25
|
+
* `.toUpperCase()`, and a template that wants Title Case is already there;
|
|
26
|
+
* shipping the shouted form instead would leave the two chromes that do not
|
|
27
|
+
* shout with a string they have to un-shout, which no case transform does
|
|
28
|
+
* correctly.
|
|
29
|
+
*
|
|
30
|
+
* Two wordings are decisions rather than transliterations of the member name:
|
|
31
|
+
*
|
|
32
|
+
* - `disconnected` is **"Idle"**. It is the state a session is in BEFORE it has
|
|
33
|
+
* ever started as well as after it ends, so it is the first word most callers
|
|
34
|
+
* see; "Disconnected" reads as a fault on a page where nothing has gone
|
|
35
|
+
* wrong yet. Both chromes that mapped this state by hand chose "Idle" too.
|
|
36
|
+
* - `connecting` and `thinking` carry an ellipsis, `listening` and `speaking`
|
|
37
|
+
* do not. The first two are waits with nothing for the caller to do; the
|
|
38
|
+
* other two describe someone actually talking. Same distinction
|
|
39
|
+
* `WORKFLOW_STATUS_LABELS` draws with its one "Working…".
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
const AGENT_STATE_LABELS = {
|
|
44
|
+
disconnected: "Idle",
|
|
45
|
+
connecting: "Connecting…",
|
|
46
|
+
ready: "Ready",
|
|
47
|
+
listening: "Listening",
|
|
48
|
+
thinking: "Thinking…",
|
|
49
|
+
speaking: "Speaking",
|
|
50
|
+
error: "Error"
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
9
53
|
//#region src/components/session-error-banner.tsx
|
|
10
54
|
/** @jsxImportSource react */
|
|
11
55
|
/**
|
|
@@ -80,6 +124,110 @@ function SessionErrorBanner({ className }) {
|
|
|
80
124
|
});
|
|
81
125
|
}
|
|
82
126
|
//#endregion
|
|
127
|
+
//#region src/components/session-state-dot.tsx
|
|
128
|
+
/** @jsxImportSource react */
|
|
129
|
+
/**
|
|
130
|
+
* One `aai-pulse` cycle per pulsing state, in milliseconds. The two states
|
|
131
|
+
* that pulse are the two in which the agent is DOING something the reader
|
|
132
|
+
* cannot otherwise see — the microphone is open, or a reply is being worked
|
|
133
|
+
* out — and `thinking` beats faster because it is the shorter wait. `speaking`
|
|
134
|
+
* has the audio itself for a signal and stays still.
|
|
135
|
+
*/
|
|
136
|
+
const PULSE_MS = {
|
|
137
|
+
listening: 1500,
|
|
138
|
+
thinking: 800
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* The dot itself: a coloured circle, pulsing or still.
|
|
142
|
+
*
|
|
143
|
+
* Shared by {@link SessionStateDot} and `ConsoleShell`, which computes its
|
|
144
|
+
* colour from the theme rather than from a palette prop and decides for itself
|
|
145
|
+
* when to pulse. Not on the package barrel — the public component is the one
|
|
146
|
+
* that reads the session.
|
|
147
|
+
*
|
|
148
|
+
* The colour is written to `background` AND `color`, so a caller can extend
|
|
149
|
+
* the dot with a `currentColor` utility (`shadow-[0_0_6px_currentColor]` for a
|
|
150
|
+
* glow) without restating the palette.
|
|
151
|
+
*
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
function StateDot({ color, pulseMs, className, title }) {
|
|
155
|
+
return /* @__PURE__ */ jsx("span", {
|
|
156
|
+
className: clsx("rounded-full inline-block shrink-0", className),
|
|
157
|
+
style: {
|
|
158
|
+
background: color,
|
|
159
|
+
color,
|
|
160
|
+
animation: pulseMs === null ? "none" : `aai-pulse ${pulseMs}ms ease-in-out infinite`
|
|
161
|
+
},
|
|
162
|
+
title
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The live session state as a coloured dot and a word, on its own narrow
|
|
167
|
+
* subscription.
|
|
168
|
+
*
|
|
169
|
+
* Three custom chromes had each written this: a `satisfies Record<AgentState,
|
|
170
|
+
* string>` palette (kept — it is the prop), a `STATE_LABELS` spread over
|
|
171
|
+
* {@link AGENT_STATE_LABELS} (kept — it is `labels`), and then the same
|
|
172
|
+
* fourteen lines of markup around them, including the same two-arm ternary
|
|
173
|
+
* deciding which states pulse and how fast. `agent-state-labels.ts` argued
|
|
174
|
+
* against a dot component on the grounds that it would take the shared part
|
|
175
|
+
* (the words) hostage to the part that is not (the palette); this takes the
|
|
176
|
+
* palette as a prop precisely so it does not. What is shared is the structure
|
|
177
|
+
* — the exhaustive colour lookup, the label fallback, the pulse rule, the
|
|
178
|
+
* `useSessionStatus()` subscription that keeps the rest of the header from
|
|
179
|
+
* re-rendering at STT-partial rate.
|
|
180
|
+
*
|
|
181
|
+
* Must be rendered inside the providers `mountClient()` installs.
|
|
182
|
+
*
|
|
183
|
+
* @example A dispatch board's readout: its own colours, three of its own words
|
|
184
|
+
* ```tsx
|
|
185
|
+
* import type { AgentState } from "@alexkroman1/aai-ui";
|
|
186
|
+
* import { SessionStateDot } from "@alexkroman1/aai-ui";
|
|
187
|
+
*
|
|
188
|
+
* const STATE_COLORS = {
|
|
189
|
+
* disconnected: "#6b7280",
|
|
190
|
+
* connecting: "#6b7280",
|
|
191
|
+
* ready: "#22c55e",
|
|
192
|
+
* listening: "#22c55e",
|
|
193
|
+
* thinking: "#eab308",
|
|
194
|
+
* speaking: "#3b82f6",
|
|
195
|
+
* error: "#6b7280",
|
|
196
|
+
* } satisfies Record<AgentState, string>;
|
|
197
|
+
*
|
|
198
|
+
* function StatusReadout() {
|
|
199
|
+
* return (
|
|
200
|
+
* <SessionStateDot
|
|
201
|
+
* colors={STATE_COLORS}
|
|
202
|
+
* labels={{ listening: "LISTENING", thinking: "PROCESSING", speaking: "TRANSMITTING" }}
|
|
203
|
+
* labelClassName="text-[11px] uppercase"
|
|
204
|
+
* />
|
|
205
|
+
* );
|
|
206
|
+
* }
|
|
207
|
+
* ```
|
|
208
|
+
*
|
|
209
|
+
* @param props - See {@link SessionStateDotProps}.
|
|
210
|
+
*
|
|
211
|
+
* @public
|
|
212
|
+
*/
|
|
213
|
+
function SessionStateDot({ colors, labels, pulse = true, className, dotClassName, labelClassName }) {
|
|
214
|
+
const state = useSessionStatus();
|
|
215
|
+
const pulseMs = pulse ? PULSE_MS[state] ?? null : null;
|
|
216
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
217
|
+
className: clsx("inline-flex items-center gap-2", className),
|
|
218
|
+
"data-state": state,
|
|
219
|
+
children: [/* @__PURE__ */ jsx(StateDot, {
|
|
220
|
+
color: colors[state],
|
|
221
|
+
pulseMs,
|
|
222
|
+
className: dotClassName ?? "w-2 h-2",
|
|
223
|
+
title: state
|
|
224
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
225
|
+
className: labelClassName,
|
|
226
|
+
children: labels?.[state] ?? AGENT_STATE_LABELS[state]
|
|
227
|
+
})]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
//#endregion
|
|
83
231
|
//#region src/components/console-shell.tsx
|
|
84
232
|
/** @jsxImportSource react */
|
|
85
233
|
/**
|
|
@@ -183,12 +331,10 @@ function ConsoleShell({ icon, title, state, pulsing, children, footer, className
|
|
|
183
331
|
}), /* @__PURE__ */ jsxs(Eyebrow, {
|
|
184
332
|
className: "shrink-0",
|
|
185
333
|
"data-state": state,
|
|
186
|
-
children: [/* @__PURE__ */ jsx(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
animation: pulsing ? "aai-pulse 1.6s ease-in-out infinite" : "none"
|
|
191
|
-
}
|
|
334
|
+
children: [/* @__PURE__ */ jsx(StateDot, {
|
|
335
|
+
color: stateColor(state, theme.primary, inkTint(theme.text, theme.bg, 65)),
|
|
336
|
+
pulseMs: pulsing ? 1600 : null,
|
|
337
|
+
className: "w-[7px] h-[7px]"
|
|
192
338
|
}), state]
|
|
193
339
|
})]
|
|
194
340
|
}),
|
|
@@ -247,4 +393,4 @@ function ChatView({ icon, title, className }) {
|
|
|
247
393
|
});
|
|
248
394
|
}
|
|
249
395
|
//#endregion
|
|
250
|
-
export { ConsoleShell as n,
|
|
396
|
+
export { AGENT_STATE_LABELS as a, SessionErrorBanner as i, ConsoleShell as n, SessionStateDot as r, ChatView as t };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { UseDownloadUrlResult } from "../use-download-url.ts";
|
|
3
|
+
/**
|
|
4
|
+
* A one-cue caption track for {@link AudioResult}: the words the clip speaks,
|
|
5
|
+
* spanning its whole length.
|
|
6
|
+
*
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export type AudioResultCaptions = {
|
|
10
|
+
/** The spoken text — the one cue. */
|
|
11
|
+
text: string;
|
|
12
|
+
/** The clip's length, which is where the cue ends. */
|
|
13
|
+
durationMs: number;
|
|
14
|
+
/** The track's `label`. Defaults to the player's `label`. */
|
|
15
|
+
label?: string | undefined;
|
|
16
|
+
/** The track's `srcLang`. Default `"en"`. */
|
|
17
|
+
srcLang?: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Props of {@link AudioResult}.
|
|
21
|
+
*
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export type AudioResultProps = {
|
|
25
|
+
/** What {@link useDownloadUrl} returned for the run's audio upload. */
|
|
26
|
+
download: UseDownloadUrlResult;
|
|
27
|
+
/** The name the download link saves as — `"summary.wav"`, `"audit.mp3"`. */
|
|
28
|
+
filename: string;
|
|
29
|
+
/** The player's `aria-label`: what this audio IS — `"Summary read aloud"`. */
|
|
30
|
+
label: string;
|
|
31
|
+
/**
|
|
32
|
+
* The heading over the player — typically the duration and size, which the
|
|
33
|
+
* run's output carries. Omitted, there is no heading.
|
|
34
|
+
*/
|
|
35
|
+
heading?: ReactNode | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The spoken text as a caption track. Omit it deliberately when the same
|
|
38
|
+
* words are rendered in full beside the player (see the component doc);
|
|
39
|
+
* pass it when they are not, or when a real track is what a page needs.
|
|
40
|
+
*/
|
|
41
|
+
captions?: AudioResultCaptions | undefined;
|
|
42
|
+
/** Additional CSS class names for the wrapping `<section>`, appended to its own. */
|
|
43
|
+
className?: string | undefined;
|
|
44
|
+
/** Rendered under the player — the spoken text, usually. */
|
|
45
|
+
children?: ReactNode;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The player for a file a RUN produced: a heading, the fetch's pending line,
|
|
49
|
+
* its announced error, the `<audio>` with an optional caption track, the
|
|
50
|
+
* download link, and whatever the page renders beneath — the spoken text.
|
|
51
|
+
*
|
|
52
|
+
* Two templates exist because of the audio round trip, and both had written
|
|
53
|
+
* this block over {@link useDownloadUrl} identically: the same pending
|
|
54
|
+
* sentence, the same `role="alert"` paragraph, the same `<audio controls>` over
|
|
55
|
+
* the object URL, the same anchor with `download` set. The `download` attribute
|
|
56
|
+
* works on an object URL because the bytes are already in the tab — it was the
|
|
57
|
+
* `href` that could not carry the agent's bearer token, never the attribute —
|
|
58
|
+
* and that is the whole reason both pages hand this a hook result rather than a
|
|
59
|
+
* path.
|
|
60
|
+
*
|
|
61
|
+
* **A caption track is a judgement, not a default.** `spoken-summary` passes
|
|
62
|
+
* `captions`: the summary was written before it was spoken, so one cue
|
|
63
|
+
* spanning the clip is an honest transcript of it. `call-audit` deliberately
|
|
64
|
+
* does not: the spoken text is rendered in full immediately below the player,
|
|
65
|
+
* which is the same information a track would carry. Both are right, which is
|
|
66
|
+
* why the prop is optional in both directions.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* import { AudioResult, createWorkflowApi, useDownloadUrl } from "@alexkroman1/aai-ui";
|
|
71
|
+
*
|
|
72
|
+
* const api = createWorkflowApi();
|
|
73
|
+
*
|
|
74
|
+
* function Player({ id, spoken, ms }: { id: string; spoken: string; ms: number }) {
|
|
75
|
+
* const audio = useDownloadUrl(id, { api });
|
|
76
|
+
* return (
|
|
77
|
+
* <AudioResult
|
|
78
|
+
* download={audio}
|
|
79
|
+
* filename="summary.wav"
|
|
80
|
+
* label="Summary read aloud"
|
|
81
|
+
* heading="Read aloud"
|
|
82
|
+
* captions={{ text: spoken, durationMs: ms }}
|
|
83
|
+
* >
|
|
84
|
+
* <p className="text-sm opacity-70">{spoken}</p>
|
|
85
|
+
* </AudioResult>
|
|
86
|
+
* );
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @param props - See {@link AudioResultProps}.
|
|
91
|
+
*
|
|
92
|
+
* @public
|
|
93
|
+
*/
|
|
94
|
+
export declare function AudioResult({ download, filename, label, heading, captions, className, children, }: AudioResultProps): ReactNode;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
3
|
+
import type { ChatMessage, ToolCallInfo } from "../types.ts";
|
|
4
|
+
import type { UseUserTranscriptResult } from "../use-user-transcript.ts";
|
|
5
|
+
/**
|
|
6
|
+
* Props of {@link ConversationView}.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type ConversationViewProps = {
|
|
11
|
+
/** One finalized message, in this chrome's own markup. */
|
|
12
|
+
renderMessage: (message: ChatMessage) => ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* One tool invocation. Absent, a compact {@link ToolCallRow} naming the tool,
|
|
15
|
+
* shimmering while it is pending.
|
|
16
|
+
*/
|
|
17
|
+
renderTool?: ((toolCall: ToolCallInfo) => ReactNode) | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* The agent's reply as it arrives. Absent, `renderMessage` is called with a
|
|
20
|
+
* synthetic assistant message carrying the text so far (its `id` is `-1`,
|
|
21
|
+
* which no real message has) — every chrome so far rendered the two the same
|
|
22
|
+
* way, and this keeps them from drifting.
|
|
23
|
+
*/
|
|
24
|
+
renderStreaming?: ((text: string) => ReactNode) | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* The caller's in-progress turn. Rendered only while `transcript.speaking`,
|
|
27
|
+
* which is the `null`-vs-`""` distinction {@link useUserTranscript} makes
|
|
28
|
+
* (`""` is speech detected with no words yet — render on it, and read
|
|
29
|
+
* `transcript.text` for the placeholder). Absent, a muted italic line.
|
|
30
|
+
*/
|
|
31
|
+
renderTranscript?: ((transcript: UseUserTranscriptResult) => ReactNode) | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Where the transcript row goes. `"inline"` (the default) is the last row
|
|
34
|
+
* inside the scroll region, as `MessageList` places it; `"below"` renders it
|
|
35
|
+
* after the scroll region as a sibling — the strip a two-pane board pins to
|
|
36
|
+
* the bottom of its conversation column, outside the scroll.
|
|
37
|
+
*/
|
|
38
|
+
transcriptPosition?: "inline" | "below" | undefined;
|
|
39
|
+
/** Rendered inside the scroll region while there is nothing to show at all. */
|
|
40
|
+
empty?: ReactNode | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The `aria-label` of the thinking row. Default `"Thinking"`. Say who: the
|
|
43
|
+
* dots are the only sign the agent is working, and to a screen reader they
|
|
44
|
+
* are punctuation.
|
|
45
|
+
*/
|
|
46
|
+
thinkingLabel?: string | undefined;
|
|
47
|
+
/** What the thinking row shows. Default: three pulsing dots. */
|
|
48
|
+
thinkingIndicator?: ReactNode | undefined;
|
|
49
|
+
/** CSS class names for the thinking row itself (the `role="status"` element). */
|
|
50
|
+
thinkingClassName?: string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Classes for the {@link AutoScroll} container. It must end up with a
|
|
53
|
+
* bounded height (`flex-1 min-h-0`, `h-full`) or nothing pins.
|
|
54
|
+
*/
|
|
55
|
+
className?: string | undefined;
|
|
56
|
+
/** Classes for the scroll region's content element — padding, gap, direction. */
|
|
57
|
+
contentClassName?: string | undefined;
|
|
58
|
+
/** Classes for the scrolling element itself. See {@link AutoScroll}. */
|
|
59
|
+
scrollClassName?: string | undefined;
|
|
60
|
+
/** Inline styles for the scroll container. */
|
|
61
|
+
style?: CSSProperties | undefined;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The conversation's skeleton over {@link useConversation}, with every row a
|
|
65
|
+
* render slot: a pinned scroll region holding the empty state, the interleaved
|
|
66
|
+
* messages and tool calls, the streaming reply and the announced thinking row,
|
|
67
|
+
* plus the live transcript — inside the scroll or pinned beneath it.
|
|
68
|
+
*
|
|
69
|
+
* `useConversation()` already made the DATA one thing: the interleave, the
|
|
70
|
+
* streaming utterance, the `null`-vs-`""` transcript distinction and the
|
|
71
|
+
* thinking-suppression rule. What three custom chromes then each wrote around
|
|
72
|
+
* it was the same fifty lines of STRUCTURE: an `AutoScroll` with a bounded
|
|
73
|
+
* height, an empty-state guard on `items.length === 0 && streaming === null`,
|
|
74
|
+
* the map with its keys, the streaming row, the thinking row with its
|
|
75
|
+
* `role="status"` and `aria-label` (and the same comment about screen readers
|
|
76
|
+
* hearing punctuation), the transcript guarded on `speaking`. The bubbles are
|
|
77
|
+
* the part each template exists to show, so those are slots; the order and the
|
|
78
|
+
* accessibility contract are this component's.
|
|
79
|
+
*
|
|
80
|
+
* {@link MessageList} is this with the stock bubbles filled in.
|
|
81
|
+
*
|
|
82
|
+
* Must be rendered inside the providers `mountClient()` installs.
|
|
83
|
+
*
|
|
84
|
+
* @example A board's radio log: its own bubbles, the stock tool row, the transcript pinned below
|
|
85
|
+
* ```tsx
|
|
86
|
+
* import { ConversationView } from "@alexkroman1/aai-ui";
|
|
87
|
+
*
|
|
88
|
+
* function RadioLog() {
|
|
89
|
+
* return (
|
|
90
|
+
* <ConversationView
|
|
91
|
+
* contentClassName="p-4 flex flex-col gap-2"
|
|
92
|
+
* empty={<p className="text-center opacity-60">Standing by.</p>}
|
|
93
|
+
* renderMessage={({ role, content }) => (
|
|
94
|
+
* <div className={role === "assistant" ? "self-start" : "self-end"}>{content}</div>
|
|
95
|
+
* )}
|
|
96
|
+
* renderTranscript={({ text }) => <div className="px-4 py-2 italic">{text}</div>}
|
|
97
|
+
* transcriptPosition="below"
|
|
98
|
+
* thinkingLabel="Dispatch is thinking"
|
|
99
|
+
* />
|
|
100
|
+
* );
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*
|
|
104
|
+
* @param props - See {@link ConversationViewProps}.
|
|
105
|
+
*
|
|
106
|
+
* @public
|
|
107
|
+
*/
|
|
108
|
+
export declare function ConversationView({ renderMessage, renderTool, renderStreaming, renderTranscript, transcriptPosition, empty, thinkingLabel, thinkingIndicator, thinkingClassName, className, contentClassName, scrollClassName, style, }: ConversationViewProps): ReactNode;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Which of the four buttons a {@link SessionControlButton} is.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export type SessionControlAction = "start" | "toggle" | "restart" | "end";
|
|
8
|
+
/**
|
|
9
|
+
* One button of {@link SessionControls}, as handed to `renderButton`.
|
|
10
|
+
*
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export type SessionControlButton = {
|
|
14
|
+
/** Which button this is. A custom renderer switches on it for its look. */
|
|
15
|
+
action: SessionControlAction;
|
|
16
|
+
/** The label to show — the caller's own word, or the default. */
|
|
17
|
+
label: string;
|
|
18
|
+
/** The handler. Already bound; wire it to `onClick` as it is. */
|
|
19
|
+
onClick: () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the call is live. Meaningful on `toggle`, whose label already says
|
|
22
|
+
* which way it will flip, and handed to every button so a renderer can dim
|
|
23
|
+
* the others while paused.
|
|
24
|
+
*/
|
|
25
|
+
running: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The five words {@link SessionControls} renders, every one overridable.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export type SessionControlsLabels = {
|
|
33
|
+
/** The button shown before the call starts. Default `"Start"`. */
|
|
34
|
+
start: string;
|
|
35
|
+
/** The toggle's label while running. Default `"Pause"`. */
|
|
36
|
+
pause: string;
|
|
37
|
+
/** The toggle's label while paused. Default `"Resume"`. */
|
|
38
|
+
resume: string;
|
|
39
|
+
/** Hang up and dial again. Default `"New Conversation"`. */
|
|
40
|
+
restart: string;
|
|
41
|
+
/** Hang up. Default `"End"`. */
|
|
42
|
+
end: string;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Props of {@link SessionControls}.
|
|
46
|
+
*
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export type SessionControlsProps = {
|
|
50
|
+
/** The words this chrome has its own term for; the rest keep the defaults. */
|
|
51
|
+
labels?: Partial<SessionControlsLabels> | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Renders one button. Absent, each is a stock {@link Button}. A chrome
|
|
54
|
+
* with its own look renders its own `<button>` from the
|
|
55
|
+
* {@link SessionControlButton} it is handed — the component still decides
|
|
56
|
+
* WHICH buttons exist and what each one does.
|
|
57
|
+
*/
|
|
58
|
+
renderButton?: ((button: SessionControlButton) => ReactNode) | undefined;
|
|
59
|
+
/** Additional CSS class names for the row, appended to its own layout classes. */
|
|
60
|
+
className?: string | undefined;
|
|
61
|
+
/** Rendered after the buttons — a count, a spacer, a status line. */
|
|
62
|
+
children?: ReactNode;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* The full control row of a custom chrome: **Start** before the call, then
|
|
66
|
+
* **Pause / Resume**, **New Conversation** and **End** once it is up.
|
|
67
|
+
*
|
|
68
|
+
* {@link Controls} is the stock console's footer — Stop/Resume and New
|
|
69
|
+
* Conversation, with the URL chips — and it has no Start branch and no End,
|
|
70
|
+
* because the default shell shows a start SCREEN and a session there ends by
|
|
71
|
+
* closing the tab. A chrome that owns its whole frame has no start screen, so
|
|
72
|
+
* three of them each wrote this row: the same `!started` branch, the same
|
|
73
|
+
* three buttons behind it, and the same twelve-line comment on why the middle
|
|
74
|
+
* one is `end(); start()` and not `reset()`. The buttons' look is the
|
|
75
|
+
* chrome's — `renderButton` — and everything else is here once.
|
|
76
|
+
*
|
|
77
|
+
* **Why `restart` is `end()` then `start()`, and never `reset()`.** `reset()`
|
|
78
|
+
* clears the transcript and reconnects carrying the same session id, so every
|
|
79
|
+
* `sessionSlot` on the agent survives: a caller who pressed "New Conversation"
|
|
80
|
+
* on a stateful agent got a blank transcript in front of their old cart, game
|
|
81
|
+
* or incident board, with nothing on screen saying so, and the next tool call
|
|
82
|
+
* repopulated it. `end()` drops the resume identity, so the redial is a
|
|
83
|
+
* brand-new session — fresh state, greeting included — and `start()` puts the
|
|
84
|
+
* chrome straight back on the call rather than at its Start button.
|
|
85
|
+
*
|
|
86
|
+
* **Why End is `end()`.** It hangs up and flips `started` back, so the row
|
|
87
|
+
* returns to its Start button and the next start is a new session. `reset()`
|
|
88
|
+
* would keep the call live — the buttons never toggle back.
|
|
89
|
+
*
|
|
90
|
+
* Reads the session through {@link useSessionControls}: two one-field
|
|
91
|
+
* subscriptions, so the row re-renders when a flag flips and not on every
|
|
92
|
+
* transcript partial.
|
|
93
|
+
*
|
|
94
|
+
* @example A board's controls in its own colours, with a trailing count
|
|
95
|
+
* ```tsx
|
|
96
|
+
* import { SessionControls } from "@alexkroman1/aai-ui";
|
|
97
|
+
*
|
|
98
|
+
* const BUTTON = "px-4 py-2 rounded-md text-xs font-semibold cursor-pointer";
|
|
99
|
+
*
|
|
100
|
+
* function ShiftControls({ logged }: { logged: number }) {
|
|
101
|
+
* return (
|
|
102
|
+
* <SessionControls
|
|
103
|
+
* labels={{ start: "Start Dispatch", end: "End Shift" }}
|
|
104
|
+
* renderButton={({ action, label, onClick }) => (
|
|
105
|
+
* <button
|
|
106
|
+
* type="button"
|
|
107
|
+
* className={BUTTON}
|
|
108
|
+
* style={{ background: action === "end" ? "#dc2626" : "#2563eb", color: "white" }}
|
|
109
|
+
* onClick={onClick}
|
|
110
|
+
* >
|
|
111
|
+
* {label}
|
|
112
|
+
* </button>
|
|
113
|
+
* )}
|
|
114
|
+
* >
|
|
115
|
+
* <span className="ml-auto text-[10px]">{logged} incidents logged</span>
|
|
116
|
+
* </SessionControls>
|
|
117
|
+
* );
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @param props - See {@link SessionControlsProps}.
|
|
122
|
+
*
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
export declare function SessionControls({ labels, renderButton, className, children, }: SessionControlsProps): ReactNode;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { AgentState } from "../types.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The dot itself: a coloured circle, pulsing or still.
|
|
5
|
+
*
|
|
6
|
+
* Shared by {@link SessionStateDot} and `ConsoleShell`, which computes its
|
|
7
|
+
* colour from the theme rather than from a palette prop and decides for itself
|
|
8
|
+
* when to pulse. Not on the package barrel — the public component is the one
|
|
9
|
+
* that reads the session.
|
|
10
|
+
*
|
|
11
|
+
* The colour is written to `background` AND `color`, so a caller can extend
|
|
12
|
+
* the dot with a `currentColor` utility (`shadow-[0_0_6px_currentColor]` for a
|
|
13
|
+
* glow) without restating the palette.
|
|
14
|
+
*
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function StateDot({ color, pulseMs, className, title, }: {
|
|
18
|
+
/** The dot's colour. */
|
|
19
|
+
color: string;
|
|
20
|
+
/** Length of one pulse cycle, or `null` for a still dot. */
|
|
21
|
+
pulseMs: number | null;
|
|
22
|
+
/** Size and any extras; the shape classes are the dot's own. */
|
|
23
|
+
className?: string | undefined;
|
|
24
|
+
/** Tooltip text, typically the raw state. */
|
|
25
|
+
title?: string | undefined;
|
|
26
|
+
}): ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* Props of {@link SessionStateDot}.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
export type SessionStateDotProps = {
|
|
33
|
+
/**
|
|
34
|
+
* The dot's colour per {@link AgentState} — the chrome's own palette. A
|
|
35
|
+
* complete record, so a state added upstream is a compile error here rather
|
|
36
|
+
* than a silently unpainted dot; `satisfies Record<AgentState, string>` on the
|
|
37
|
+
* caller's literal is the shape to write it in.
|
|
38
|
+
*/
|
|
39
|
+
colors: Readonly<Record<AgentState, string>>;
|
|
40
|
+
/**
|
|
41
|
+
* The words this chrome has a better term for. Anything not named falls back
|
|
42
|
+
* to {@link AGENT_STATE_LABELS}, so a page overrides one member (`speaking:
|
|
43
|
+
* "Narrating"`) without restating the union.
|
|
44
|
+
*/
|
|
45
|
+
labels?: Partial<Readonly<Record<AgentState, string>>> | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Whether the dot pulses while `listening` (slowly) and `thinking` (faster).
|
|
48
|
+
* Defaults to `true`; a chrome whose dot glows rather than beats passes
|
|
49
|
+
* `false`.
|
|
50
|
+
*/
|
|
51
|
+
pulse?: boolean | undefined;
|
|
52
|
+
/** Additional CSS class names for the wrapping `<span>`, appended to its own. */
|
|
53
|
+
className?: string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* REPLACES the dot's default size (`w-2 h-2`) rather than adding to it —
|
|
56
|
+
* there is no `tailwind-merge` in this package, so two conflicting width
|
|
57
|
+
* utilities would not have a reliable winner. The dot's shape classes stay.
|
|
58
|
+
*/
|
|
59
|
+
dotClassName?: string | undefined;
|
|
60
|
+
/** Additional CSS class names for the label `<span>`. */
|
|
61
|
+
labelClassName?: string | undefined;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The live session state as a coloured dot and a word, on its own narrow
|
|
65
|
+
* subscription.
|
|
66
|
+
*
|
|
67
|
+
* Three custom chromes had each written this: a `satisfies Record<AgentState,
|
|
68
|
+
* string>` palette (kept — it is the prop), a `STATE_LABELS` spread over
|
|
69
|
+
* {@link AGENT_STATE_LABELS} (kept — it is `labels`), and then the same
|
|
70
|
+
* fourteen lines of markup around them, including the same two-arm ternary
|
|
71
|
+
* deciding which states pulse and how fast. `agent-state-labels.ts` argued
|
|
72
|
+
* against a dot component on the grounds that it would take the shared part
|
|
73
|
+
* (the words) hostage to the part that is not (the palette); this takes the
|
|
74
|
+
* palette as a prop precisely so it does not. What is shared is the structure
|
|
75
|
+
* — the exhaustive colour lookup, the label fallback, the pulse rule, the
|
|
76
|
+
* `useSessionStatus()` subscription that keeps the rest of the header from
|
|
77
|
+
* re-rendering at STT-partial rate.
|
|
78
|
+
*
|
|
79
|
+
* Must be rendered inside the providers `mountClient()` installs.
|
|
80
|
+
*
|
|
81
|
+
* @example A dispatch board's readout: its own colours, three of its own words
|
|
82
|
+
* ```tsx
|
|
83
|
+
* import type { AgentState } from "@alexkroman1/aai-ui";
|
|
84
|
+
* import { SessionStateDot } from "@alexkroman1/aai-ui";
|
|
85
|
+
*
|
|
86
|
+
* const STATE_COLORS = {
|
|
87
|
+
* disconnected: "#6b7280",
|
|
88
|
+
* connecting: "#6b7280",
|
|
89
|
+
* ready: "#22c55e",
|
|
90
|
+
* listening: "#22c55e",
|
|
91
|
+
* thinking: "#eab308",
|
|
92
|
+
* speaking: "#3b82f6",
|
|
93
|
+
* error: "#6b7280",
|
|
94
|
+
* } satisfies Record<AgentState, string>;
|
|
95
|
+
*
|
|
96
|
+
* function StatusReadout() {
|
|
97
|
+
* return (
|
|
98
|
+
* <SessionStateDot
|
|
99
|
+
* colors={STATE_COLORS}
|
|
100
|
+
* labels={{ listening: "LISTENING", thinking: "PROCESSING", speaking: "TRANSMITTING" }}
|
|
101
|
+
* labelClassName="text-[11px] uppercase"
|
|
102
|
+
* />
|
|
103
|
+
* );
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* @param props - See {@link SessionStateDotProps}.
|
|
108
|
+
*
|
|
109
|
+
* @public
|
|
110
|
+
*/
|
|
111
|
+
export declare function SessionStateDot({ colors, labels, pulse, className, dotClassName, labelClassName, }: SessionStateDotProps): ReactNode;
|