@alexkroman1/aai-ui 6.11.0 → 7.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 +104 -3
- package/dist/_run-controls.d.ts +33 -0
- package/dist/{chat-view-ByQFf94G.js → chat-view-BKsFfFZJ.js} +57 -11
- package/dist/client-config-B4nznRvH.js +134 -0
- package/dist/client-config.d.ts +45 -2
- package/dist/client-dir.d.ts +3 -1
- package/dist/client-dir.js +3 -1
- package/dist/components/auto-scroll.d.ts +24 -13
- package/dist/components/button.d.ts +8 -4
- package/dist/components/button.js +4 -4
- package/dist/components/chat-view.d.ts +4 -3
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +71 -10
- package/dist/components/controls.d.ts +15 -4
- package/dist/components/controls.js +48 -2
- package/dist/components/form-fields.d.ts +142 -0
- package/dist/components/form-types.d.ts +6 -0
- package/dist/components/form.d.ts +38 -80
- package/dist/components/markdown.d.ts +31 -5
- package/dist/components/message-list.d.ts +21 -4
- package/dist/components/message-list.js +1 -1
- package/dist/components/sidebar-layout.d.ts +11 -0
- package/dist/components/sidebar-layout.js +2 -0
- package/dist/components/start-screen.d.ts +8 -0
- package/dist/components/start-screen.js +2 -0
- package/dist/components/tool-call-block.js +1 -1
- package/dist/components/tool-call-row.d.ts +24 -0
- package/dist/components/upload-progress.d.ts +17 -7
- package/dist/components/workflow-fields.d.ts +10 -22
- package/dist/components/workflow-progress.d.ts +29 -10
- package/dist/context.d.ts +36 -0
- package/dist/context.js +105 -14
- package/dist/default-client/assets/index-S5fkKi6B.css +2 -0
- package/dist/default-client/assets/index-fEkrcZgo.js +293 -0
- package/dist/default-client/index.html +2 -2
- package/dist/define-client.d.ts +67 -62
- package/dist/define-client.js +56 -22
- package/dist/hooks.d.ts +88 -3
- package/dist/index.d.ts +12 -11
- package/dist/index.js +501 -167
- package/dist/internal.d.ts +40 -0
- package/dist/internal.js +6 -0
- package/dist/{message-list-CpPV7dGx.js → message-list-DHddO4QC.js} +217 -72
- package/dist/{session-core-CAfYmUbg.js → session-core-C2JtLArh.js} +267 -165
- package/dist/session-core-audio-setup.d.ts +3 -0
- package/dist/session-core-messages.d.ts +3 -0
- package/dist/session-core-state.d.ts +146 -0
- package/dist/session-core-types.d.ts +77 -32
- package/dist/session-core.d.ts +3 -2
- package/dist/session-core.js +1 -1
- package/dist/{tool-call-block-D6pTEPrT.js → tool-call-block-DoF-cSIZ.js} +27 -19
- package/dist/tool-config-context-DzAofqi_.js +19 -0
- package/dist/types.d.ts +31 -5
- package/dist/types.js +5 -4
- package/dist/{controls-CjG91QJ4.js → url-chips-DpM7Oocj.js} +3 -46
- package/dist/use-conversation.d.ts +122 -0
- package/dist/use-download-url.d.ts +83 -0
- package/dist/use-workflow-form.d.ts +59 -3
- package/dist/use-workflow-run.d.ts +35 -0
- package/dist/use-workflow-stream.d.ts +36 -62
- package/dist/workflow-client.d.ts +36 -11
- package/dist/workflow-status-labels.d.ts +35 -0
- package/package.json +10 -5
- package/styles.css +14 -0
- package/dist/default-client/assets/index-DTLrhtTF.css +0 -2
- package/dist/default-client/assets/index-DXODx_9r.js +0 -293
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser session's {@link AgentState}, and the error beside it, as a
|
|
3
|
+
* statechart.
|
|
4
|
+
*
|
|
5
|
+
* These two were written independently from thirteen sites across
|
|
6
|
+
* `session-core.ts`, `session-core-messages.ts` and
|
|
7
|
+
* `session-core-audio-setup.ts`, each one deciding for itself whether its write
|
|
8
|
+
* was legal by reading the snapshot back first. Three shipped bugs came out of
|
|
9
|
+
* that, and all three are the same shape — a transition nothing forbade:
|
|
10
|
+
*
|
|
11
|
+
* - **A straggler audio chunk flipped an errored session to `"speaking"`.**
|
|
12
|
+
* Guarded by hand at the call site
|
|
13
|
+
* (`if (snap.state === "error" || (snap.state === "disconnected" && …))`).
|
|
14
|
+
* Here `error` simply does not handle `SPEAK`.
|
|
15
|
+
* - **The frame that announced a session's death also wiped the banner
|
|
16
|
+
* reporting it.** Every fatal path in the host tears the transport down, and
|
|
17
|
+
* tearing down EMITS — so `reply.cancelled` arrived right behind the error and
|
|
18
|
+
* `toListening()` painted a live-mic state over it. A missing provider key is
|
|
19
|
+
* the case that made it visible: the one message that says exactly what to fix
|
|
20
|
+
* was on screen for a few hundred milliseconds and left a session that looked
|
|
21
|
+
* live and was deaf.
|
|
22
|
+
* - **A later frame RECOVERED the state.** `clearRecoveredError` reads a
|
|
23
|
+
* non-error frame as proof the session works, which is right for a
|
|
24
|
+
* turn-level failure and wrong for a server error that ended the call.
|
|
25
|
+
*
|
|
26
|
+
* The second and third were fixed with a `conn.fatalError` boolean that every
|
|
27
|
+
* writer had to remember to consult. It is the `fatal` region here, so
|
|
28
|
+
* forgetting is not available: `LISTEN` and `ACTIVITY` are declined in one
|
|
29
|
+
* place rather than at each of their five call sites.
|
|
30
|
+
*
|
|
31
|
+
* ## Two regions, because the fatal latch OUTLIVES the error state
|
|
32
|
+
*
|
|
33
|
+
* `fatal` is not a substate of `error`, tempting as that looks. It is cleared by
|
|
34
|
+
* exactly one thing — the next `config` frame, i.e. a completed handshake — and
|
|
35
|
+
* that is per CONNECTION rather than per session, so a reconnect that really
|
|
36
|
+
* works is not pinned to a dead session's banner. Between the error and that
|
|
37
|
+
* frame the phase runs `error → connecting → ready` while the latch stays set,
|
|
38
|
+
* which a substate cannot express.
|
|
39
|
+
*
|
|
40
|
+
* ## The published surface is unchanged
|
|
41
|
+
*
|
|
42
|
+
* {@link AgentState} is a public type in the versioned `aai-ui:session`
|
|
43
|
+
* capability, so {@link AgentStateSnapshot.state} is one of its seven names and
|
|
44
|
+
* nothing here widens it. The internal distinctions live in the second region
|
|
45
|
+
* and in context, not in the projection.
|
|
46
|
+
*/
|
|
47
|
+
import type { AgentState, SessionError } from "./types.ts";
|
|
48
|
+
/** What the session core folds into its snapshot after each transition. */
|
|
49
|
+
export type AgentStateSnapshot = {
|
|
50
|
+
state: AgentState;
|
|
51
|
+
error: SessionError | null;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Everything that moves the session's state.
|
|
55
|
+
*
|
|
56
|
+
* Named for what HAPPENED rather than for the state wanted, which is the point:
|
|
57
|
+
* `reply.cancelled` sends `LISTEN` and gets `"error"` back when the session is
|
|
58
|
+
* fatally over, and the caller neither knows nor needs to.
|
|
59
|
+
*/
|
|
60
|
+
export type AgentStateEvent =
|
|
61
|
+
/** A connect attempt started — including a reconnect or a handshake retry. */
|
|
62
|
+
{
|
|
63
|
+
type: "CONNECT";
|
|
64
|
+
}
|
|
65
|
+
/** The socket opened; the handshake has not completed yet. */
|
|
66
|
+
| {
|
|
67
|
+
type: "SOCKET_OPEN";
|
|
68
|
+
}
|
|
69
|
+
/** A completed handshake: a live session, whatever ended the last one. */
|
|
70
|
+
| {
|
|
71
|
+
type: "HANDSHAKE_COMPLETE";
|
|
72
|
+
}
|
|
73
|
+
/** A turn boundary, or the audio path coming up: back to the mic. */
|
|
74
|
+
| {
|
|
75
|
+
type: "LISTEN";
|
|
76
|
+
}
|
|
77
|
+
/** A reset: back to the mic AND the conversation cleared. */
|
|
78
|
+
| {
|
|
79
|
+
type: "RESET";
|
|
80
|
+
}
|
|
81
|
+
/** The agent is working. */
|
|
82
|
+
| {
|
|
83
|
+
type: "THINK";
|
|
84
|
+
}
|
|
85
|
+
/** Audio for this turn reached the client. */
|
|
86
|
+
| {
|
|
87
|
+
type: "SPEAK";
|
|
88
|
+
}
|
|
89
|
+
/** A non-error frame arrived, so the session demonstrably works. */
|
|
90
|
+
| {
|
|
91
|
+
type: "ACTIVITY";
|
|
92
|
+
}
|
|
93
|
+
/** A turn-level failure: a banner over a session that keeps running. */
|
|
94
|
+
| {
|
|
95
|
+
type: "TURN_ERROR";
|
|
96
|
+
error: SessionError;
|
|
97
|
+
}
|
|
98
|
+
/** The session is over. */
|
|
99
|
+
| {
|
|
100
|
+
type: "FATAL";
|
|
101
|
+
error: SessionError;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* A failure that stops the session but might not be the end of it: the
|
|
105
|
+
* handshake exhausting its retries, a socket error close, or the audio path
|
|
106
|
+
* dying. The `fatal` latch stays clear, so a later non-error frame recovers
|
|
107
|
+
* — which is right for a client-side failure and is exactly what a FATAL
|
|
108
|
+
* server error must not get.
|
|
109
|
+
*/
|
|
110
|
+
| {
|
|
111
|
+
type: "FAILED";
|
|
112
|
+
error: SessionError;
|
|
113
|
+
}
|
|
114
|
+
/** The socket closed for good. */
|
|
115
|
+
| {
|
|
116
|
+
type: "CLOSED";
|
|
117
|
+
}
|
|
118
|
+
/** The caller hung up. */
|
|
119
|
+
| {
|
|
120
|
+
type: "DISCONNECT";
|
|
121
|
+
}
|
|
122
|
+
/** The caller ended the session and forgot it. */
|
|
123
|
+
| {
|
|
124
|
+
type: "END";
|
|
125
|
+
};
|
|
126
|
+
/** The session core's handle on its own state. */
|
|
127
|
+
export type SessionStateMachine = {
|
|
128
|
+
/** The published state and the error beside it. */
|
|
129
|
+
snapshot(): AgentStateSnapshot;
|
|
130
|
+
/**
|
|
131
|
+
* Apply `event` and return the resulting projection.
|
|
132
|
+
*
|
|
133
|
+
* Returns rather than writes so the caller folds it into ONE `updateState`
|
|
134
|
+
* call: a snapshot is published on every write, and splitting one transition
|
|
135
|
+
* into two would let a subscriber render half of it.
|
|
136
|
+
*/
|
|
137
|
+
apply(event: AgentStateEvent): AgentStateSnapshot;
|
|
138
|
+
/**
|
|
139
|
+
* Is the session fatally over? Asked by the one caller that has to clear the
|
|
140
|
+
* CONVERSATION and not merely the state — a `session.reset` on a dead session
|
|
141
|
+
* keeps its whole banner.
|
|
142
|
+
*/
|
|
143
|
+
fatal(): boolean;
|
|
144
|
+
};
|
|
145
|
+
/** Create the state machine for one browser session. */
|
|
146
|
+
export declare function createSessionStateMachine(): SessionStateMachine;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Epoch } from "@alexkroman1/aai/internal";
|
|
7
7
|
import type { VoiceIO } from "./audio.ts";
|
|
8
|
-
import type { AgentState, ChatMessage, SessionError, ToolCallInfo,
|
|
8
|
+
import type { AgentState, ChatMessage, SessionError, ToolCallInfo, WebSocketConstructor } from "./types.ts";
|
|
9
9
|
/**
|
|
10
10
|
* A custom event emitted by the agent via `ctx.send(event, data)` — the
|
|
11
11
|
* payload the session records in `SessionSnapshot.customEvents` (`id` is a
|
|
@@ -27,11 +27,30 @@ export type AgentCustomEvent = {
|
|
|
27
27
|
* Consumers (e.g. React hooks via `useSyncExternalStore`) read this to render.
|
|
28
28
|
* A new object reference is created on every state change.
|
|
29
29
|
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* **Four fields describe liveness and they answer different questions.** They
|
|
32
|
+
* are routinely collapsed into one truthy check, which is how a chrome ends up
|
|
33
|
+
* showing a live indicator over a call that has ended:
|
|
34
|
+
*
|
|
35
|
+
* | Field | The question it answers |
|
|
36
|
+
* | --- | --- |
|
|
37
|
+
* | `started` | Has the caller pressed Start? `end()` puts it back to `false`; `disconnect()` does not. |
|
|
38
|
+
* | `running` | Is the socket MEANT to be up? `toggle()` is what flips it. |
|
|
39
|
+
* | `recording` | Is the microphone actually live right now? |
|
|
40
|
+
* | `state` | What is the agent doing — see {@link AgentState}. |
|
|
41
|
+
*
|
|
30
42
|
* @public
|
|
31
43
|
*/
|
|
32
44
|
export type SessionSnapshot = {
|
|
45
|
+
/** What the agent is doing. See {@link AgentState} for the seven members. */
|
|
33
46
|
readonly state: AgentState;
|
|
34
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* True while the microphone is live and streaming to the server.
|
|
49
|
+
*
|
|
50
|
+
* This is the mic, not the session: a session can be `running` with the mic
|
|
51
|
+
* still opening, and a failure to acquire it leaves this `false` with the
|
|
52
|
+
* socket up.
|
|
53
|
+
*/
|
|
35
54
|
readonly recording: boolean;
|
|
36
55
|
/**
|
|
37
56
|
* The WebSocket URL a program can connect to directly — the long-living
|
|
@@ -50,8 +69,24 @@ export type SessionSnapshot = {
|
|
|
50
69
|
* never collides when the capped arrays slide.
|
|
51
70
|
*/
|
|
52
71
|
readonly contentVersion: number;
|
|
72
|
+
/**
|
|
73
|
+
* The conversation so far, oldest first — user and assistant turns only.
|
|
74
|
+
* Tool activity is NOT in here; it is in `toolCalls`. Capped, so the oldest
|
|
75
|
+
* entries slide off a long call.
|
|
76
|
+
*/
|
|
53
77
|
readonly messages: ChatMessage[];
|
|
78
|
+
/**
|
|
79
|
+
* Every tool call the agent has made this session, in order, each carrying
|
|
80
|
+
* its own pending/settled state. Capped like `messages`. `useToolResult` and
|
|
81
|
+
* `useToolCallStart` are the narrow readers; this is the whole log.
|
|
82
|
+
*/
|
|
54
83
|
readonly toolCalls: ToolCallInfo[];
|
|
84
|
+
/**
|
|
85
|
+
* Custom events the agent pushed with `ctx.send(event, data)`, in order.
|
|
86
|
+
* A LOG rather than a value — `useEvent(name, cb)` is the reader that
|
|
87
|
+
* delivers each one exactly once; reading this array directly means owning
|
|
88
|
+
* the cursor yourself.
|
|
89
|
+
*/
|
|
55
90
|
readonly customEvents: AgentCustomEvent[];
|
|
56
91
|
/**
|
|
57
92
|
* Latest state the agent projected via `syncState`, or `null` before the
|
|
@@ -59,10 +94,50 @@ export type SessionSnapshot = {
|
|
|
59
94
|
* reads current state rather than replaying events it missed.
|
|
60
95
|
*/
|
|
61
96
|
readonly agentState: unknown;
|
|
97
|
+
/**
|
|
98
|
+
* The caller's in-progress turn, as STT reports it.
|
|
99
|
+
*
|
|
100
|
+
* **`null` and `""` are different turns, and collapsing them is the mistake
|
|
101
|
+
* this field invites.** `null` is silence; `""` is speech DETECTED with no
|
|
102
|
+
* words back yet — where a live session sits for a few hundred milliseconds
|
|
103
|
+
* at the start of every turn. Read as one falsy check, the live-transcript
|
|
104
|
+
* row appears a beat late, on the first word rather than on the first sound,
|
|
105
|
+
* which is the moment it exists for.
|
|
106
|
+
*
|
|
107
|
+
* Prefer {@link useUserTranscript}, which returns the distinction as two
|
|
108
|
+
* named things (`speaking` to render on, `text` with a placeholder) rather
|
|
109
|
+
* than leaving each chrome to re-derive the ternary.
|
|
110
|
+
*
|
|
111
|
+
* Cleared when the turn is committed to `messages`.
|
|
112
|
+
*/
|
|
62
113
|
readonly userTranscript: string | null;
|
|
114
|
+
/**
|
|
115
|
+
* The agent's reply as it streams, or `null` when it is not speaking.
|
|
116
|
+
* Cleared when the reply is committed to `messages`, so a chrome renders
|
|
117
|
+
* this row and the finished message, never both.
|
|
118
|
+
*/
|
|
63
119
|
readonly agentTranscript: string | null;
|
|
120
|
+
/**
|
|
121
|
+
* The session's current failure, or `null`. Carries a `code`
|
|
122
|
+
* ({@link SessionErrorCode}), a message, and whether it was FATAL.
|
|
123
|
+
*
|
|
124
|
+
* A fatal error LATCHES: nothing clears it but the next completed handshake,
|
|
125
|
+
* because the frame announcing a session's death is also the frame that used
|
|
126
|
+
* to wipe the message explaining it. A non-fatal one is retired by later
|
|
127
|
+
* activity, which is what the recovery was written for.
|
|
128
|
+
*/
|
|
64
129
|
readonly error: SessionError | null;
|
|
130
|
+
/**
|
|
131
|
+
* Whether the caller has pressed Start. `false` until the first `start()`,
|
|
132
|
+
* and back to `false` after `end()` — which is what makes a start-screen
|
|
133
|
+
* chrome show its Start control again. `disconnect()` leaves it `true`.
|
|
134
|
+
*/
|
|
65
135
|
readonly started: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Whether the session is MEANT to be connected — the pause/resume state
|
|
138
|
+
* `toggle()` flips, not a report of the socket. A reconnecting session is
|
|
139
|
+
* still `running`.
|
|
140
|
+
*/
|
|
66
141
|
readonly running: boolean;
|
|
67
142
|
};
|
|
68
143
|
/**
|
|
@@ -123,14 +198,6 @@ export type SessionCore = {
|
|
|
123
198
|
/** Alias for `disconnect` for use with `using`. */
|
|
124
199
|
[Symbol.dispose](): void;
|
|
125
200
|
};
|
|
126
|
-
/**
|
|
127
|
-
* Options accepted by `createSessionCore` — an alias of
|
|
128
|
-
* {@link VoiceSessionOptions}, which documents every field. Two names, one
|
|
129
|
-
* type: `client()` and `createSessionCore` share the same session options.
|
|
130
|
-
*
|
|
131
|
-
* @public
|
|
132
|
-
*/
|
|
133
|
-
export type SessionCoreOptions = VoiceSessionOptions;
|
|
134
201
|
/**
|
|
135
202
|
* Shared mutable connection state for audio initialization.
|
|
136
203
|
*
|
|
@@ -151,28 +218,6 @@ export type ConnState = {
|
|
|
151
218
|
* reconnect on demand).
|
|
152
219
|
*/
|
|
153
220
|
retiredByServer: boolean;
|
|
154
|
-
/**
|
|
155
|
-
* A FATAL error has been reported for this connection — the session is
|
|
156
|
-
* over, and no later frame may take its banner off the screen.
|
|
157
|
-
*
|
|
158
|
-
* The recovery rule below it (`clearRecoveredError`) reads a non-error
|
|
159
|
-
* frame as proof the session works, which is right for a client-side or
|
|
160
|
-
* turn-level failure and wrong for a server error that ended the call:
|
|
161
|
-
* every fatal path in the host tears the transport down, and tearing down
|
|
162
|
-
* EMITS — `terminate()` calls `onCancelled()` immediately after
|
|
163
|
-
* `emitError`. So the very next frame recovered the state to "listening"
|
|
164
|
-
* and wiped the message. A missing provider key is the case that made this
|
|
165
|
-
* visible: the one error that says exactly what to go and fix
|
|
166
|
-
* ("Cartesia TTS: missing API key. Set CARTESIA_API_KEY in the agent env.")
|
|
167
|
-
* was on screen for a few hundred milliseconds and left a session that
|
|
168
|
-
* looked live and was deaf.
|
|
169
|
-
*
|
|
170
|
-
* Cleared by exactly one thing — the next `config` frame, i.e. a completed
|
|
171
|
-
* handshake. That is per CONNECTION rather than per session, so a
|
|
172
|
-
* reconnect that really works is not pinned to a dead session's banner,
|
|
173
|
-
* and it is the only frame a dying session cannot produce.
|
|
174
|
-
*/
|
|
175
|
-
fatalError: boolean;
|
|
176
221
|
voiceIO: VoiceIO | null;
|
|
177
222
|
audioSetupInFlight: boolean;
|
|
178
223
|
/** Connection epoch, bumped on each connect()/retry (see `createEpoch`).
|
package/dist/session-core.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { SessionCore
|
|
1
|
+
import type { SessionCore } from "./session-core-types.ts";
|
|
2
|
+
import { type VoiceSessionOptions } from "./types.ts";
|
|
2
3
|
/**
|
|
3
4
|
* Create a framework-agnostic voice session core that connects to an AAI
|
|
4
5
|
* server via WebSocket.
|
|
@@ -26,4 +27,4 @@ import type { SessionCore, SessionCoreOptions } from "./session-core-types.ts";
|
|
|
26
27
|
*
|
|
27
28
|
* @public
|
|
28
29
|
*/
|
|
29
|
-
export declare function createSessionCore(options:
|
|
30
|
+
export declare function createSessionCore(options: VoiceSessionOptions): SessionCore;
|
package/dist/session-core.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createSessionCore } from "./session-core-
|
|
1
|
+
import { t as createSessionCore } from "./session-core-C2JtLArh.js";
|
|
2
2
|
export { createSessionCore };
|
|
@@ -2,9 +2,10 @@ import { useTheme } from "./context.js";
|
|
|
2
2
|
import { r as inkTint } from "./_colors-CcAi2FOU.js";
|
|
3
3
|
import { t as Eyebrow } from "./eyebrow-C6ZFuiz6.js";
|
|
4
4
|
import { i as tryParseJSON, r as truncate } from "./_utils-B6498_bm.js";
|
|
5
|
+
import { n as useToolConfig } from "./tool-config-context-DzAofqi_.js";
|
|
5
6
|
import clsx from "clsx";
|
|
6
7
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import {
|
|
8
|
+
import { memo, useMemo, useState } from "react";
|
|
8
9
|
//#region components/tool-call-row.tsx
|
|
9
10
|
/** @jsxImportSource react */
|
|
10
11
|
const VARIANT_CLASSES = {
|
|
@@ -33,6 +34,30 @@ const VARIANT_CLASSES = {
|
|
|
33
34
|
* Colors come from the nearest theme context (see {@link useTheme}); without
|
|
34
35
|
* a provider the default AssemblyAI theme applies.
|
|
35
36
|
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* import { ToolCallRow, useSession } from "@alexkroman1/aai-ui";
|
|
40
|
+
*
|
|
41
|
+
* // A custom chrome's tool log, from the snapshot's own `toolCalls`.
|
|
42
|
+
* function ToolLog() {
|
|
43
|
+
* const { toolCalls } = useSession();
|
|
44
|
+
* return (
|
|
45
|
+
* <div>
|
|
46
|
+
* {toolCalls.map((call) => (
|
|
47
|
+
* <ToolCallRow
|
|
48
|
+
* key={call.callId}
|
|
49
|
+
* title={call.name}
|
|
50
|
+
* detail={call.result}
|
|
51
|
+
* pending={call.status === "pending"}
|
|
52
|
+
* />
|
|
53
|
+
* ))}
|
|
54
|
+
* </div>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @param props - See {@link ToolCallRowProps}.
|
|
60
|
+
*
|
|
36
61
|
* @public
|
|
37
62
|
*/
|
|
38
63
|
function ToolCallRow({ title, detail, pending = false, icon, variant = "default", className, children }) {
|
|
@@ -91,23 +116,6 @@ function ToolCallRow({ title, detail, pending = false, icon, variant = "default"
|
|
|
91
116
|
});
|
|
92
117
|
}
|
|
93
118
|
//#endregion
|
|
94
|
-
//#region components/tool-config-context.ts
|
|
95
|
-
/**
|
|
96
|
-
* Context for tool display configuration. Installed by `client()` from
|
|
97
|
-
* `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
|
|
98
|
-
*
|
|
99
|
-
* @internal
|
|
100
|
-
*/
|
|
101
|
-
const ToolConfigContext = createContext({});
|
|
102
|
-
/**
|
|
103
|
-
* Read tool display configuration from the nearest `ToolConfigContext.Provider`.
|
|
104
|
-
*
|
|
105
|
-
* @internal
|
|
106
|
-
*/
|
|
107
|
-
function useToolConfig() {
|
|
108
|
-
return useContext(ToolConfigContext);
|
|
109
|
-
}
|
|
110
|
-
//#endregion
|
|
111
119
|
//#region components/tool-call-block.tsx
|
|
112
120
|
/** @jsxImportSource react */
|
|
113
121
|
function formatResult(result) {
|
|
@@ -174,4 +182,4 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
|
|
|
174
182
|
});
|
|
175
183
|
});
|
|
176
184
|
//#endregion
|
|
177
|
-
export {
|
|
185
|
+
export { ToolCallRow as n, ToolCallBlock as t };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
//#region components/tool-config-context.ts
|
|
3
|
+
/**
|
|
4
|
+
* Context for tool display configuration. Installed by `client()` from
|
|
5
|
+
* `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const ToolConfigContext = createContext({});
|
|
10
|
+
/**
|
|
11
|
+
* Read tool display configuration from the nearest `ToolConfigContext.Provider`.
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
14
|
+
*/
|
|
15
|
+
function useToolConfig() {
|
|
16
|
+
return useContext(ToolConfigContext);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { useToolConfig as n, ToolConfigContext as t };
|
package/dist/types.d.ts
CHANGED
|
@@ -21,14 +21,40 @@ export { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
|
|
|
21
21
|
*
|
|
22
22
|
* Cast because `voiceIsolation` is newer than TypeScript's DOM lib.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
|
|
25
|
+
* reason as the audio budgets above: it is a framework decision with no
|
|
26
|
+
* `client()` field to set, and the root is the authoring surface. A custom
|
|
27
|
+
* chrome that bypasses `client()` and opens its own microphone reaches it there
|
|
28
|
+
* alongside the providers it also needs.
|
|
28
29
|
*/
|
|
29
30
|
export declare const VOICE_CAPTURE_CONSTRAINTS: MediaTrackConstraints;
|
|
30
31
|
/**
|
|
31
|
-
* Current state of the voice agent session
|
|
32
|
+
* Current state of the voice agent session — the `state` field of
|
|
33
|
+
* {@link SessionSnapshot}, and what a chrome paints its status indicator from.
|
|
34
|
+
*
|
|
35
|
+
* @remarks
|
|
36
|
+
* The seven members, in the order a call passes through them:
|
|
37
|
+
*
|
|
38
|
+
* - `"disconnected"` — no socket. The state before the first `start()` and
|
|
39
|
+
* after `disconnect()` / `end()`.
|
|
40
|
+
* - `"connecting"` — dialling. Covers the broker lookup and every automatic
|
|
41
|
+
* reconnect attempt, so a session flickers back through it mid-call.
|
|
42
|
+
* - `"ready"` — the socket is open and the handshake is done, but no turn has
|
|
43
|
+
* happened yet. **The default chrome paints this with the same live
|
|
44
|
+
* indicator as `"listening"`**, which is deliberate — to a caller they are
|
|
45
|
+
* the same "the agent is there" — but they are not the same thing, and a
|
|
46
|
+
* session can wedge here (see `session-core-handshake.ts`).
|
|
47
|
+
* - `"listening"` — the microphone is open and the agent is waiting for the
|
|
48
|
+
* caller. Check {@link SessionSnapshot.recording} for whether the mic is
|
|
49
|
+
* actually live.
|
|
50
|
+
* - `"thinking"` — the caller's turn is committed and the agent is working:
|
|
51
|
+
* the LLM step, and any tool calls under it.
|
|
52
|
+
* - `"speaking"` — the agent's reply is playing. A caller may still barge in;
|
|
53
|
+
* the mic stays open throughout.
|
|
54
|
+
* - `"error"` — the session reported a failure. See
|
|
55
|
+
* {@link SessionSnapshot.error} for what it was. A FATAL error latches here
|
|
56
|
+
* until the next completed handshake, so a later frame cannot quietly paint
|
|
57
|
+
* over the banner explaining a dead call.
|
|
32
58
|
*
|
|
33
59
|
* @public
|
|
34
60
|
*/
|
package/dist/types.js
CHANGED
|
@@ -20,10 +20,11 @@ import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
|
|
|
20
20
|
*
|
|
21
21
|
* Cast because `voiceIsolation` is newer than TypeScript's DOM lib.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
23
|
+
* On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
|
|
24
|
+
* reason as the audio budgets above: it is a framework decision with no
|
|
25
|
+
* `client()` field to set, and the root is the authoring surface. A custom
|
|
26
|
+
* chrome that bypasses `client()` and opens its own microphone reaches it there
|
|
27
|
+
* alongside the providers it also needs.
|
|
27
28
|
*/
|
|
28
29
|
const VOICE_CAPTURE_CONSTRAINTS = {
|
|
29
30
|
echoCancellation: true,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useSessionSelector, useTheme } from "./context.js";
|
|
2
2
|
import { r as inkTint } from "./_colors-CcAi2FOU.js";
|
|
3
|
-
import { Button } from "./components/button.js";
|
|
4
3
|
import { t as pageBaseUrl } from "./_utils-B6498_bm.js";
|
|
5
4
|
import clsx from "clsx";
|
|
6
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import {
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
7
|
//#region components/url-chips.tsx
|
|
9
8
|
/** @jsxImportSource react */
|
|
10
9
|
/** How long the "Copied" confirmation replaces the label after a click. */
|
|
@@ -97,46 +96,4 @@ function SessionUrlChips({ className }) {
|
|
|
97
96
|
});
|
|
98
97
|
}
|
|
99
98
|
//#endregion
|
|
100
|
-
|
|
101
|
-
/** @jsxImportSource react */
|
|
102
|
-
/**
|
|
103
|
-
* Session control buttons: **Stop / Resume** and **New Conversation**.
|
|
104
|
-
*
|
|
105
|
-
* Reads session state from {@link useSession}. Must be rendered inside a
|
|
106
|
-
* `SessionProvider`.
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* ```tsx
|
|
110
|
-
* import { Controls } from "@alexkroman1/aai-ui";
|
|
111
|
-
*
|
|
112
|
-
* function Footer() {
|
|
113
|
-
* return <Controls className="justify-end" />;
|
|
114
|
-
* }
|
|
115
|
-
* ```
|
|
116
|
-
*
|
|
117
|
-
* @param className - Additional CSS class names applied to the container.
|
|
118
|
-
*
|
|
119
|
-
* @public
|
|
120
|
-
*/
|
|
121
|
-
const Controls = memo(function Controls({ className }) {
|
|
122
|
-
const running = useSessionSelector((s) => s.running);
|
|
123
|
-
const { toggle, reset } = useSessionCore();
|
|
124
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
125
|
-
className: clsx("flex flex-wrap items-center gap-3 shrink-0", className),
|
|
126
|
-
children: [
|
|
127
|
-
/* @__PURE__ */ jsx(Button, {
|
|
128
|
-
variant: "secondary",
|
|
129
|
-
onClick: toggle,
|
|
130
|
-
children: running ? "Stop" : "Resume"
|
|
131
|
-
}),
|
|
132
|
-
/* @__PURE__ */ jsx(Button, {
|
|
133
|
-
variant: "ghost",
|
|
134
|
-
onClick: reset,
|
|
135
|
-
children: "New Conversation"
|
|
136
|
-
}),
|
|
137
|
-
/* @__PURE__ */ jsx(SessionUrlChips, { className: "basis-full sm:basis-auto sm:ml-auto sm:max-w-[60%]" })
|
|
138
|
-
]
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
//#endregion
|
|
142
|
-
export { UiUrlChip as i, ApiUrlChip as n, SessionUrlChips as r, Controls as t };
|
|
99
|
+
export { SessionUrlChips as n, UiUrlChip as r, ApiUrlChip as t };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useConversation` — the exchange, already assembled, with nothing rendered.
|
|
3
|
+
*
|
|
4
|
+
* `<MessageList>` owns four decisions that are not obvious and are not
|
|
5
|
+
* derivable from the snapshot by eye: the message/tool-call INTERLEAVE, the
|
|
6
|
+
* streaming agent utterance, the live user transcript with its `null`-vs-`""`
|
|
7
|
+
* protocol distinction, and the thinking indicator's suppression rule. Its only
|
|
8
|
+
* prop is `className`, so the moment a client wants its own bubble markup it
|
|
9
|
+
* drops all four at once — and the three hand-rolled chromes in the template
|
|
10
|
+
* tree each shipped a strictly worse conversation for exactly that reason. One
|
|
11
|
+
* of them runs fifteen tools and its operator sees none of them, because the
|
|
12
|
+
* tool calls live in a second array nothing in that page ever reads.
|
|
13
|
+
*
|
|
14
|
+
* This is the headless half. `<MessageList>` is now a thin consumer of it —
|
|
15
|
+
* which is the only way to know the hook is complete, since a hook that the
|
|
16
|
+
* package's own list cannot be built from is a hook a custom chrome will find a
|
|
17
|
+
* hole in.
|
|
18
|
+
*
|
|
19
|
+
* ## It subscribes NARROWLY, and that is half the value
|
|
20
|
+
*
|
|
21
|
+
* The three custom chromes also call whole-page `useSession()`, which re-renders
|
|
22
|
+
* on *every* snapshot change — so a dispatch board re-renders at STT-partial
|
|
23
|
+
* rate. `<ChatView>` deliberately avoids that with per-field
|
|
24
|
+
* {@link useSessionSelector} calls, and so does this: a component that reads the
|
|
25
|
+
* conversation gets the conversation's own update rate, not the session's.
|
|
26
|
+
*/
|
|
27
|
+
import type { ChatMessage, ToolCallInfo } from "./types.ts";
|
|
28
|
+
import { type UseUserTranscriptResult } from "./use-user-transcript.ts";
|
|
29
|
+
/**
|
|
30
|
+
* One row of the conversation: a finalized message, or a tool invocation.
|
|
31
|
+
*
|
|
32
|
+
* A discriminated union rather than two arrays, because the ORDER between them
|
|
33
|
+
* is the thing this hook computes — handing back two lists would hand back the
|
|
34
|
+
* problem. `kind` is what a `switch` in a custom renderer narrows on.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export type ConversationItem = {
|
|
39
|
+
kind: "message";
|
|
40
|
+
message: ChatMessage;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "tool";
|
|
43
|
+
toolCall: ToolCallInfo;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* What {@link useConversation} returns.
|
|
47
|
+
*
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export type UseConversationResult = {
|
|
51
|
+
/**
|
|
52
|
+
* Messages and tool calls in one list, in the order they happened.
|
|
53
|
+
*
|
|
54
|
+
* Referentially stable while neither array changes, so a consumer may map it
|
|
55
|
+
* inside a `useMemo` keyed on it, or hand rows to `memo()`ed components,
|
|
56
|
+
* without rebuilding the list on unrelated snapshot updates.
|
|
57
|
+
*/
|
|
58
|
+
items: readonly ConversationItem[];
|
|
59
|
+
/**
|
|
60
|
+
* The agent's utterance as it arrives, or `null` between turns.
|
|
61
|
+
*
|
|
62
|
+
* Not yet a member of `items`: it has no id and it is replaced wholesale on
|
|
63
|
+
* every delta, so it is rendered as its own trailing row and disappears when
|
|
64
|
+
* the finalized message takes its place.
|
|
65
|
+
*/
|
|
66
|
+
streaming: string | null;
|
|
67
|
+
/**
|
|
68
|
+
* The caller's in-progress turn — {@link useUserTranscript}'s result,
|
|
69
|
+
* forwarded rather than re-derived, so the `null`-vs-`""` distinction is made
|
|
70
|
+
* in exactly one place.
|
|
71
|
+
*/
|
|
72
|
+
transcript: UseUserTranscriptResult;
|
|
73
|
+
/**
|
|
74
|
+
* Whether to show a thinking indicator.
|
|
75
|
+
*
|
|
76
|
+
* The suppression rule, and it is why this is a field rather than
|
|
77
|
+
* `state === "thinking"`: the agent is `thinking` for a stretch during which
|
|
78
|
+
* something ELSE is already saying so. A pending tool call draws its own
|
|
79
|
+
* spinner, and a trailing agent message means the reply has begun landing —
|
|
80
|
+
* in both cases a second indicator underneath reads as a second thing
|
|
81
|
+
* happening. So it is on only while `thinking` with no pending tool call, and
|
|
82
|
+
* either no messages yet, a trailing USER message, or a settled tool call
|
|
83
|
+
* after it.
|
|
84
|
+
*/
|
|
85
|
+
thinking: boolean;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Subscribe to the conversation: the interleaved exchange, the streaming
|
|
89
|
+
* utterance, the live transcript and the thinking rule — with no markup.
|
|
90
|
+
*
|
|
91
|
+
* Must be used inside the provider `client()` installs.
|
|
92
|
+
*
|
|
93
|
+
* @example A custom bubble, keeping every rule `<MessageList>` knows
|
|
94
|
+
* ```tsx
|
|
95
|
+
* import { useConversation } from "@alexkroman1/aai-ui";
|
|
96
|
+
*
|
|
97
|
+
* function Transcript() {
|
|
98
|
+
* const { items, streaming, transcript, thinking } = useConversation();
|
|
99
|
+
* return (
|
|
100
|
+
* <div>
|
|
101
|
+
* {items.map((item) =>
|
|
102
|
+
* item.kind === "message" ? (
|
|
103
|
+
* <p key={item.message.id} data-role={item.message.role}>
|
|
104
|
+
* {item.message.content}
|
|
105
|
+
* </p>
|
|
106
|
+
* ) : (
|
|
107
|
+
* <code key={item.toolCall.callId}>{item.toolCall.name}</code>
|
|
108
|
+
* ),
|
|
109
|
+
* )}
|
|
110
|
+
* {streaming !== null && <p data-role="assistant">{streaming}</p>}
|
|
111
|
+
* {transcript.speaking && <p data-role="user">{transcript.text}</p>}
|
|
112
|
+
* {thinking && <p>…</p>}
|
|
113
|
+
* </div>
|
|
114
|
+
* );
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* @returns See {@link UseConversationResult}.
|
|
119
|
+
*
|
|
120
|
+
* @public
|
|
121
|
+
*/
|
|
122
|
+
export declare function useConversation(): UseConversationResult;
|