@alexkroman1/aai-ui 0.12.3 → 0.14.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 +253 -0
- package/dist/_colors-CpZO-88A.js +78 -0
- package/dist/_module-url-C13kAJ87.js +15 -0
- package/dist/_page-shell.d.ts +11 -0
- package/dist/_recover-run.d.ts +79 -0
- package/dist/_repeat-until.d.ts +30 -0
- package/dist/_run-controls.d.ts +33 -0
- package/dist/_submission-state.d.ts +92 -0
- package/dist/_tap-to-talk-state.d.ts +88 -0
- package/dist/_utils-Mi-SCFA1.js +47 -0
- package/dist/_utils.d.ts +26 -0
- package/dist/_web-storage.d.ts +43 -0
- package/dist/_workflow-api-ref.d.ts +37 -0
- package/dist/_workflow-files.d.ts +34 -0
- package/dist/_workflow-test-defs.d.ts +34 -0
- package/dist/aai-logo-D8pMVn9_.js +85 -0
- package/dist/agent-state-labels.d.ts +65 -0
- package/dist/agent-state.d.ts +108 -0
- package/dist/audio/capture.d.ts +44 -0
- package/dist/audio/index.d.ts +18 -0
- package/dist/audio/index.js +431 -0
- package/dist/audio/preconnect.d.ts +87 -0
- package/dist/audio/voice-io.d.ts +112 -0
- package/dist/build-default-client.d.ts +1 -0
- package/dist/chat-view-BCbI7gD4.js +396 -0
- package/dist/client-config-C7fQrioM.js +139 -0
- package/dist/client-config.d.ts +119 -0
- package/dist/client-dir.d.ts +44 -0
- package/dist/client-dir.js +58 -0
- package/dist/client-identity.d.ts +131 -0
- package/dist/client-tool.d.ts +51 -0
- package/dist/components/_colors.d.ts +73 -0
- package/dist/components/_form-readiness.d.ts +56 -0
- package/dist/components/_form-values.d.ts +19 -0
- package/dist/components/aai-logo.d.ts +12 -0
- package/dist/components/audio-result.d.ts +94 -0
- package/dist/components/auto-scroll.d.ts +74 -0
- package/dist/components/bullet-list.d.ts +74 -0
- package/dist/components/button.d.ts +29 -21
- package/dist/components/button.js +62 -23
- package/dist/components/chat-view.d.ts +24 -18
- package/dist/components/chat-view.js +2 -60
- package/dist/components/console-shell.d.ts +85 -0
- package/dist/components/controls.d.ts +21 -6
- package/dist/components/controls.js +31 -19
- package/dist/components/conversation-view.d.ts +130 -0
- package/dist/components/eyebrow.d.ts +12 -0
- package/dist/components/facts.d.ts +81 -0
- package/dist/components/form-fields.d.ts +142 -0
- package/dist/components/form-types.d.ts +73 -0
- package/dist/components/form.d.ts +96 -0
- package/dist/components/markdown.d.ts +57 -0
- package/dist/components/message-list.d.ts +34 -7
- package/dist/components/message-list.js +2 -75
- package/dist/components/session-controls.d.ts +125 -0
- package/dist/components/session-error-banner.d.ts +69 -0
- package/dist/components/session-state-dot.d.ts +111 -0
- package/dist/components/sidebar-layout.d.ts +31 -10
- package/dist/components/sidebar-layout.js +32 -12
- package/dist/components/start-screen.d.ts +17 -6
- package/dist/components/start-screen.js +34 -14
- package/dist/components/tool-call-block.d.ts +15 -14
- package/dist/components/tool-call-block.js +2 -117
- package/dist/components/tool-call-row.d.ts +75 -0
- package/dist/components/tool-config-context.d.ts +22 -0
- package/dist/components/upload-progress.d.ts +76 -0
- package/dist/components/url-chips.d.ts +29 -0
- package/dist/components/workflow-fields.d.ts +46 -0
- package/dist/components/workflow-pending-note.d.ts +81 -0
- package/dist/components/workflow-progress.d.ts +81 -0
- package/dist/components/workflow-run-error.d.ts +48 -0
- package/dist/components/workflow-run-panel.d.ts +79 -0
- package/dist/context.d.ts +288 -0
- package/dist/context.js +382 -0
- package/dist/conversation-log.d.ts +88 -0
- package/dist/default-client/assets/_module-url-BX0RuRU2.js +1 -0
- package/dist/default-client/assets/audio-mpgipo2Q.js +1 -0
- package/dist/default-client/assets/capture-processor-D0q7Qrdd.js +90 -0
- package/dist/default-client/assets/index-8ihb1Urm.css +2 -0
- package/dist/default-client/assets/index-DPfmSFmc.js +126 -0
- package/dist/default-client/assets/playback-processor-CjDbc9iH.js +314 -0
- package/dist/default-client/favicon.ico +0 -0
- package/dist/default-client/index.html +15 -0
- package/dist/default-client-entry.d.ts +6 -0
- package/dist/default-client.d.ts +19 -0
- package/dist/define-client.d.ts +169 -26
- package/dist/define-client.js +209 -55
- package/dist/eyebrow-UfmSz9yy.js +27 -0
- package/dist/hooks-DN4gHYsj.js +182 -0
- package/dist/hooks.d.ts +172 -0
- package/dist/hooks.js +3 -0
- package/dist/hooks.test-d.d.ts +14 -0
- package/dist/inbox-protocol.d.ts +128 -0
- package/dist/inbox.d.ts +132 -0
- package/dist/index.d.ts +126 -29
- package/dist/index.js +5500 -15
- package/dist/internal.d.ts +43 -0
- package/dist/internal.js +8 -0
- package/dist/linked-client.d.ts +70 -0
- package/dist/message-list-DHZcLhRA.js +682 -0
- package/dist/notice-player.d.ts +70 -0
- package/dist/page.d.ts +114 -0
- package/dist/phone.d.ts +50 -0
- package/dist/route-fetch.d.ts +79 -0
- package/dist/session/audio-effects.d.ts +37 -0
- package/dist/session/audio-setup.d.ts +57 -0
- package/dist/session/audio-state.d.ts +157 -0
- package/dist/session/browser-session.d.ts +39 -0
- package/dist/session/close.d.ts +26 -0
- package/dist/session/dial.d.ts +65 -0
- package/dist/session/handshake.d.ts +60 -0
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +2 -0
- package/dist/session/messages.d.ts +63 -0
- package/dist/session/mic.d.ts +64 -0
- package/dist/session/preconnect.d.ts +48 -0
- package/dist/session/reconnect.d.ts +25 -0
- package/dist/session/resume-store.d.ts +57 -0
- package/dist/session/state.d.ts +146 -0
- package/dist/session/ticket.d.ts +44 -0
- package/dist/session/types.d.ts +476 -0
- package/dist/session/url.d.ts +17 -0
- package/dist/session/user-turn.d.ts +55 -0
- package/dist/session-D9jM5-TF.js +2140 -0
- package/dist/stored-value.d.ts +103 -0
- package/dist/tool-call-block-uXtMOUWN.js +204 -0
- package/dist/tool-config-context-Es4YUzV2.js +19 -0
- package/dist/types.d.ts +272 -34
- package/dist/types.js +35 -4
- package/dist/upload/files.d.ts +83 -0
- package/dist/upload/index.d.ts +15 -0
- package/dist/upload/pause.d.ts +31 -0
- package/dist/upload/recall.d.ts +67 -0
- package/dist/upload/report.d.ts +26 -0
- package/dist/upload/session.d.ts +99 -0
- package/dist/url-chips-oyy_Eiqh.js +239 -0
- package/dist/use-client-runs.d.ts +87 -0
- package/dist/use-conversation-log.d.ts +86 -0
- package/dist/use-conversation.d.ts +131 -0
- package/dist/use-copy.d.ts +74 -0
- package/dist/use-download-url.d.ts +84 -0
- package/dist/use-flash.d.ts +61 -0
- package/dist/use-inbox.d.ts +101 -0
- package/dist/use-push-to-talk.d.ts +105 -0
- package/dist/use-route-mutation.d.ts +113 -0
- package/dist/use-route.d.ts +82 -0
- package/dist/use-run-key.d.ts +133 -0
- package/dist/use-session-controls.d.ts +69 -0
- package/dist/use-session-id.d.ts +57 -0
- package/dist/use-tap-to-talk.d.ts +138 -0
- package/dist/use-user-transcript-uyHhzy4d.js +73 -0
- package/dist/use-user-transcript.d.ts +75 -0
- package/dist/use-workflow-form.d.ts +285 -0
- package/dist/use-workflow-progress.d.ts +137 -0
- package/dist/use-workflow-run-DdhWmbx-.js +697 -0
- package/dist/use-workflow-run.d.ts +100 -0
- package/dist/use-workflow-runs.d.ts +71 -0
- package/dist/use-workflow-stream.d.ts +154 -0
- package/dist/use-workflows.d.ts +77 -0
- package/dist/workflow-client.d.ts +136 -0
- package/dist/workflow-def-types.d.ts +31 -0
- package/dist/workflow-events.d.ts +39 -0
- package/dist/workflow-field-kind.d.ts +95 -0
- package/dist/workflow-status-labels.d.ts +35 -0
- package/dist/worklets/_module-url.d.ts +10 -0
- package/dist/worklets/_playback-bench-harness.d.ts +181 -0
- package/dist/worklets/_playback-bench-host.d.ts +63 -0
- package/dist/worklets/_playback-bench-page.d.ts +65 -0
- package/dist/worklets/_tts-trace-harness.d.ts +142 -0
- package/dist/worklets/_worklet-test-utils.d.ts +27 -0
- package/dist/worklets/capture-processor.d.ts +4 -2
- package/dist/worklets/capture-processor.js +73 -33
- package/dist/worklets/playback-processor.d.ts +4 -2
- package/dist/worklets/playback-processor.js +258 -42
- package/package.json +59 -32
- package/styles.css +152 -30
- package/dist/_jsdom-setup.d.ts +0 -0
- package/dist/audio.d.ts +0 -45
- package/dist/audio.js +0 -121
- package/dist/client-context.d.ts +0 -33
- package/dist/client-context.js +0 -15
- package/dist/client-handler.d.ts +0 -43
- package/dist/components/app.d.ts +0 -23
- package/dist/components/app.js +0 -41
- package/dist/components/error-banner.d.ts +0 -21
- package/dist/components/error-banner.js +0 -27
- package/dist/components/message-bubble.d.ts +0 -23
- package/dist/components/message-bubble.js +0 -35
- package/dist/components/state-indicator.d.ts +0 -29
- package/dist/components/state-indicator.js +0 -37
- package/dist/components/thinking-indicator.d.ts +0 -16
- package/dist/components/thinking-indicator.js +0 -34
- package/dist/components/tool-icons.d.ts +0 -17
- package/dist/components/tool-icons.js +0 -128
- package/dist/components/transcript.d.ts +0 -25
- package/dist/components/transcript.js +0 -35
- package/dist/session-CWB7vmqz.js +0 -429
- package/dist/session.d.ts +0 -69
- package/dist/session.js +0 -2
- package/dist/signals.d.ts +0 -120
- package/dist/signals.js +0 -161
- package/dist/tsdown.config.d.ts +0 -2
- package/dist/types.test-d.d.ts +0 -7
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import { useSessionSelector, useTheme } from "./context.js";
|
|
2
|
+
import { a as inkTint, o as primaryTint } from "./_colors-CpZO-88A.js";
|
|
3
|
+
import { n as useUserTranscript } from "./use-user-transcript-uyHhzy4d.js";
|
|
4
|
+
import { n as ToolCallRow, t as ToolCallBlock } from "./tool-call-block-uXtMOUWN.js";
|
|
5
|
+
import { memo, useCallback, useMemo } from "react";
|
|
6
|
+
import clsx from "clsx";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { StickToBottom } from "use-stick-to-bottom";
|
|
9
|
+
import ReactMarkdown from "react-markdown";
|
|
10
|
+
import remarkGfm from "remark-gfm";
|
|
11
|
+
//#region src/components/auto-scroll.tsx
|
|
12
|
+
/** @jsxImportSource react */
|
|
13
|
+
/**
|
|
14
|
+
* A scroll container that stays pinned to the bottom as its content grows,
|
|
15
|
+
* releases when the reader scrolls up, and re-engages once they return to the
|
|
16
|
+
* bottom.
|
|
17
|
+
*
|
|
18
|
+
* For clients that render their own chat chrome instead of using
|
|
19
|
+
* {@link MessageList} — a terminal, a dispatch board, a themed transcript.
|
|
20
|
+
* `MessageList` already behaves this way; this is the same mechanism with no
|
|
21
|
+
* opinion about what goes inside it.
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* The pattern this replaces is a `useEffect` that calls
|
|
25
|
+
* `ref.current?.scrollIntoView()` on every message change. That version has
|
|
26
|
+
* three faults, and they compound: it fights the reader, since scrolling up to
|
|
27
|
+
* re-read is undone by the next transcript delta; it misses growth that is not
|
|
28
|
+
* a new message, because a streamed reply, an expanding tool block or a
|
|
29
|
+
* markdown reflow changes height without changing the dependency array; and it
|
|
30
|
+
* needs a synthetic dependency (`messages.length + transcript.length`) to fire
|
|
31
|
+
* at all, which is where the dead `if (version < 0) return;` line comes from.
|
|
32
|
+
* A `ResizeObserver` on the content — what this uses — has none of those.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* import { AutoScroll, useSession } from "@alexkroman1/aai-ui";
|
|
37
|
+
*
|
|
38
|
+
* function Transcript() {
|
|
39
|
+
* const session = useSession();
|
|
40
|
+
* return (
|
|
41
|
+
* <AutoScroll className="flex-1 min-h-0" contentClassName="flex flex-col gap-2 p-4">
|
|
42
|
+
* {session.messages.map((m) => (
|
|
43
|
+
* <div key={m.id}>{m.content}</div>
|
|
44
|
+
* ))}
|
|
45
|
+
* </AutoScroll>
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* @param props - Scroll container props.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
function AutoScroll({ children, className, contentClassName, scrollClassName = "overflow-y-auto [scrollbar-width:none]", style, initial = "instant", resize = "smooth" }) {
|
|
55
|
+
return /* @__PURE__ */ jsx(StickToBottom, {
|
|
56
|
+
role: "log",
|
|
57
|
+
className: clsx("flex-1 min-h-0", className),
|
|
58
|
+
style,
|
|
59
|
+
initial,
|
|
60
|
+
resize,
|
|
61
|
+
children: /* @__PURE__ */ jsx(StickToBottom.Content, {
|
|
62
|
+
scrollClassName,
|
|
63
|
+
className: contentClassName,
|
|
64
|
+
children
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/use-conversation.ts
|
|
70
|
+
/**
|
|
71
|
+
* `useConversation` — the exchange, already assembled, with nothing rendered.
|
|
72
|
+
*
|
|
73
|
+
* `<MessageList>` owns four decisions that are not obvious and are not
|
|
74
|
+
* derivable from the snapshot by eye: the message/tool-call INTERLEAVE, the
|
|
75
|
+
* streaming agent utterance, the live user transcript with its `null`-vs-`""`
|
|
76
|
+
* protocol distinction, and the thinking indicator's suppression rule. Its only
|
|
77
|
+
* prop is `className`, so the moment a client wants its own bubble markup it
|
|
78
|
+
* drops all four at once — and the three hand-rolled chromes in the template
|
|
79
|
+
* tree each shipped a strictly worse conversation for exactly that reason. One
|
|
80
|
+
* of them runs fifteen tools and its operator sees none of them, because the
|
|
81
|
+
* tool calls live in a second array nothing in that page ever reads.
|
|
82
|
+
*
|
|
83
|
+
* This is the headless half. `<MessageList>` is now a thin consumer of it —
|
|
84
|
+
* which is the only way to know the hook is complete, since a hook that the
|
|
85
|
+
* package's own list cannot be built from is a hook a custom chrome will find a
|
|
86
|
+
* hole in.
|
|
87
|
+
*
|
|
88
|
+
* ## It subscribes NARROWLY, and that is half the value
|
|
89
|
+
*
|
|
90
|
+
* The three custom chromes also call whole-page `useSession()`, which re-renders
|
|
91
|
+
* on *every* snapshot change — so a dispatch board re-renders at STT-partial
|
|
92
|
+
* rate. `<ChatView>` deliberately avoids that with per-field
|
|
93
|
+
* {@link useSessionSelector} calls, and so does this: a component that reads the
|
|
94
|
+
* conversation gets the conversation's own update rate, not the session's.
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Interleave messages and tool calls, ordered by insertion time.
|
|
98
|
+
*
|
|
99
|
+
* Each tool call belongs immediately after its anchor message
|
|
100
|
+
* (`afterMessageId`); tool calls whose anchor slid out of the retained window —
|
|
101
|
+
* or that were inserted before any message existed — come first, since there is
|
|
102
|
+
* nothing left for them to follow.
|
|
103
|
+
*/
|
|
104
|
+
function interleave(messages, toolCalls) {
|
|
105
|
+
const items = [];
|
|
106
|
+
let tci = 0;
|
|
107
|
+
const pushToolCallsThrough = (maxAfterId) => {
|
|
108
|
+
let tc = toolCalls[tci];
|
|
109
|
+
while (tc && tc.afterMessageId <= maxAfterId) {
|
|
110
|
+
items.push({
|
|
111
|
+
kind: "tool",
|
|
112
|
+
toolCall: tc
|
|
113
|
+
});
|
|
114
|
+
tci++;
|
|
115
|
+
tc = toolCalls[tci];
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const firstMessage = messages[0];
|
|
119
|
+
if (firstMessage) pushToolCallsThrough(firstMessage.id - 1);
|
|
120
|
+
for (const message of messages) {
|
|
121
|
+
items.push({
|
|
122
|
+
kind: "message",
|
|
123
|
+
message
|
|
124
|
+
});
|
|
125
|
+
pushToolCallsThrough(message.id);
|
|
126
|
+
}
|
|
127
|
+
pushToolCallsThrough(Number.POSITIVE_INFINITY);
|
|
128
|
+
return items;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Whether the thinking indicator should show — see
|
|
132
|
+
* {@link UseConversationResult.thinking} for the rule and the argument.
|
|
133
|
+
*/
|
|
134
|
+
function isThinking(state, messages, toolCalls) {
|
|
135
|
+
if (state !== "thinking") return false;
|
|
136
|
+
const last = toolCalls.at(-1);
|
|
137
|
+
if (last?.status === "pending") return false;
|
|
138
|
+
const lastMessage = messages.at(-1);
|
|
139
|
+
return !lastMessage || lastMessage.role === "user" || Boolean(last);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Subscribe to the conversation: the interleaved exchange, the streaming
|
|
143
|
+
* utterance, the live transcript and the thinking rule — with no markup.
|
|
144
|
+
*
|
|
145
|
+
* Must be used inside the provider `mountClient()` installs.
|
|
146
|
+
*
|
|
147
|
+
* @example A custom bubble, keeping every rule `<MessageList>` knows
|
|
148
|
+
* ```tsx
|
|
149
|
+
* import { useConversation } from "@alexkroman1/aai-ui";
|
|
150
|
+
*
|
|
151
|
+
* function Transcript() {
|
|
152
|
+
* const { items, streaming, transcript, thinking } = useConversation();
|
|
153
|
+
* return (
|
|
154
|
+
* <div>
|
|
155
|
+
* {items.map((item) =>
|
|
156
|
+
* item.kind === "message" ? (
|
|
157
|
+
* <p key={item.message.id} data-role={item.message.role}>
|
|
158
|
+
* {item.message.content}
|
|
159
|
+
* </p>
|
|
160
|
+
* ) : (
|
|
161
|
+
* <code key={item.toolCall.callId}>{item.toolCall.name}</code>
|
|
162
|
+
* ),
|
|
163
|
+
* )}
|
|
164
|
+
* {streaming !== null && <p data-role="assistant">{streaming}</p>}
|
|
165
|
+
* {transcript.speaking && <p data-role="user">{transcript.text}</p>}
|
|
166
|
+
* {thinking && <p>…</p>}
|
|
167
|
+
* </div>
|
|
168
|
+
* );
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @returns See {@link UseConversationResult}.
|
|
173
|
+
*
|
|
174
|
+
* @public
|
|
175
|
+
*/
|
|
176
|
+
function useConversation() {
|
|
177
|
+
const state = useSessionSelector((s) => s.state);
|
|
178
|
+
const messages = useSessionSelector((s) => s.messages);
|
|
179
|
+
const toolCalls = useSessionSelector((s) => s.toolCalls);
|
|
180
|
+
const streaming = useSessionSelector((s) => s.agentTranscript);
|
|
181
|
+
const transcript = useUserTranscript();
|
|
182
|
+
const items = useMemo(() => interleave(messages, toolCalls), [messages, toolCalls]);
|
|
183
|
+
const thinking = useMemo(() => isThinking(state, messages, toolCalls), [
|
|
184
|
+
state,
|
|
185
|
+
messages,
|
|
186
|
+
toolCalls
|
|
187
|
+
]);
|
|
188
|
+
return useMemo(() => ({
|
|
189
|
+
items,
|
|
190
|
+
streaming,
|
|
191
|
+
transcript,
|
|
192
|
+
thinking
|
|
193
|
+
}), [
|
|
194
|
+
items,
|
|
195
|
+
streaming,
|
|
196
|
+
transcript,
|
|
197
|
+
thinking
|
|
198
|
+
]);
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/components/conversation-view.tsx
|
|
202
|
+
/**
|
|
203
|
+
* The id of the synthetic message the streaming row is rendered as. Real ids
|
|
204
|
+
* are assigned from zero upward at append time, so a negative one cannot
|
|
205
|
+
* collide with — or be mistaken for — a message in `items`.
|
|
206
|
+
*/
|
|
207
|
+
const STREAMING_MESSAGE_ID = -1;
|
|
208
|
+
/** The id a `spoken` log entry is rendered with — negative for the same reason. */
|
|
209
|
+
const SPOKEN_MESSAGE_ID = -2;
|
|
210
|
+
function defaultTool(toolCall) {
|
|
211
|
+
return /* @__PURE__ */ jsx(ToolCallRow, {
|
|
212
|
+
title: toolCall.name,
|
|
213
|
+
pending: toolCall.status === "pending",
|
|
214
|
+
variant: "compact",
|
|
215
|
+
className: "self-start"
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function defaultNote(entry) {
|
|
219
|
+
return /* @__PURE__ */ jsx("p", {
|
|
220
|
+
className: "text-xs text-center opacity-50",
|
|
221
|
+
children: entry.text
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function defaultTranscript(transcript) {
|
|
225
|
+
return /* @__PURE__ */ jsx("p", {
|
|
226
|
+
className: "italic opacity-70",
|
|
227
|
+
children: transcript.text
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
const DEFAULT_INDICATOR = /* @__PURE__ */ jsx("span", {
|
|
231
|
+
style: { animation: "aai-pulse 1.2s ease-in-out infinite" },
|
|
232
|
+
children: "· · ·"
|
|
233
|
+
});
|
|
234
|
+
/**
|
|
235
|
+
* The conversation's skeleton over {@link useConversation}, with every row a
|
|
236
|
+
* render slot: a pinned scroll region holding the empty state, the interleaved
|
|
237
|
+
* messages and tool calls, the streaming reply and the announced thinking row,
|
|
238
|
+
* plus the live transcript — inside the scroll or pinned beneath it.
|
|
239
|
+
*
|
|
240
|
+
* `useConversation()` already made the DATA one thing: the interleave, the
|
|
241
|
+
* streaming utterance, the `null`-vs-`""` transcript distinction and the
|
|
242
|
+
* thinking-suppression rule. What three custom chromes then each wrote around
|
|
243
|
+
* it was the same fifty lines of STRUCTURE: an `AutoScroll` with a bounded
|
|
244
|
+
* height, an empty-state guard on `items.length === 0 && streaming === null`,
|
|
245
|
+
* the map with its keys, the streaming row, the thinking row with its
|
|
246
|
+
* `role="status"` and `aria-label` (and the same comment about screen readers
|
|
247
|
+
* hearing punctuation), the transcript guarded on `speaking`. The bubbles are
|
|
248
|
+
* the part each template exists to show, so those are slots; the order and the
|
|
249
|
+
* accessibility contract are this component's.
|
|
250
|
+
*
|
|
251
|
+
* {@link MessageList} is this with the stock bubbles filled in.
|
|
252
|
+
*
|
|
253
|
+
* Must be rendered inside the providers `mountClient()` installs.
|
|
254
|
+
*
|
|
255
|
+
* @example A board's radio log: its own bubbles, the stock tool row, the transcript pinned below
|
|
256
|
+
* ```tsx
|
|
257
|
+
* import { ConversationView } from "@alexkroman1/aai-ui";
|
|
258
|
+
*
|
|
259
|
+
* function RadioLog() {
|
|
260
|
+
* return (
|
|
261
|
+
* <ConversationView
|
|
262
|
+
* contentClassName="p-4 flex flex-col gap-2"
|
|
263
|
+
* empty={<p className="text-center opacity-60">Standing by.</p>}
|
|
264
|
+
* renderMessage={({ role, content }) => (
|
|
265
|
+
* <div className={role === "assistant" ? "self-start" : "self-end"}>{content}</div>
|
|
266
|
+
* )}
|
|
267
|
+
* renderTranscript={({ text }) => <div className="px-4 py-2 italic">{text}</div>}
|
|
268
|
+
* transcriptPosition="below"
|
|
269
|
+
* thinkingLabel="Dispatch is thinking"
|
|
270
|
+
* />
|
|
271
|
+
* );
|
|
272
|
+
* }
|
|
273
|
+
* ```
|
|
274
|
+
*
|
|
275
|
+
* @param props - See {@link ConversationViewProps}.
|
|
276
|
+
*
|
|
277
|
+
* @public
|
|
278
|
+
*/
|
|
279
|
+
function ConversationView({ renderMessage, renderTool = defaultTool, renderStreaming, renderTranscript = defaultTranscript, transcriptPosition = "inline", log, renderNote = defaultNote, renderSessionHeader, empty, thinkingLabel = "Thinking", thinkingIndicator = DEFAULT_INDICATOR, thinkingClassName, className, contentClassName, scrollClassName, style }) {
|
|
280
|
+
const { items, streaming, transcript, thinking } = useConversation();
|
|
281
|
+
const liveItems = log ? void 0 : items;
|
|
282
|
+
const rows = useMemo(() => {
|
|
283
|
+
const renderItem = (item, key) => item.kind === "message" ? renderKeyed(`${key}m${item.message.id}`, renderMessage(item.message)) : renderKeyed(`${key}t${item.toolCall.callId}`, renderTool(item.toolCall));
|
|
284
|
+
if (!log) return (liveItems ?? []).map((item) => renderItem(item, ""));
|
|
285
|
+
return logRows(log, renderItem, renderMessage, renderNote, renderSessionHeader);
|
|
286
|
+
}, [
|
|
287
|
+
liveItems,
|
|
288
|
+
log,
|
|
289
|
+
renderMessage,
|
|
290
|
+
renderTool,
|
|
291
|
+
renderNote,
|
|
292
|
+
renderSessionHeader
|
|
293
|
+
]);
|
|
294
|
+
const streamingMessage = useMemo(() => streaming === null ? null : {
|
|
295
|
+
id: STREAMING_MESSAGE_ID,
|
|
296
|
+
role: "assistant",
|
|
297
|
+
content: streaming
|
|
298
|
+
}, [streaming]);
|
|
299
|
+
const transcriptRow = transcript.speaking ? renderTranscript(transcript) : null;
|
|
300
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(AutoScroll, {
|
|
301
|
+
className,
|
|
302
|
+
contentClassName,
|
|
303
|
+
scrollClassName,
|
|
304
|
+
style,
|
|
305
|
+
children: [
|
|
306
|
+
(log ?? items).length === 0 && streaming === null && empty,
|
|
307
|
+
rows,
|
|
308
|
+
streamingMessage !== null && (renderStreaming ? renderStreaming(streamingMessage.content) : renderMessage(streamingMessage)),
|
|
309
|
+
transcriptPosition === "inline" && transcriptRow,
|
|
310
|
+
thinking && /* @__PURE__ */ jsx("div", {
|
|
311
|
+
role: "status",
|
|
312
|
+
"aria-label": thinkingLabel,
|
|
313
|
+
className: thinkingClassName,
|
|
314
|
+
children: thinkingIndicator
|
|
315
|
+
})
|
|
316
|
+
]
|
|
317
|
+
}), transcriptPosition === "below" && transcriptRow] });
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* A log's rows. Keys are the entry's own identity (kind, time, session and run)
|
|
321
|
+
* plus a repeat count, not its index: the log is trimmed from the FRONT, and an
|
|
322
|
+
* index key would remount every row on each trim. Items within a session entry
|
|
323
|
+
* are keyed by position — a chain is append-only.
|
|
324
|
+
*/
|
|
325
|
+
function logRows(log, renderItem, renderMessage, renderNote, renderSessionHeader) {
|
|
326
|
+
const seen = /* @__PURE__ */ new Map();
|
|
327
|
+
return log.flatMap((entry) => {
|
|
328
|
+
const id = entry.kind === "session" ? `s${entry.at}:${entry.sessionId}:${entry.run}` : `${entry.kind}${entry.at}`;
|
|
329
|
+
const n = seen.get(id) ?? 0;
|
|
330
|
+
seen.set(id, n + 1);
|
|
331
|
+
const key = `${id}#${n}`;
|
|
332
|
+
if (entry.kind === "note") return [renderKeyed(key, renderNote(entry))];
|
|
333
|
+
if (entry.kind === "spoken") return [renderKeyed(key, renderMessage({
|
|
334
|
+
id: SPOKEN_MESSAGE_ID,
|
|
335
|
+
role: "assistant",
|
|
336
|
+
content: entry.text
|
|
337
|
+
}))];
|
|
338
|
+
return [renderKeyed(`${key}h`, renderSessionHeader?.(entry) ?? null), ...entry.items.map((item, i) => renderItem(item, `${key}/${i}`))];
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/** Give a slot's output a key without asking the renderer to remember one. */
|
|
342
|
+
function renderKeyed(key, node) {
|
|
343
|
+
return /* @__PURE__ */ jsx(RowSlot, { children: node }, key);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A keyed wrapper that renders nothing of its own — a `Fragment` with a key
|
|
347
|
+
* would do, but spelling it as a component keeps `renderKeyed` a plain call.
|
|
348
|
+
*/
|
|
349
|
+
function RowSlot({ children }) {
|
|
350
|
+
return children;
|
|
351
|
+
}
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/components/markdown.tsx
|
|
354
|
+
/** @jsxImportSource react */
|
|
355
|
+
const BARE_ORDERED_MARKER = /^(\s*)(\d{1,9})([.)])\s*$/;
|
|
356
|
+
const BARE_BULLET_MARKER = /^(\s*)([-*+])\s*$/;
|
|
357
|
+
const CODE_FENCE = /^\s*(```|~~~)/;
|
|
358
|
+
/**
|
|
359
|
+
* Escape list markers that have no content, so a terse reply renders as
|
|
360
|
+
* text instead of vanishing. CommonMark parses a line that is only `42.`
|
|
361
|
+
* as an *empty* ordered list starting at 42 — a voice agent answering
|
|
362
|
+
* "what's 6 times 7?" with "42." would otherwise display nothing (and a
|
|
363
|
+
* streaming transcript briefly ending on `1.` would blank the row).
|
|
364
|
+
* Lines inside fenced code blocks are left alone.
|
|
365
|
+
*/
|
|
366
|
+
function escapeBareListMarkers(text) {
|
|
367
|
+
let inFence = false;
|
|
368
|
+
return text.split("\n").map((line) => {
|
|
369
|
+
if (CODE_FENCE.test(line)) {
|
|
370
|
+
inFence = !inFence;
|
|
371
|
+
return line;
|
|
372
|
+
}
|
|
373
|
+
if (inFence) return line;
|
|
374
|
+
const ordered = BARE_ORDERED_MARKER.exec(line);
|
|
375
|
+
if (ordered) return `${ordered[1]}${ordered[2]}\\${ordered[3]}`;
|
|
376
|
+
const bullet = BARE_BULLET_MARKER.exec(line);
|
|
377
|
+
if (bullet) return `${bullet[1]}\\${bullet[2]}`;
|
|
378
|
+
return line;
|
|
379
|
+
}).join("\n");
|
|
380
|
+
}
|
|
381
|
+
const SCALE_CLASSES = {
|
|
382
|
+
default: {
|
|
383
|
+
h1: "mt-3 mb-1.5 text-[17px] font-semibold",
|
|
384
|
+
h2: "mt-3 mb-1.5 text-[16px] font-semibold",
|
|
385
|
+
h3: "mt-3 mb-1.5 text-[15px] font-semibold",
|
|
386
|
+
code: "text-[12.5px]",
|
|
387
|
+
table: "my-1.5 w-full border-collapse text-sm"
|
|
388
|
+
},
|
|
389
|
+
compact: {
|
|
390
|
+
h1: "mt-3 mb-1.5 text-[15px] font-medium",
|
|
391
|
+
h2: "mt-3 mb-1.5 text-[14px] font-medium",
|
|
392
|
+
h3: "mt-3 mb-1.5 text-[13px] font-medium",
|
|
393
|
+
code: "text-[11px]",
|
|
394
|
+
table: "my-1.5 w-full border-collapse text-[12px]"
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* Agent prose, rendered as Markdown.
|
|
399
|
+
*
|
|
400
|
+
* Pipeline and S2S models write emphasis, lists, `code`, and links; before
|
|
401
|
+
* this they arrived as literal asterisks and backticks. Styling is
|
|
402
|
+
* per-element (theme colors via inline styles, spacing via Tailwind) so it
|
|
403
|
+
* stays on the default client's type scale and follows custom themes.
|
|
404
|
+
* The optional `variant` selects the type scale (see
|
|
405
|
+
* {@link MarkdownVariant}).
|
|
406
|
+
* GFM is on for tables and strikethrough. react-markdown does not render
|
|
407
|
+
* raw HTML unless rehype-raw is added — keep it that way, this text comes
|
|
408
|
+
* from a model.
|
|
409
|
+
*
|
|
410
|
+
* Memoized alongside `MessageBubble`: message content is referentially
|
|
411
|
+
* stable across snapshots, so only the streaming row re-parses.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* ```tsx
|
|
415
|
+
* import { Markdown, useSessionSelector } from "@alexkroman1/aai-ui";
|
|
416
|
+
*
|
|
417
|
+
* // The agent's reply as it streams, rendered rather than shown as literal
|
|
418
|
+
* // asterisks and backticks.
|
|
419
|
+
* function LiveReply() {
|
|
420
|
+
* const text = useSessionSelector((snapshot) => snapshot.agentTranscript);
|
|
421
|
+
* return text === null ? null : <Markdown text={text} variant="compact" />;
|
|
422
|
+
* }
|
|
423
|
+
* ```
|
|
424
|
+
*
|
|
425
|
+
* @public
|
|
426
|
+
*/
|
|
427
|
+
const Markdown = memo(function Markdown({ text, variant = "default" }) {
|
|
428
|
+
const theme = useTheme();
|
|
429
|
+
const scale = SCALE_CLASSES[variant];
|
|
430
|
+
const components = useMemo(() => ({
|
|
431
|
+
p: (props) => /* @__PURE__ */ jsx("p", {
|
|
432
|
+
className: "my-1.5 first:mt-0 last:mb-0",
|
|
433
|
+
...props
|
|
434
|
+
}),
|
|
435
|
+
ul: (props) => /* @__PURE__ */ jsx("ul", {
|
|
436
|
+
className: "my-1.5 list-disc pl-5",
|
|
437
|
+
...props
|
|
438
|
+
}),
|
|
439
|
+
ol: (props) => /* @__PURE__ */ jsx("ol", {
|
|
440
|
+
className: "my-1.5 list-decimal pl-5",
|
|
441
|
+
...props
|
|
442
|
+
}),
|
|
443
|
+
li: (props) => /* @__PURE__ */ jsx("li", {
|
|
444
|
+
className: "my-0.5",
|
|
445
|
+
...props
|
|
446
|
+
}),
|
|
447
|
+
h1: (props) => /* @__PURE__ */ jsx("h1", {
|
|
448
|
+
className: scale.h1,
|
|
449
|
+
...props
|
|
450
|
+
}),
|
|
451
|
+
h2: (props) => /* @__PURE__ */ jsx("h2", {
|
|
452
|
+
className: scale.h2,
|
|
453
|
+
...props
|
|
454
|
+
}),
|
|
455
|
+
h3: (props) => /* @__PURE__ */ jsx("h3", {
|
|
456
|
+
className: scale.h3,
|
|
457
|
+
...props
|
|
458
|
+
}),
|
|
459
|
+
a: ({ style, ...props }) => /* @__PURE__ */ jsx("a", {
|
|
460
|
+
className: "underline underline-offset-2",
|
|
461
|
+
style: {
|
|
462
|
+
color: theme.primary,
|
|
463
|
+
...style
|
|
464
|
+
},
|
|
465
|
+
target: "_blank",
|
|
466
|
+
rel: "noreferrer noopener",
|
|
467
|
+
...props
|
|
468
|
+
}),
|
|
469
|
+
code: ({ className, style, children, ...rest }) => {
|
|
470
|
+
return /language-/.test(className ?? "") ? /* @__PURE__ */ jsx("code", {
|
|
471
|
+
className: clsx("font-aai-mono", scale.code),
|
|
472
|
+
...rest,
|
|
473
|
+
children
|
|
474
|
+
}) : /* @__PURE__ */ jsx("code", {
|
|
475
|
+
className: clsx("rounded-sm border px-1 py-0.5 font-aai-mono", scale.code),
|
|
476
|
+
style: {
|
|
477
|
+
borderColor: theme.border,
|
|
478
|
+
background: inkTint(theme.text, theme.surface, 3),
|
|
479
|
+
...style
|
|
480
|
+
},
|
|
481
|
+
...rest,
|
|
482
|
+
children
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
pre: ({ style, ...props }) => /* @__PURE__ */ jsx("pre", {
|
|
486
|
+
className: "my-1.5 overflow-x-auto rounded-md border p-2.5 whitespace-pre-wrap wrap-break-word",
|
|
487
|
+
style: {
|
|
488
|
+
borderColor: theme.border,
|
|
489
|
+
background: inkTint(theme.text, theme.surface, 3),
|
|
490
|
+
...style
|
|
491
|
+
},
|
|
492
|
+
...props
|
|
493
|
+
}),
|
|
494
|
+
blockquote: ({ style, ...props }) => /* @__PURE__ */ jsx("blockquote", {
|
|
495
|
+
className: "my-1.5 border-l-2 pl-3",
|
|
496
|
+
style: {
|
|
497
|
+
borderColor: theme.border,
|
|
498
|
+
color: inkTint(theme.text, theme.surface, 75),
|
|
499
|
+
...style
|
|
500
|
+
},
|
|
501
|
+
...props
|
|
502
|
+
}),
|
|
503
|
+
table: (props) => /* @__PURE__ */ jsx("table", {
|
|
504
|
+
className: scale.table,
|
|
505
|
+
...props
|
|
506
|
+
}),
|
|
507
|
+
th: ({ style, ...props }) => /* @__PURE__ */ jsx("th", {
|
|
508
|
+
className: "border px-2 py-1 text-left font-medium",
|
|
509
|
+
style: {
|
|
510
|
+
borderColor: theme.border,
|
|
511
|
+
...style
|
|
512
|
+
},
|
|
513
|
+
...props
|
|
514
|
+
}),
|
|
515
|
+
td: ({ style, ...props }) => /* @__PURE__ */ jsx("td", {
|
|
516
|
+
className: "border px-2 py-1",
|
|
517
|
+
style: {
|
|
518
|
+
borderColor: theme.border,
|
|
519
|
+
...style
|
|
520
|
+
},
|
|
521
|
+
...props
|
|
522
|
+
}),
|
|
523
|
+
hr: ({ style, ...props }) => /* @__PURE__ */ jsx("hr", {
|
|
524
|
+
className: "my-2.5 border-t",
|
|
525
|
+
style: {
|
|
526
|
+
borderColor: theme.border,
|
|
527
|
+
...style
|
|
528
|
+
},
|
|
529
|
+
...props
|
|
530
|
+
})
|
|
531
|
+
}), [theme, scale]);
|
|
532
|
+
return /* @__PURE__ */ jsx(ReactMarkdown, {
|
|
533
|
+
remarkPlugins: [remarkGfm],
|
|
534
|
+
components,
|
|
535
|
+
children: escapeBareListMarkers(text)
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region src/components/message-list.tsx
|
|
540
|
+
/** @jsxImportSource react */
|
|
541
|
+
const DOT_STYLES = [
|
|
542
|
+
0,
|
|
543
|
+
.16,
|
|
544
|
+
.32
|
|
545
|
+
].map((delay) => ({
|
|
546
|
+
animation: "aai-bounce 1.4s infinite ease-in-out both",
|
|
547
|
+
animationDelay: `${delay}s`
|
|
548
|
+
}));
|
|
549
|
+
/** The three bouncing dots alone, with no row around them. */
|
|
550
|
+
function Dots() {
|
|
551
|
+
const theme = useTheme();
|
|
552
|
+
const muted = inkTint(theme.text, theme.surface, 75);
|
|
553
|
+
return DOT_STYLES.map((style) => /* @__PURE__ */ jsx("div", {
|
|
554
|
+
className: "w-1.5 h-1.5 rounded-full",
|
|
555
|
+
style: {
|
|
556
|
+
...style,
|
|
557
|
+
background: muted
|
|
558
|
+
}
|
|
559
|
+
}, style.animationDelay));
|
|
560
|
+
}
|
|
561
|
+
/** The row the thinking indicator sits in — `ConversationView` renders this exact element around `Dots`. */
|
|
562
|
+
const THINKING_ROW = "flex items-center gap-2 text-sm font-medium min-h-5";
|
|
563
|
+
/**
|
|
564
|
+
* Animated three-dot "thinking" indicator.
|
|
565
|
+
*
|
|
566
|
+
* `role="status"` with a label, for the same reason `ConsoleShell` announces
|
|
567
|
+
* its error banner: three animated dots are the only signal that the agent is
|
|
568
|
+
* working on a reply, and to a screen reader they are three empty `<div>`s.
|
|
569
|
+
* It is also the indicator's semantic handle — a spec asserting its presence by
|
|
570
|
+
* counting `.rounded-full` elements breaks when the three dots become a spinner
|
|
571
|
+
* (correct behaviour, red test) and again when any sibling row gains a round
|
|
572
|
+
* badge (wrong behaviour, green test).
|
|
573
|
+
*
|
|
574
|
+
* The trailing thinking row is `ConversationView`'s own `role="status"`
|
|
575
|
+
* element, so this is only rendered INSIDE the live transcript bubble, where
|
|
576
|
+
* speech has been detected and no words have come back yet.
|
|
577
|
+
*
|
|
578
|
+
* @internal
|
|
579
|
+
*/
|
|
580
|
+
function ThinkingDots() {
|
|
581
|
+
return /* @__PURE__ */ jsx("div", {
|
|
582
|
+
role: "status",
|
|
583
|
+
"aria-label": "Thinking",
|
|
584
|
+
className: THINKING_ROW,
|
|
585
|
+
children: /* @__PURE__ */ jsx(Dots, {})
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Right-aligned user bubble — a primary-tinted card (the design system's
|
|
590
|
+
* indigo-50 fill with an indigo-100 edge, derived from the theme primary so
|
|
591
|
+
* custom themes stay coherent). Shared by finalized messages and the live
|
|
592
|
+
* transcript.
|
|
593
|
+
*/
|
|
594
|
+
function UserBubble({ theme, color, children }) {
|
|
595
|
+
return /* @__PURE__ */ jsx("div", {
|
|
596
|
+
className: "flex flex-col w-full items-end",
|
|
597
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
598
|
+
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]",
|
|
599
|
+
style: {
|
|
600
|
+
background: primaryTint(theme.primary, theme.surface, 7),
|
|
601
|
+
borderColor: primaryTint(theme.primary, theme.surface, 16),
|
|
602
|
+
color
|
|
603
|
+
},
|
|
604
|
+
children
|
|
605
|
+
})
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Renders a single chat message: labeled agent prose, or a user bubble.
|
|
610
|
+
*
|
|
611
|
+
* Memoized so appending one message re-renders one row, not the whole capped
|
|
612
|
+
* list: message objects and the theme are referentially stable across
|
|
613
|
+
* snapshots, and rows are keyed on stable ids (`ChatMessage.id`) that survive
|
|
614
|
+
* the sliding 200-message window.
|
|
615
|
+
*/
|
|
616
|
+
const MessageBubble = memo(function MessageBubble({ message, theme }) {
|
|
617
|
+
if (message.role === "user") return /* @__PURE__ */ jsx(UserBubble, {
|
|
618
|
+
theme,
|
|
619
|
+
color: theme.text,
|
|
620
|
+
children: message.content
|
|
621
|
+
});
|
|
622
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
623
|
+
className: "flex flex-col gap-1 max-w-[82%]",
|
|
624
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
625
|
+
className: "text-[10px] font-medium tracking-[1.2px] uppercase leading-none",
|
|
626
|
+
style: { color: inkTint(theme.text, theme.surface, 65) },
|
|
627
|
+
children: "Agent"
|
|
628
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
629
|
+
className: "wrap-break-word text-[15px] font-normal leading-[23px]",
|
|
630
|
+
style: { color: theme.text },
|
|
631
|
+
children: /* @__PURE__ */ jsx(Markdown, { text: message.content })
|
|
632
|
+
})]
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
/**
|
|
636
|
+
* Scrollable list of all chat messages, tool-call blocks, live transcript,
|
|
637
|
+
* streaming agent utterance, and a thinking indicator.
|
|
638
|
+
*
|
|
639
|
+
* Messages and tool calls are interleaved in the correct order. The list
|
|
640
|
+
* auto-scrolls to the latest content.
|
|
641
|
+
*
|
|
642
|
+
* Must be rendered inside a `SessionProvider`.
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```tsx
|
|
646
|
+
* import { MessageList } from "@alexkroman1/aai-ui";
|
|
647
|
+
*
|
|
648
|
+
* function Conversation() {
|
|
649
|
+
* return <MessageList className="flex-1" />;
|
|
650
|
+
* }
|
|
651
|
+
* ```
|
|
652
|
+
*
|
|
653
|
+
* @param props - Container props.
|
|
654
|
+
*
|
|
655
|
+
* @public
|
|
656
|
+
*/
|
|
657
|
+
const MessageList = memo(function MessageList({ className, log }) {
|
|
658
|
+
const theme = useTheme();
|
|
659
|
+
const renderMessage = useCallback((message) => /* @__PURE__ */ jsx(MessageBubble, {
|
|
660
|
+
message,
|
|
661
|
+
theme
|
|
662
|
+
}), [theme]);
|
|
663
|
+
const renderTool = useCallback((toolCall) => /* @__PURE__ */ jsx(ToolCallBlock, { toolCall }), []);
|
|
664
|
+
const renderTranscript = useCallback((transcript) => /* @__PURE__ */ jsx(UserBubble, {
|
|
665
|
+
theme,
|
|
666
|
+
color: inkTint(theme.text, theme.surface, 65),
|
|
667
|
+
children: transcript.partial ? transcript.partial : /* @__PURE__ */ jsx(ThinkingDots, {})
|
|
668
|
+
}), [theme]);
|
|
669
|
+
return /* @__PURE__ */ jsx(ConversationView, {
|
|
670
|
+
className,
|
|
671
|
+
log,
|
|
672
|
+
style: { background: theme.surface },
|
|
673
|
+
contentClassName: "flex flex-col gap-4 p-7",
|
|
674
|
+
renderMessage,
|
|
675
|
+
renderTool,
|
|
676
|
+
renderTranscript,
|
|
677
|
+
thinkingClassName: THINKING_ROW,
|
|
678
|
+
thinkingIndicator: /* @__PURE__ */ jsx(Dots, {})
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
//#endregion
|
|
682
|
+
export { useConversation as a, interleave as i, Markdown as n, AutoScroll as o, ConversationView as r, MessageList as t };
|