@alexkroman1/aai-ui 15.1.0 → 16.0.1

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.
@@ -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
- * There is deliberately no `StatusDot` component beside it. The dot markup and
22
- * its palette are what each chrome exists to look like a CRT's glow, a
23
- * dispatch board's alert coloursand a component would take the one part
24
- * that is genuinely shared (the words) hostage to the part that is not.
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-CdOnSh5m.js";
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("span", {
187
- className: "w-[7px] h-[7px] rounded-full",
188
- style: {
189
- background: stateColor(state, theme.primary, inkTint(theme.text, theme.bg, 65)),
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, SessionErrorBanner as r, ChatView as t };
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;
@@ -1,3 +1,3 @@
1
+ import { t as ChatView } from "../chat-view-DsylXJsw.js";
1
2
  import "../context.js";
2
- import { t as ChatView } from "../chat-view-C_3T7Ln8.js";
3
3
  export { ChatView };
@@ -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;
@@ -1,3 +1,3 @@
1
- import { t as MessageList } from "../message-list-CdOnSh5m.js";
1
+ import { t as MessageList } from "../message-list-BLyPRS1Q.js";
2
2
  import "../context.js";
3
3
  export { MessageList };
@@ -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;