@alexkroman1/aai-ui 3.2.0 → 4.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/audio.d.ts +0 -33
- package/dist/audio.js +2 -20
- package/dist/{chat-view-C1oJxsWz.js → chat-view-CW-tZCMm.js} +1 -4
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +0 -9
- package/dist/components/markdown.d.ts +21 -0
- package/dist/components/message-list.d.ts +0 -8
- package/dist/components/message-list.js +3 -230
- package/dist/components/url-chips.d.ts +0 -15
- package/dist/context.d.ts +1 -1
- package/dist/default-client/assets/audio-DQseUQnN.js +1 -0
- package/dist/default-client/assets/{capture-processor-DYl6YIu5.js → capture-processor-DKFNcEhw.js} +1 -1
- package/dist/default-client/assets/index-CRfdZPH1.css +2 -0
- package/dist/default-client/assets/index-CkZ6t_eJ.js +417 -0
- package/dist/default-client/assets/{playback-processor-CeEGzgVT.js → playback-processor-HeyncQUX.js} +1 -1
- package/dist/default-client/index.html +2 -2
- package/dist/{define-client-fjNCp9be.js → define-client-DqRs1XPd.js} +2 -2
- package/dist/define-client.d.ts +1 -1
- package/dist/define-client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -7
- package/dist/message-list-q2lTiFhF.js +381 -0
- package/dist/{session-core-DgioOYyx.js → session-core-CkZ8LyEA.js} +7 -23
- package/dist/session-core-audio-setup.d.ts +3 -6
- package/dist/session-core-types.d.ts +0 -9
- package/dist/session-core.d.ts +0 -1
- package/dist/session-core.js +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +2 -2
- package/package.json +4 -2
- package/dist/default-client/assets/audio-CIZMTfDQ.js +0 -1
- package/dist/default-client/assets/index-BeVXjGwG.js +0 -344
- package/dist/default-client/assets/index-Dv-Q5VRL.css +0 -2
package/dist/default-client/assets/{playback-processor-CeEGzgVT.js → playback-processor-HeyncQUX.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{i as e}from"./index-
|
|
1
|
+
import{i as e}from"./index-CkZ6t_eJ.js";import{t}from"./_module-url-BX0RuRU2.js";var n=t(`
|
|
2
2
|
class PlaybackProcessor extends AudioWorkletProcessor {
|
|
3
3
|
constructor(options) {
|
|
4
4
|
super();
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<title>aai</title>
|
|
7
7
|
<link rel="icon" href="./favicon.ico" />
|
|
8
8
|
<style>html, body { background: #FBF8F2; margin: 0; }</style>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-CkZ6t_eJ.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CRfdZPH1.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<main id="app"></main>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { SessionProvider, ThemeProvider } from "./context.js";
|
|
2
|
-
import { t as ChatView } from "./chat-view-
|
|
2
|
+
import { t as ChatView } from "./chat-view-CW-tZCMm.js";
|
|
3
3
|
import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
|
|
4
4
|
import { SidebarLayout } from "./components/sidebar-layout.js";
|
|
5
5
|
import { StartScreen } from "./components/start-screen.js";
|
|
6
|
-
import { t as createSessionCore } from "./session-core-
|
|
6
|
+
import { t as createSessionCore } from "./session-core-CkZ8LyEA.js";
|
|
7
7
|
import { CLIENT_CONFIG_PATH, ClientConfigResponseSchema } from "@alexkroman1/aai/protocol";
|
|
8
8
|
import { createElement, useEffect, useState } from "react";
|
|
9
9
|
import { jsx } from "react/jsx-runtime";
|
package/dist/define-client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
2
|
import { type ComponentType } from "react";
|
|
3
3
|
import { type ToolDisplayConfig } from "./components/tool-config-context.ts";
|
|
4
|
-
import {
|
|
4
|
+
import type { SessionCore } from "./session-core-types.ts";
|
|
5
5
|
import type { ClientTheme, WebSocketConstructor } from "./types.ts";
|
|
6
6
|
/**
|
|
7
7
|
* Base options shared by both client tiers.
|
package/dist/define-client.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { floatToPcm16 } from "./audio.ts";
|
|
2
1
|
export { buildAgentUrl, type ClientConfigResponse, fetchClientConfig, } from "./client-config.ts";
|
|
3
2
|
export { Button } from "./components/button.tsx";
|
|
4
3
|
export { ChatView } from "./components/chat-view.tsx";
|
|
5
4
|
export { Controls } from "./components/controls.tsx";
|
|
5
|
+
export { Markdown } from "./components/markdown.tsx";
|
|
6
6
|
export { MessageList } from "./components/message-list.tsx";
|
|
7
7
|
export { SidebarLayout } from "./components/sidebar-layout.tsx";
|
|
8
8
|
export { StartScreen } from "./components/start-screen.tsx";
|
|
@@ -14,7 +14,7 @@ export { SessionProvider, ThemeProvider, useSession, useSessionSelector, useThem
|
|
|
14
14
|
export type { ClientConfig, ClientHandle, } from "./define-client.tsx";
|
|
15
15
|
export { client } from "./define-client.tsx";
|
|
16
16
|
export { useEvent, useToolCallStart, useToolResult } from "./hooks.ts";
|
|
17
|
-
export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core.ts";
|
|
18
17
|
export { createSessionCore } from "./session-core.ts";
|
|
18
|
+
export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core-types.ts";
|
|
19
19
|
export type { AgentState, ChatMessage, ClientTheme, SessionError, SessionErrorCode, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor, } from "./types.ts";
|
|
20
20
|
export { VOICE_CAPTURE_CONSTRAINTS } from "./types.ts";
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { floatToPcm16 } from "./audio.js";
|
|
3
|
-
import { n as buildAgentUrl, r as fetchClientConfig, t as client } from "./define-client-fjNCp9be.js";
|
|
1
|
+
import { n as buildAgentUrl, r as fetchClientConfig, t as client } from "./define-client-DqRs1XPd.js";
|
|
4
2
|
import { SessionProvider, ThemeProvider, useSession, useSessionSelector, useTheme } from "./context.js";
|
|
5
3
|
import { Button } from "./components/button.js";
|
|
6
|
-
import { t as ChatView } from "./chat-view-
|
|
4
|
+
import { t as ChatView } from "./chat-view-CW-tZCMm.js";
|
|
7
5
|
import { i as UiUrlChip, n as ApiUrlChip, r as SessionUrlChips, t as Controls } from "./controls-DV368uhb.js";
|
|
6
|
+
import { n as Markdown, t as MessageList } from "./message-list-q2lTiFhF.js";
|
|
8
7
|
import { n as ToolConfigContext } from "./tool-call-block-DIxpG8GM.js";
|
|
9
|
-
import { MessageList } from "./components/message-list.js";
|
|
10
8
|
import { SidebarLayout } from "./components/sidebar-layout.js";
|
|
11
9
|
import { StartScreen } from "./components/start-screen.js";
|
|
12
|
-
import { t as createSessionCore } from "./session-core-
|
|
10
|
+
import { t as createSessionCore } from "./session-core-CkZ8LyEA.js";
|
|
11
|
+
import { VOICE_CAPTURE_CONSTRAINTS } from "./types.js";
|
|
13
12
|
import { useEvent, useToolCallStart, useToolResult } from "./hooks.js";
|
|
14
|
-
export { ApiUrlChip, Button, ChatView, Controls, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, client, createSessionCore, fetchClientConfig,
|
|
13
|
+
export { ApiUrlChip, Button, ChatView, Controls, Markdown, MessageList, SessionProvider, SessionUrlChips, SidebarLayout, StartScreen, ThemeProvider, ToolConfigContext, UiUrlChip, VOICE_CAPTURE_CONSTRAINTS, buildAgentUrl, client, createSessionCore, fetchClientConfig, useEvent, useSession, useSessionSelector, useTheme, useToolCallStart, useToolResult };
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { useSessionSelector, useTheme } from "./context.js";
|
|
2
|
+
import { a as primaryTint, i as TEXT_MUTED, n as SURFACE_TINT, r as TEXT_FAINT } from "./_colors-DYX7XRTr.js";
|
|
3
|
+
import { t as ToolCallBlock } from "./tool-call-block-DIxpG8GM.js";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { memo, useCallback, useEffect, useMemo, useRef } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import ReactMarkdown from "react-markdown";
|
|
8
|
+
import remarkGfm from "remark-gfm";
|
|
9
|
+
//#region components/markdown.tsx
|
|
10
|
+
/** @jsxImportSource react */
|
|
11
|
+
const BARE_ORDERED_MARKER = /^(\s*)(\d{1,9})([.)])\s*$/;
|
|
12
|
+
const BARE_BULLET_MARKER = /^(\s*)([-*+])\s*$/;
|
|
13
|
+
const CODE_FENCE = /^\s*(```|~~~)/;
|
|
14
|
+
/**
|
|
15
|
+
* Escape list markers that have no content, so a terse reply renders as
|
|
16
|
+
* text instead of vanishing. CommonMark parses a line that is only `42.`
|
|
17
|
+
* as an *empty* ordered list starting at 42 — a voice agent answering
|
|
18
|
+
* "what's 6 times 7?" with "42." would otherwise display nothing (and a
|
|
19
|
+
* streaming transcript briefly ending on `1.` would blank the row).
|
|
20
|
+
* Lines inside fenced code blocks are left alone.
|
|
21
|
+
*/
|
|
22
|
+
function escapeBareListMarkers(text) {
|
|
23
|
+
let inFence = false;
|
|
24
|
+
return text.split("\n").map((line) => {
|
|
25
|
+
if (CODE_FENCE.test(line)) {
|
|
26
|
+
inFence = !inFence;
|
|
27
|
+
return line;
|
|
28
|
+
}
|
|
29
|
+
if (inFence) return line;
|
|
30
|
+
const ordered = BARE_ORDERED_MARKER.exec(line);
|
|
31
|
+
if (ordered) return `${ordered[1]}${ordered[2]}\\${ordered[3]}`;
|
|
32
|
+
const bullet = BARE_BULLET_MARKER.exec(line);
|
|
33
|
+
if (bullet) return `${bullet[1]}\\${bullet[2]}`;
|
|
34
|
+
return line;
|
|
35
|
+
}).join("\n");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Agent prose, rendered as Markdown.
|
|
39
|
+
*
|
|
40
|
+
* Pipeline and S2S models write emphasis, lists, `code`, and links; before
|
|
41
|
+
* this they arrived as literal asterisks and backticks. Styling is
|
|
42
|
+
* per-element (theme colors via inline styles, spacing via Tailwind) so it
|
|
43
|
+
* stays on the default client's type scale and follows custom themes.
|
|
44
|
+
* GFM is on for tables and strikethrough. react-markdown does not render
|
|
45
|
+
* raw HTML unless rehype-raw is added — keep it that way, this text comes
|
|
46
|
+
* from a model.
|
|
47
|
+
*
|
|
48
|
+
* Memoized alongside `MessageBubble`: message content is referentially
|
|
49
|
+
* stable across snapshots, so only the streaming row re-parses.
|
|
50
|
+
*
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
const Markdown = memo(function Markdown({ text }) {
|
|
54
|
+
const theme = useTheme();
|
|
55
|
+
return /* @__PURE__ */ jsx(ReactMarkdown, {
|
|
56
|
+
remarkPlugins: [remarkGfm],
|
|
57
|
+
components: {
|
|
58
|
+
p: (props) => /* @__PURE__ */ jsx("p", {
|
|
59
|
+
className: "my-1.5 first:mt-0 last:mb-0",
|
|
60
|
+
...props
|
|
61
|
+
}),
|
|
62
|
+
ul: (props) => /* @__PURE__ */ jsx("ul", {
|
|
63
|
+
className: "my-1.5 list-disc pl-5",
|
|
64
|
+
...props
|
|
65
|
+
}),
|
|
66
|
+
ol: (props) => /* @__PURE__ */ jsx("ol", {
|
|
67
|
+
className: "my-1.5 list-decimal pl-5",
|
|
68
|
+
...props
|
|
69
|
+
}),
|
|
70
|
+
li: (props) => /* @__PURE__ */ jsx("li", {
|
|
71
|
+
className: "my-0.5",
|
|
72
|
+
...props
|
|
73
|
+
}),
|
|
74
|
+
h1: (props) => /* @__PURE__ */ jsx("h1", {
|
|
75
|
+
className: "mt-3 mb-1.5 text-[17px] font-semibold",
|
|
76
|
+
...props
|
|
77
|
+
}),
|
|
78
|
+
h2: (props) => /* @__PURE__ */ jsx("h2", {
|
|
79
|
+
className: "mt-3 mb-1.5 text-[16px] font-semibold",
|
|
80
|
+
...props
|
|
81
|
+
}),
|
|
82
|
+
h3: (props) => /* @__PURE__ */ jsx("h3", {
|
|
83
|
+
className: "mt-3 mb-1.5 text-[15px] font-semibold",
|
|
84
|
+
...props
|
|
85
|
+
}),
|
|
86
|
+
a: ({ style, ...props }) => /* @__PURE__ */ jsx("a", {
|
|
87
|
+
className: "underline underline-offset-2",
|
|
88
|
+
style: {
|
|
89
|
+
color: theme.primary,
|
|
90
|
+
...style
|
|
91
|
+
},
|
|
92
|
+
target: "_blank",
|
|
93
|
+
rel: "noreferrer noopener",
|
|
94
|
+
...props
|
|
95
|
+
}),
|
|
96
|
+
code: ({ className, style, children, ...rest }) => {
|
|
97
|
+
return /language-/.test(className ?? "") ? /* @__PURE__ */ jsx("code", {
|
|
98
|
+
className: "font-aai-mono text-[12.5px]",
|
|
99
|
+
...rest,
|
|
100
|
+
children
|
|
101
|
+
}) : /* @__PURE__ */ jsx("code", {
|
|
102
|
+
className: "rounded-sm border px-1 py-0.5 font-aai-mono text-[12.5px]",
|
|
103
|
+
style: {
|
|
104
|
+
borderColor: theme.border,
|
|
105
|
+
background: SURFACE_TINT,
|
|
106
|
+
...style
|
|
107
|
+
},
|
|
108
|
+
...rest,
|
|
109
|
+
children
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
pre: ({ style, ...props }) => /* @__PURE__ */ jsx("pre", {
|
|
113
|
+
className: "my-1.5 overflow-x-auto rounded-md border p-2.5 whitespace-pre-wrap wrap-break-word",
|
|
114
|
+
style: {
|
|
115
|
+
borderColor: theme.border,
|
|
116
|
+
background: SURFACE_TINT,
|
|
117
|
+
...style
|
|
118
|
+
},
|
|
119
|
+
...props
|
|
120
|
+
}),
|
|
121
|
+
blockquote: ({ style, ...props }) => /* @__PURE__ */ jsx("blockquote", {
|
|
122
|
+
className: "my-1.5 border-l-2 pl-3",
|
|
123
|
+
style: {
|
|
124
|
+
borderColor: theme.border,
|
|
125
|
+
color: TEXT_MUTED,
|
|
126
|
+
...style
|
|
127
|
+
},
|
|
128
|
+
...props
|
|
129
|
+
}),
|
|
130
|
+
table: (props) => /* @__PURE__ */ jsx("table", {
|
|
131
|
+
className: "my-1.5 w-full border-collapse text-sm",
|
|
132
|
+
...props
|
|
133
|
+
}),
|
|
134
|
+
th: ({ style, ...props }) => /* @__PURE__ */ jsx("th", {
|
|
135
|
+
className: "border px-2 py-1 text-left font-medium",
|
|
136
|
+
style: {
|
|
137
|
+
borderColor: theme.border,
|
|
138
|
+
...style
|
|
139
|
+
},
|
|
140
|
+
...props
|
|
141
|
+
}),
|
|
142
|
+
td: ({ style, ...props }) => /* @__PURE__ */ jsx("td", {
|
|
143
|
+
className: "border px-2 py-1",
|
|
144
|
+
style: {
|
|
145
|
+
borderColor: theme.border,
|
|
146
|
+
...style
|
|
147
|
+
},
|
|
148
|
+
...props
|
|
149
|
+
}),
|
|
150
|
+
hr: ({ style, ...props }) => /* @__PURE__ */ jsx("hr", {
|
|
151
|
+
className: "my-2.5 border-t",
|
|
152
|
+
style: {
|
|
153
|
+
borderColor: theme.border,
|
|
154
|
+
...style
|
|
155
|
+
},
|
|
156
|
+
...props
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
children: escapeBareListMarkers(text)
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region components/message-list.tsx
|
|
164
|
+
/** @jsxImportSource react */
|
|
165
|
+
const DOT_STYLES = [
|
|
166
|
+
0,
|
|
167
|
+
.16,
|
|
168
|
+
.32
|
|
169
|
+
].map((delay) => ({
|
|
170
|
+
animation: "aai-bounce 1.4s infinite ease-in-out both",
|
|
171
|
+
animationDelay: `${delay}s`
|
|
172
|
+
}));
|
|
173
|
+
/** Animated three-dot "thinking" indicator. @internal */
|
|
174
|
+
function ThinkingDots() {
|
|
175
|
+
return /* @__PURE__ */ jsx("div", {
|
|
176
|
+
className: "flex items-center gap-2 text-sm font-medium min-h-5",
|
|
177
|
+
style: { color: TEXT_MUTED },
|
|
178
|
+
children: DOT_STYLES.map((style, i) => /* @__PURE__ */ jsx("div", {
|
|
179
|
+
className: "w-1.5 h-1.5 rounded-full",
|
|
180
|
+
style: {
|
|
181
|
+
...style,
|
|
182
|
+
background: TEXT_MUTED
|
|
183
|
+
}
|
|
184
|
+
}, i))
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Right-aligned user bubble — a primary-tinted card (the design system's
|
|
189
|
+
* indigo-50 fill with an indigo-100 edge, derived from the theme primary so
|
|
190
|
+
* custom themes stay coherent). Shared by finalized messages and the live
|
|
191
|
+
* transcript.
|
|
192
|
+
*/
|
|
193
|
+
function UserBubble({ theme, color, children }) {
|
|
194
|
+
return /* @__PURE__ */ jsx("div", {
|
|
195
|
+
className: "flex flex-col w-full items-end",
|
|
196
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
197
|
+
className: "max-w-[min(78%,64ch)] border px-3.5 py-2.5 rounded-md whitespace-pre-wrap wrap-break-word text-[15px] font-normal leading-[22px]",
|
|
198
|
+
style: {
|
|
199
|
+
background: primaryTint(theme.primary, theme.surface, 7),
|
|
200
|
+
borderColor: primaryTint(theme.primary, theme.surface, 16),
|
|
201
|
+
color
|
|
202
|
+
},
|
|
203
|
+
children
|
|
204
|
+
})
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Renders a single chat message: labeled agent prose, or a user bubble.
|
|
209
|
+
*
|
|
210
|
+
* Memoized so appending one message re-renders one row, not the whole capped
|
|
211
|
+
* list: message objects and the theme are referentially stable across
|
|
212
|
+
* snapshots, and rows are keyed on stable ids (`ChatMessage.id`) that survive
|
|
213
|
+
* the sliding 200-message window.
|
|
214
|
+
*/
|
|
215
|
+
const MessageBubble = memo(function MessageBubble({ message, theme }) {
|
|
216
|
+
if (message.role === "user") return /* @__PURE__ */ jsx(UserBubble, {
|
|
217
|
+
theme,
|
|
218
|
+
color: theme.text,
|
|
219
|
+
children: message.content
|
|
220
|
+
});
|
|
221
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
222
|
+
className: "flex flex-col gap-1 max-w-[82%]",
|
|
223
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
224
|
+
className: "text-[10px] font-medium tracking-[1.2px] uppercase leading-none",
|
|
225
|
+
style: { color: TEXT_FAINT },
|
|
226
|
+
children: "Agent"
|
|
227
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
228
|
+
className: "wrap-break-word text-[15px] font-normal leading-[23px]",
|
|
229
|
+
style: { color: theme.text },
|
|
230
|
+
children: /* @__PURE__ */ jsx(Markdown, { text: message.content })
|
|
231
|
+
})]
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
/**
|
|
235
|
+
* How close to the bottom (px) the container must be for auto-scroll to stay
|
|
236
|
+
* engaged. Generous enough that an in-flight smooth scroll doesn't unpin.
|
|
237
|
+
*/
|
|
238
|
+
const NEAR_BOTTOM_PX = 96;
|
|
239
|
+
/**
|
|
240
|
+
* Smooth-scroll to the anchor whenever the content version advances — but
|
|
241
|
+
* only while the container is pinned near the bottom. A user who scrolled up
|
|
242
|
+
* to read history isn't yanked back down by streaming updates; scrolling back
|
|
243
|
+
* to the bottom re-engages the auto-scroll.
|
|
244
|
+
*
|
|
245
|
+
* The scroll runs inside `requestAnimationFrame` and is deduped per frame:
|
|
246
|
+
* several snapshot updates in one frame (transcript + message + tool call)
|
|
247
|
+
* trigger a single scroll after layout instead of one forced layout each.
|
|
248
|
+
*
|
|
249
|
+
* While a partial transcript is streaming, updates arrive faster than a
|
|
250
|
+
* smooth scroll finishes — each restart leaves the animation perpetually
|
|
251
|
+
* mid-flight — so `streaming` switches to an instant jump; committed-content
|
|
252
|
+
* updates keep the smooth animation.
|
|
253
|
+
*/
|
|
254
|
+
function useAutoScroll(contentVersion, streaming) {
|
|
255
|
+
const anchorRef = useRef(null);
|
|
256
|
+
const pinnedRef = useRef(true);
|
|
257
|
+
const scheduledRef = useRef(false);
|
|
258
|
+
const streamingRef = useRef(streaming);
|
|
259
|
+
streamingRef.current = streaming;
|
|
260
|
+
const onScroll = useCallback((event) => {
|
|
261
|
+
const el = event.currentTarget;
|
|
262
|
+
pinnedRef.current = el.scrollTop + el.clientHeight >= el.scrollHeight - NEAR_BOTTOM_PX;
|
|
263
|
+
}, []);
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (contentVersion === 0 || scheduledRef.current || !pinnedRef.current) return;
|
|
266
|
+
scheduledRef.current = true;
|
|
267
|
+
requestAnimationFrame(() => {
|
|
268
|
+
scheduledRef.current = false;
|
|
269
|
+
if (!pinnedRef.current) return;
|
|
270
|
+
anchorRef.current?.scrollIntoView({
|
|
271
|
+
behavior: streamingRef.current ? "instant" : "smooth",
|
|
272
|
+
block: "end"
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
}, [contentVersion]);
|
|
276
|
+
return {
|
|
277
|
+
anchorRef,
|
|
278
|
+
onScroll
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Interleave messages and tool calls into render items, ordered by insertion
|
|
283
|
+
* time. Each tool call renders immediately after its anchor message
|
|
284
|
+
* (`afterMessageId`); tool calls whose anchor slid out of the retained window
|
|
285
|
+
* (or that were inserted before any message existed) render first.
|
|
286
|
+
*/
|
|
287
|
+
function interleave(messages, toolCalls, renderMessage, renderToolCall) {
|
|
288
|
+
const items = [];
|
|
289
|
+
let tci = 0;
|
|
290
|
+
const pushToolCallsThrough = (maxAfterId) => {
|
|
291
|
+
let tc = toolCalls[tci];
|
|
292
|
+
while (tc && tc.afterMessageId <= maxAfterId) {
|
|
293
|
+
items.push(renderToolCall(tc));
|
|
294
|
+
tci++;
|
|
295
|
+
tc = toolCalls[tci];
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const firstMessage = messages[0];
|
|
299
|
+
if (firstMessage) pushToolCallsThrough(firstMessage.id - 1);
|
|
300
|
+
for (const msg of messages) {
|
|
301
|
+
items.push(renderMessage(msg));
|
|
302
|
+
pushToolCallsThrough(msg.id);
|
|
303
|
+
}
|
|
304
|
+
pushToolCallsThrough(Number.POSITIVE_INFINITY);
|
|
305
|
+
return items;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Scrollable list of all chat messages, tool-call blocks, live transcript,
|
|
309
|
+
* streaming agent utterance, and a thinking indicator.
|
|
310
|
+
*
|
|
311
|
+
* Messages and tool calls are interleaved in the correct order. The list
|
|
312
|
+
* auto-scrolls to the latest content.
|
|
313
|
+
*
|
|
314
|
+
* Must be rendered inside a {@link SessionProvider}.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```tsx
|
|
318
|
+
* <MessageList className="flex-1" />
|
|
319
|
+
* ```
|
|
320
|
+
*
|
|
321
|
+
* @param className - Additional CSS class names applied to the scroll container.
|
|
322
|
+
*
|
|
323
|
+
* @public
|
|
324
|
+
*/
|
|
325
|
+
const MessageList = memo(function MessageList({ className }) {
|
|
326
|
+
const state = useSessionSelector((s) => s.state);
|
|
327
|
+
const messages = useSessionSelector((s) => s.messages);
|
|
328
|
+
const toolCalls = useSessionSelector((s) => s.toolCalls);
|
|
329
|
+
const userTranscript = useSessionSelector((s) => s.userTranscript);
|
|
330
|
+
const agentTranscript = useSessionSelector((s) => s.agentTranscript);
|
|
331
|
+
const contentVersion = useSessionSelector((s) => s.contentVersion);
|
|
332
|
+
const theme = useTheme();
|
|
333
|
+
const showThinking = useMemo(() => {
|
|
334
|
+
if (state !== "thinking") return false;
|
|
335
|
+
const last = toolCalls.at(-1);
|
|
336
|
+
if (last?.status === "pending") return false;
|
|
337
|
+
const lastMsg = messages.at(-1);
|
|
338
|
+
return !lastMsg || lastMsg.role === "user" || Boolean(last);
|
|
339
|
+
}, [
|
|
340
|
+
state,
|
|
341
|
+
toolCalls,
|
|
342
|
+
messages
|
|
343
|
+
]);
|
|
344
|
+
const { anchorRef, onScroll } = useAutoScroll(contentVersion, userTranscript !== null);
|
|
345
|
+
const items = useMemo(() => interleave(messages, toolCalls, (msg) => /* @__PURE__ */ jsx(MessageBubble, {
|
|
346
|
+
message: msg,
|
|
347
|
+
theme
|
|
348
|
+
}, msg.id), (tc) => /* @__PURE__ */ jsx(ToolCallBlock, { toolCall: tc }, tc.callId)), [
|
|
349
|
+
messages,
|
|
350
|
+
toolCalls,
|
|
351
|
+
theme
|
|
352
|
+
]);
|
|
353
|
+
return /* @__PURE__ */ jsx("div", {
|
|
354
|
+
role: "log",
|
|
355
|
+
className: clsx("flex-1 overflow-y-auto [scrollbar-width:none]", className),
|
|
356
|
+
style: { background: theme.surface },
|
|
357
|
+
onScroll,
|
|
358
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
359
|
+
className: "flex flex-col gap-4 p-7",
|
|
360
|
+
children: [
|
|
361
|
+
items,
|
|
362
|
+
agentTranscript && /* @__PURE__ */ jsx(MessageBubble, {
|
|
363
|
+
message: {
|
|
364
|
+
role: "assistant",
|
|
365
|
+
content: agentTranscript
|
|
366
|
+
},
|
|
367
|
+
theme
|
|
368
|
+
}),
|
|
369
|
+
userTranscript !== null && /* @__PURE__ */ jsx(UserBubble, {
|
|
370
|
+
theme,
|
|
371
|
+
color: "#6F6A60",
|
|
372
|
+
children: userTranscript ? userTranscript : /* @__PURE__ */ jsx(ThinkingDots, {})
|
|
373
|
+
}),
|
|
374
|
+
showThinking && /* @__PURE__ */ jsx(ThinkingDots, {}),
|
|
375
|
+
/* @__PURE__ */ jsx("div", { ref: anchorRef })
|
|
376
|
+
]
|
|
377
|
+
})
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
//#endregion
|
|
381
|
+
export { Markdown as n, MessageList as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MIC_SEND_MAX_BUFFERED_BYTES } from "./types.js";
|
|
2
|
-
import { DEFAULT_MAX_HISTORY, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
|
|
3
2
|
import { ServerMessageSchema, lenientParse } from "@alexkroman1/aai/protocol";
|
|
3
|
+
import { DEFAULT_MAX_HISTORY, WS_OPEN, errorMessage, safeJsonParse } from "@alexkroman1/aai";
|
|
4
4
|
import ReconnectingWebSocket from "partysocket/ws";
|
|
5
5
|
//#region session-core-audio-setup.ts
|
|
6
6
|
/**
|
|
@@ -23,20 +23,17 @@ import ReconnectingWebSocket from "partysocket/ws";
|
|
|
23
23
|
* VoiceIO is closed immediately to prevent it from being assigned to a newer
|
|
24
24
|
* connection.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* the session; a text-only session's opt-in record button must not brick an
|
|
29
|
-
* otherwise healthy session, so failure there is a banner (`error` set,
|
|
30
|
-
* state kept) and the session stays interactive.
|
|
26
|
+
* A failure is fatal: a voice session can't function without the mic, so it
|
|
27
|
+
* sets the error state and ends the session.
|
|
31
28
|
*/
|
|
32
|
-
async function initAudioCapture(conn, msg, deps
|
|
29
|
+
async function initAudioCapture(conn, msg, deps) {
|
|
33
30
|
if (conn.audioSetupInFlight) return;
|
|
34
31
|
conn.audioSetupInFlight = true;
|
|
35
32
|
const gen = conn.generation;
|
|
36
33
|
const stale = () => conn.generation !== gen || !conn.ws || conn.ws.readyState !== WS_OPEN;
|
|
37
34
|
const reportAudioFailure = (message) => {
|
|
38
35
|
deps.cleanupAudio();
|
|
39
|
-
|
|
36
|
+
deps.updateState({
|
|
40
37
|
state: "error",
|
|
41
38
|
error: {
|
|
42
39
|
code: "audio",
|
|
@@ -45,13 +42,6 @@ async function initAudioCapture(conn, msg, deps, fatal) {
|
|
|
45
42
|
running: false,
|
|
46
43
|
recording: false
|
|
47
44
|
});
|
|
48
|
-
else deps.updateState({
|
|
49
|
-
error: {
|
|
50
|
-
code: "audio",
|
|
51
|
-
message
|
|
52
|
-
},
|
|
53
|
-
recording: false
|
|
54
|
-
});
|
|
55
45
|
};
|
|
56
46
|
try {
|
|
57
47
|
const [{ createVoiceIO }, captureWorklet, playbackWorklet] = await Promise.all([
|
|
@@ -312,7 +302,6 @@ function createMessageHandlers(deps) {
|
|
|
312
302
|
case "error":
|
|
313
303
|
handleErrorEvent(e);
|
|
314
304
|
break;
|
|
315
|
-
case "idle_timeout": break;
|
|
316
305
|
default: break;
|
|
317
306
|
}
|
|
318
307
|
}
|
|
@@ -504,8 +493,7 @@ function createSessionCore(options) {
|
|
|
504
493
|
audioSetupInFlight: false,
|
|
505
494
|
generation: 0,
|
|
506
495
|
preInitAudio: [],
|
|
507
|
-
preInitDone: false
|
|
508
|
-
readyConfig: null
|
|
496
|
+
preInitDone: false
|
|
509
497
|
};
|
|
510
498
|
let connectionController = null;
|
|
511
499
|
let hasConnected = false;
|
|
@@ -566,11 +554,7 @@ function createSessionCore(options) {
|
|
|
566
554
|
}
|
|
567
555
|
const isReconnect = hasConnected;
|
|
568
556
|
hasConnected = true;
|
|
569
|
-
conn
|
|
570
|
-
sampleRate: config.sampleRate,
|
|
571
|
-
ttsSampleRate: config.ttsSampleRate
|
|
572
|
-
};
|
|
573
|
-
initAudioCapture(conn, config, audioDeps, true);
|
|
557
|
+
initAudioCapture(conn, config, audioDeps);
|
|
574
558
|
if (isReconnect && currentSnapshot.messages.length > 0) sendJson({
|
|
575
559
|
type: "history",
|
|
576
560
|
messages: currentSnapshot.messages.map((m) => ({
|
|
@@ -24,13 +24,10 @@ export type AudioSetupDeps = {
|
|
|
24
24
|
* VoiceIO is closed immediately to prevent it from being assigned to a newer
|
|
25
25
|
* connection.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* the session; a text-only session's opt-in record button must not brick an
|
|
30
|
-
* otherwise healthy session, so failure there is a banner (`error` set,
|
|
31
|
-
* state kept) and the session stays interactive.
|
|
27
|
+
* A failure is fatal: a voice session can't function without the mic, so it
|
|
28
|
+
* sets the error state and ends the session.
|
|
32
29
|
*/
|
|
33
30
|
export declare function initAudioCapture(conn: ConnState, msg: {
|
|
34
31
|
sampleRate: number;
|
|
35
32
|
ttsSampleRate: number;
|
|
36
|
-
}, deps: AudioSetupDeps
|
|
33
|
+
}, deps: AudioSetupDeps): Promise<void>;
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* Type declarations for the framework-agnostic voice session core.
|
|
3
3
|
*
|
|
4
4
|
* Split out of `session-core.ts` to keep that module focused on behaviour.
|
|
5
|
-
* The public types here are re-exported from `session-core.ts` for
|
|
6
|
-
* backwards compatibility.
|
|
7
5
|
*/
|
|
8
6
|
import type { VoiceIO } from "./audio.ts";
|
|
9
7
|
import type { AgentState, ChatMessage, SessionError, ToolCallInfo, VoiceSessionOptions, WebSocketConstructor } from "./types.ts";
|
|
@@ -111,11 +109,4 @@ export type ConnState = {
|
|
|
111
109
|
* signal must be replayed after draining preInitAudio, or a short greeting
|
|
112
110
|
* buffered during mic-permission never finishes playing. */
|
|
113
111
|
preInitDone: boolean;
|
|
114
|
-
/** The server's `config` payload for the current connection — kept so
|
|
115
|
-
* text-only sessions can init the mic lazily (record button) and file
|
|
116
|
-
* uploads know the STT sample rate to resample to. */
|
|
117
|
-
readyConfig: {
|
|
118
|
-
sampleRate: number;
|
|
119
|
-
ttsSampleRate: number;
|
|
120
|
-
} | null;
|
|
121
112
|
};
|
package/dist/session-core.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { SessionCore, SessionCoreOptions } from "./session-core-types.ts";
|
|
2
|
-
export type { CustomEvent, SessionCore, SessionCoreOptions, SessionSnapshot, } from "./session-core-types.ts";
|
|
3
2
|
/**
|
|
4
3
|
* Create a framework-agnostic voice session core that connects to an AAI
|
|
5
4
|
* server via WebSocket.
|
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-CkZ8LyEA.js";
|
|
2
2
|
export { createSessionCore };
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionErrorCode } from "@alexkroman1/aai/protocol";
|
|
2
|
-
export { CAPTURE_STOP_ACK_TIMEOUT_MS,
|
|
2
|
+
export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, } from "@alexkroman1/aai";
|
|
3
3
|
/**
|
|
4
4
|
* `getUserMedia` audio constraints for every capture path in this package.
|
|
5
5
|
*
|
package/dist/types.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CAPTURE_STOP_ACK_TIMEOUT_MS,
|
|
1
|
+
import { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS } from "@alexkroman1/aai";
|
|
2
2
|
//#region types.ts
|
|
3
3
|
/**
|
|
4
4
|
* `getUserMedia` audio constraints for every capture path in this package.
|
|
@@ -29,4 +29,4 @@ const VOICE_CAPTURE_CONSTRAINTS = {
|
|
|
29
29
|
voiceIsolation: false
|
|
30
30
|
};
|
|
31
31
|
//#endregion
|
|
32
|
-
export { CAPTURE_STOP_ACK_TIMEOUT_MS,
|
|
32
|
+
export { CAPTURE_STOP_ACK_TIMEOUT_MS, MIC_BUFFER_SECONDS, MIC_SEND_MAX_BUFFERED_BYTES, MIC_SILENCE_PROBE_MS, PLAYBACK_BUFFER_SECONDS, PLAYBACK_CONCEAL_FADE_MS, PLAYBACK_CONCEAL_FLOOR, PLAYBACK_DONE_MAX_WAIT_MS, PLAYBACK_DONE_POLL_MS, PLAYBACK_JITTER_MS, PLAYBACK_REFILL_MS, VOICE_CAPTURE_CONSTRAINTS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"clsx": "^2.1.1",
|
|
21
21
|
"partysocket": "^1.3.0",
|
|
22
|
+
"react-markdown": "^10.1.0",
|
|
23
|
+
"remark-gfm": "^4.0.1",
|
|
22
24
|
"use-sync-external-store": "^1.6.0",
|
|
23
|
-
"@alexkroman1/aai": "
|
|
25
|
+
"@alexkroman1/aai": "4.0.0"
|
|
24
26
|
},
|
|
25
27
|
"peerDependencies": {
|
|
26
28
|
"react": "^19.0.0",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as e,o as t,t as n}from"./index-BeVXjGwG.js";function r(e){let t=new Int16Array(e.length),n=0;for(let r of e){let e=Math.max(-1,Math.min(1,r));t[n++]=e<0?e*32768:e*32767}return t}function i(e,t,n){if(e!==t)throw Error(`Browser refused the ${n} sample rate: asked for ${t} Hz, got ${e} Hz`)}function a(e){e.then(e=>{for(let t of e.getTracks())t.stop()}).catch(()=>{})}function o(e,t,n){let r=new AudioWorkletNode(e,`capture-processor`,{channelCount:1,channelCountMode:`explicit`}),i=null;return r.port.onmessage=e=>{let r=e.data;r.event===`chunk`&&r.buffer?t(r.buffer):r.event===`silent`?n?.():r.event===`stopped`&&(i?.(),i=null)},{node:r,start(){r.port.postMessage({event:`start`})},stop(){return new Promise(e=>{let t=setTimeout(e,250);i=()=>{clearTimeout(t),e()},r.port.postMessage({event:`stop`})})}}}async function s(r){let{sttSampleRate:s,ttsSampleRate:c,captureWorkletSrc:l,playbackWorkletSrc:u,onMicData:d,onError:f,onPlaybackStats:p,onMicSilent:m}=r,h=new AudioContext({sampleRate:c,latencyHint:`playback`}),g=s===c,_=g?h:new AudioContext({sampleRate:s,latencyHint:`interactive`});async function v(){let e=g?[h]:[h,_];await Promise.all(e.map(e=>e.close().catch(e=>{console.warn(`AudioContext close failed:`,e)})))}let y=navigator.mediaDevices.getUserMedia({audio:{deviceId:{ideal:`default`},...n}}),b;try{[b]=await Promise.all([y,h.resume(),_.resume(),_.audioWorklet.addModule(l),h.audioWorklet.addModule(u)]),i(_.sampleRate,s,`capture`),i(h.sampleRate,c,`playback`)}catch(e){throw a(y),await v(),e}let x=_.createMediaStreamSource(b),S=o(_,d,m);x.connect(S.node),S.node.onprocessorerror=()=>{let e=Error(`Audio capture worklet crashed`);console.error(`[aai-ui]`,e.message),f?.(e)},S.start();let C=null,w=null,T=new AbortController;function E(){if(C)return C;let e=new AudioWorkletNode(h,`playback-processor`);return e.connect(h.destination),e.port.onmessage=e=>{if(e.data.event===`stop`){let t=e.data.stats;if(t&&t.concealedSamples>0&&p?.(t),e.data.reason===`interrupt`)return;w?.(),w=null}},e.onprocessorerror=()=>{let e=Error(`Audio playback worklet crashed`);console.error(`[aai-ui]`,e.message),w?.(),w=null,f?.(e)},C=e,e}let D={enqueue(e){T.signal.aborted||e.byteLength!==0&&E().port.postMessage({event:`write`,buffer:new Uint8Array(e)},[e])},done(){return!C||(C.port.postMessage({event:`done`}),h.state!==`running`)?Promise.resolve():new Promise(n=>{w?.();let r=()=>{clearInterval(i),clearTimeout(a),w===r&&(w=null),n()},i=setInterval(()=>{h.state!==`running`&&r()},t),a=setTimeout(r,e);w=r})},flush(){C&&(w?.(),w=null,C.port.postMessage({event:`interrupt`}))},async close(){if(!T.signal.aborted){T.abort(),await S.stop(),x.disconnect(),S.node.disconnect(),C&&C.disconnect();for(let e of b.getTracks())e.stop();await v()}},async[Symbol.asyncDispose](){await D.close()}};return D}export{i as assertGranted,o as createCaptureNode,s as createVoiceIO,r as floatToPcm16,a as releaseStreamOnFailure};
|