@alexkroman1/aai-ui 0.12.3 → 1.0.2
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/_react-test-utils.d.ts +86 -0
- package/dist/audio.js +4 -5
- package/dist/build-default-client.d.ts +1 -0
- package/dist/components/button.d.ts +4 -4
- package/dist/components/button.js +27 -4
- package/dist/components/chat-view.d.ts +14 -5
- package/dist/components/chat-view.js +64 -23
- package/dist/components/controls.d.ts +1 -1
- package/dist/components/controls.js +7 -4
- package/dist/components/message-list.d.ts +2 -2
- package/dist/components/message-list.js +100 -27
- package/dist/components/sidebar-layout.d.ts +7 -7
- package/dist/components/sidebar-layout.js +15 -8
- package/dist/components/start-screen.d.ts +4 -4
- package/dist/components/start-screen.js +16 -7
- package/dist/components/tool-call-block.d.ts +5 -6
- package/dist/components/tool-call-block.js +2 -117
- package/dist/components/tool-config-context.d.ts +22 -0
- package/dist/context.d.ts +23 -0
- package/dist/context.js +39 -0
- package/dist/default-client/assets/audio-DH9LpexG.js +1 -0
- package/dist/default-client/assets/capture-processor-C26lSiVr.js +53 -0
- package/dist/default-client/assets/default-client-AeJhjHfj.js +49 -0
- package/dist/default-client/assets/default-client-BK1ItCvw.css +2 -0
- package/dist/default-client/assets/playback-processor-dSA8Im99.js +101 -0
- package/dist/default-client/default-client.html +15 -0
- package/dist/default-client.d.ts +2 -0
- package/dist/define-client.d.ts +72 -24
- package/dist/define-client.js +71 -45
- package/dist/hooks.d.ts +5 -0
- package/dist/hooks.js +55 -0
- package/dist/index.d.ts +10 -32
- package/dist/index.js +7 -14
- package/dist/session-core.d.ts +70 -0
- package/dist/session-core.js +437 -0
- package/dist/tool-call-block-CCuChsFm.js +131 -0
- package/dist/types.d.ts +19 -22
- package/package.json +21 -17
- package/styles.css +3 -33
- 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/types.test-d.d.ts +0 -7
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities for React-based component tests.
|
|
3
|
+
* No dependency on @preact/signals.
|
|
4
|
+
*/
|
|
5
|
+
import type { SessionCore, SessionSnapshot } from "./session-core.ts";
|
|
6
|
+
import type { AgentState, ChatMessage, SessionError, ToolCallInfo } from "./types.ts";
|
|
7
|
+
/**
|
|
8
|
+
* Create a mock SessionCore for React component tests.
|
|
9
|
+
*
|
|
10
|
+
* Returns a `SessionCore`-compatible object with mutable snapshot. Call
|
|
11
|
+
* `core.update(partial)` to mutate the snapshot and notify subscribers,
|
|
12
|
+
* triggering React re-renders.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createMockSessionCore(overrides?: Partial<{
|
|
15
|
+
state: AgentState;
|
|
16
|
+
messages: ChatMessage[];
|
|
17
|
+
toolCalls: ToolCallInfo[];
|
|
18
|
+
userTranscript: string | null;
|
|
19
|
+
agentTranscript: string | null;
|
|
20
|
+
error: SessionError | null;
|
|
21
|
+
started: boolean;
|
|
22
|
+
running: boolean;
|
|
23
|
+
}>): SessionCore & {
|
|
24
|
+
update(partial: Partial<SessionSnapshot>): void;
|
|
25
|
+
};
|
|
26
|
+
/** Default voice options for tests. */
|
|
27
|
+
export declare function voiceOpts(overrides?: Partial<import("./audio.ts").VoiceIOOptions>): {
|
|
28
|
+
sttSampleRate: number;
|
|
29
|
+
ttsSampleRate: number;
|
|
30
|
+
captureWorkletSrc: string;
|
|
31
|
+
playbackWorkletSrc: string;
|
|
32
|
+
onMicData: (pcm16: ArrayBuffer) => void;
|
|
33
|
+
onPlaybackProgress?: (samplesPlayed: number) => void;
|
|
34
|
+
};
|
|
35
|
+
export declare class MockMessagePort {
|
|
36
|
+
onmessage: ((e: MessageEvent) => void) | null;
|
|
37
|
+
posted: unknown[];
|
|
38
|
+
postMessage(data: unknown, _transfer?: Transferable[]): void;
|
|
39
|
+
simulateMessage(data: unknown): void;
|
|
40
|
+
}
|
|
41
|
+
export declare class MockAudioNode {
|
|
42
|
+
connected: (MockAudioNode | MockAudioWorkletNode)[];
|
|
43
|
+
connect(dest: MockAudioNode | MockAudioWorkletNode): void;
|
|
44
|
+
disconnect(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare class MockGainNode extends MockAudioNode {
|
|
47
|
+
gain: {
|
|
48
|
+
value: number;
|
|
49
|
+
setTargetAtTime(value: number, _startTime: number, _tc: number): void;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export declare class MockAudioWorkletNode {
|
|
53
|
+
port: MockMessagePort;
|
|
54
|
+
connected: MockAudioNode[];
|
|
55
|
+
name: string;
|
|
56
|
+
options: unknown;
|
|
57
|
+
constructor(_ctx: MockAudioContext, name: string, options?: unknown);
|
|
58
|
+
connect(dest: MockAudioNode): void;
|
|
59
|
+
disconnect(): void;
|
|
60
|
+
}
|
|
61
|
+
export declare class MockAudioContext {
|
|
62
|
+
sampleRate: number;
|
|
63
|
+
state: AudioContextState;
|
|
64
|
+
currentTime: number;
|
|
65
|
+
destination: MockAudioNode;
|
|
66
|
+
audioWorklet: {
|
|
67
|
+
modules: string[];
|
|
68
|
+
addModule(url: string): Promise<void>;
|
|
69
|
+
};
|
|
70
|
+
closed: boolean;
|
|
71
|
+
constructor(opts?: {
|
|
72
|
+
sampleRate?: number;
|
|
73
|
+
});
|
|
74
|
+
resume(): Promise<void>;
|
|
75
|
+
createMediaStreamSource(_stream: unknown): MockAudioNode;
|
|
76
|
+
createGain(): MockGainNode;
|
|
77
|
+
close(): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
export type AudioMockContext = {
|
|
80
|
+
lastContext: () => MockAudioContext;
|
|
81
|
+
workletNodes: () => MockAudioWorkletNode[];
|
|
82
|
+
};
|
|
83
|
+
export declare function installAudioMocks(): AudioMockContext & {
|
|
84
|
+
restore: () => void;
|
|
85
|
+
};
|
|
86
|
+
export declare function findWorkletNode(nodes: MockAudioWorkletNode[], name: string): MockAudioWorkletNode;
|
package/dist/audio.js
CHANGED
|
@@ -45,8 +45,9 @@ async function createVoiceIO(opts) {
|
|
|
45
45
|
});
|
|
46
46
|
mic.connect(capNode);
|
|
47
47
|
const chunkSizeBytes = Math.floor(sttSampleRate * MIC_BUFFER_SECONDS) * 2;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
48
|
+
const bufSize = chunkSizeBytes * 2;
|
|
49
|
+
const capBufA = new Uint8Array(bufSize);
|
|
50
|
+
const capBufB = new Uint8Array(bufSize);
|
|
50
51
|
let capBuf = capBufA;
|
|
51
52
|
let capOffset = 0;
|
|
52
53
|
capNode.port.postMessage({ event: "start" });
|
|
@@ -56,11 +57,9 @@ async function createVoiceIO(opts) {
|
|
|
56
57
|
capBuf.set(chunk, capOffset);
|
|
57
58
|
capOffset += chunk.byteLength;
|
|
58
59
|
if (capOffset >= chunkSizeBytes) {
|
|
59
|
-
|
|
60
|
-
const filled = capOffset;
|
|
60
|
+
onMicData(capBuf.buffer.slice(0, capOffset));
|
|
61
61
|
capBuf = capBuf === capBufA ? capBufB : capBufA;
|
|
62
62
|
capOffset = 0;
|
|
63
|
-
onMicData(sendBuf.buffer.slice(0, filled));
|
|
64
63
|
}
|
|
65
64
|
};
|
|
66
65
|
let playNode = null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
/**
|
|
3
3
|
* Visual style of a {@link Button}.
|
|
4
4
|
*
|
|
@@ -42,9 +42,9 @@ export type ButtonSize = "default" | "lg";
|
|
|
42
42
|
*
|
|
43
43
|
* @public
|
|
44
44
|
*/
|
|
45
|
-
export declare function Button({ variant, size, className, children, ...rest }: {
|
|
45
|
+
export declare function Button({ variant, size, className, children, style, ...rest }: {
|
|
46
46
|
variant?: ButtonVariant;
|
|
47
47
|
size?: ButtonSize;
|
|
48
48
|
className?: string;
|
|
49
|
-
children?:
|
|
50
|
-
} & Omit<
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "className">): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { useTheme } from "../context.js";
|
|
1
2
|
import clsx from "clsx";
|
|
2
|
-
import { jsx } from "
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
4
|
//#region components/button.tsx
|
|
5
|
+
/** @jsxImportSource react */
|
|
4
6
|
const LG_STYLE = {
|
|
5
7
|
display: "grid",
|
|
6
8
|
placeItems: "center",
|
|
@@ -33,11 +35,32 @@ const LG_STYLE = {
|
|
|
33
35
|
*
|
|
34
36
|
* @public
|
|
35
37
|
*/
|
|
36
|
-
function Button({ variant = "default", size = "default", className, children, ...rest }) {
|
|
38
|
+
function Button({ variant = "default", size = "default", className, children, style, ...rest }) {
|
|
39
|
+
const theme = useTheme();
|
|
40
|
+
let variantStyle;
|
|
41
|
+
if (variant === "default") variantStyle = {
|
|
42
|
+
background: theme.primary,
|
|
43
|
+
color: "#fff",
|
|
44
|
+
borderColor: "transparent"
|
|
45
|
+
};
|
|
46
|
+
else if (variant === "secondary") variantStyle = {
|
|
47
|
+
background: "rgba(255,255,255,0.059)",
|
|
48
|
+
color: "rgba(255,255,255,0.618)",
|
|
49
|
+
borderColor: theme.border
|
|
50
|
+
};
|
|
51
|
+
else variantStyle = {
|
|
52
|
+
background: "transparent",
|
|
53
|
+
color: "rgba(255,255,255,0.618)",
|
|
54
|
+
borderColor: theme.border
|
|
55
|
+
};
|
|
37
56
|
return /* @__PURE__ */ jsx("button", {
|
|
38
57
|
type: "button",
|
|
39
|
-
style:
|
|
40
|
-
|
|
58
|
+
style: {
|
|
59
|
+
...size === "lg" ? LG_STYLE : void 0,
|
|
60
|
+
...variantStyle,
|
|
61
|
+
...style
|
|
62
|
+
},
|
|
63
|
+
className: clsx(size !== "lg" && "flex items-center justify-center appearance-none m-0 h-8 px-3 py-1.5 w-fit leading-none", "rounded-aai text-sm font-medium cursor-pointer border outline-none", className),
|
|
41
64
|
...rest,
|
|
42
65
|
children
|
|
43
66
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
/**
|
|
3
3
|
* The main chat interface for a voice agent session.
|
|
4
|
-
* Displays a header (with title and
|
|
5
|
-
*
|
|
4
|
+
* Displays a header (with optional icon, title, and state indicator), an
|
|
5
|
+
* inline error banner, the {@link MessageList}, and session {@link Controls}.
|
|
6
6
|
*
|
|
7
7
|
* Must be rendered inside a {@link SessionProvider}.
|
|
8
8
|
*
|
|
@@ -20,10 +20,19 @@ import type * as preact from "preact";
|
|
|
20
20
|
* </SidebarLayout>
|
|
21
21
|
* ```
|
|
22
22
|
*
|
|
23
|
+
* @example Custom header icon
|
|
24
|
+
* ```tsx
|
|
25
|
+
* <ChatView icon={<img src="/logo.svg" />} title="My Agent" />
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @param icon - Optional element rendered before the title in the header.
|
|
29
|
+
* @param title - Optional title string for the header.
|
|
23
30
|
* @param className - Additional CSS class names applied to the root element.
|
|
24
31
|
*
|
|
25
32
|
* @public
|
|
26
33
|
*/
|
|
27
|
-
export declare function ChatView({ className }: {
|
|
34
|
+
export declare function ChatView({ icon, title, className, }: {
|
|
35
|
+
icon?: ReactNode;
|
|
36
|
+
title?: string;
|
|
28
37
|
className?: string;
|
|
29
|
-
}):
|
|
38
|
+
}): ReactNode;
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useSession } from "../signals.js";
|
|
1
|
+
import { useSession, useTheme } from "../context.js";
|
|
3
2
|
import { Controls } from "./controls.js";
|
|
4
|
-
import { ErrorBanner } from "./error-banner.js";
|
|
5
3
|
import { MessageList } from "./message-list.js";
|
|
6
|
-
import { StateIndicator } from "./state-indicator.js";
|
|
7
4
|
import clsx from "clsx";
|
|
8
|
-
import { jsx, jsxs } from "
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
6
|
//#region components/chat-view.tsx
|
|
7
|
+
/** @jsxImportSource react */
|
|
8
|
+
const STATE_COLORS = {
|
|
9
|
+
disconnected: "rgba(255,255,255,0.422)",
|
|
10
|
+
connecting: "rgba(255,255,255,0.422)",
|
|
11
|
+
ready: "#7fd88f",
|
|
12
|
+
listening: "#56b6c2",
|
|
13
|
+
thinking: "#f5a742",
|
|
14
|
+
speaking: "#e06c75",
|
|
15
|
+
error: "#e06c75"
|
|
16
|
+
};
|
|
10
17
|
/**
|
|
11
18
|
* The main chat interface for a voice agent session.
|
|
12
|
-
* Displays a header (with title and
|
|
13
|
-
*
|
|
19
|
+
* Displays a header (with optional icon, title, and state indicator), an
|
|
20
|
+
* inline error banner, the {@link MessageList}, and session {@link Controls}.
|
|
14
21
|
*
|
|
15
22
|
* Must be rendered inside a {@link SessionProvider}.
|
|
16
23
|
*
|
|
@@ -28,30 +35,64 @@ import { jsx, jsxs } from "preact/jsx-runtime";
|
|
|
28
35
|
* </SidebarLayout>
|
|
29
36
|
* ```
|
|
30
37
|
*
|
|
38
|
+
* @example Custom header icon
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <ChatView icon={<img src="/logo.svg" />} title="My Agent" />
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @param icon - Optional element rendered before the title in the header.
|
|
44
|
+
* @param title - Optional title string for the header.
|
|
31
45
|
* @param className - Additional CSS class names applied to the root element.
|
|
32
46
|
*
|
|
33
47
|
* @public
|
|
34
48
|
*/
|
|
35
|
-
function ChatView({ className }) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
49
|
+
function ChatView({ icon, title, className }) {
|
|
50
|
+
const session = useSession();
|
|
51
|
+
const theme = useTheme();
|
|
38
52
|
return /* @__PURE__ */ jsxs("div", {
|
|
39
|
-
|
|
53
|
+
className: clsx("flex flex-col h-screen max-w-130 mx-auto font-aai text-sm", className),
|
|
54
|
+
style: {
|
|
55
|
+
background: theme.bg,
|
|
56
|
+
color: theme.text
|
|
57
|
+
},
|
|
40
58
|
children: [
|
|
41
59
|
/* @__PURE__ */ jsxs("div", {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
className: "flex items-center gap-3 px-4 py-3 border-b shrink-0",
|
|
61
|
+
style: { borderColor: theme.border },
|
|
62
|
+
children: [
|
|
63
|
+
icon,
|
|
64
|
+
title ? /* @__PURE__ */ jsx("span", {
|
|
65
|
+
className: "text-sm font-semibold",
|
|
66
|
+
style: { color: theme.primary },
|
|
67
|
+
children: title
|
|
68
|
+
}) : !icon && /* @__PURE__ */ jsx("pre", {
|
|
69
|
+
className: "font-aai-mono text-[10px] leading-[1.1] font-bold m-0",
|
|
70
|
+
style: { color: theme.primary },
|
|
71
|
+
children: "▄▀█ ▄▀█ █\n█▀█ █▀█ █"
|
|
72
|
+
}),
|
|
73
|
+
/* @__PURE__ */ jsx("div", {
|
|
74
|
+
className: "ml-auto",
|
|
75
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
76
|
+
className: "inline-flex items-center justify-center gap-1.5 text-[13px] font-medium leading-[130%] capitalize",
|
|
77
|
+
style: { color: "rgba(255,255,255,0.284)" },
|
|
78
|
+
"data-state": session.state,
|
|
79
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
80
|
+
className: "w-2 h-2 rounded-full",
|
|
81
|
+
style: { background: STATE_COLORS[session.state] }
|
|
82
|
+
}), session.state]
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
}),
|
|
87
|
+
session.error && /* @__PURE__ */ jsx("div", {
|
|
88
|
+
className: "mx-4 mt-3 px-3 py-2 rounded-aai border text-[13px] leading-[130%]",
|
|
89
|
+
style: {
|
|
90
|
+
borderColor: "rgba(224,108,117,0.4)",
|
|
91
|
+
background: "rgba(224,108,117,0.08)",
|
|
92
|
+
color: "#e06c75"
|
|
93
|
+
},
|
|
94
|
+
children: session.error.message
|
|
53
95
|
}),
|
|
54
|
-
/* @__PURE__ */ jsx(ErrorBanner, { error: session.error }),
|
|
55
96
|
/* @__PURE__ */ jsx(MessageList, {}),
|
|
56
97
|
/* @__PURE__ */ jsx(Controls, {})
|
|
57
98
|
]
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { useSession } from "../
|
|
1
|
+
import { useSession, useTheme } from "../context.js";
|
|
2
2
|
import { Button } from "./button.js";
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import { jsx, jsxs } from "
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region components/controls.tsx
|
|
6
|
+
/** @jsxImportSource react */
|
|
6
7
|
/**
|
|
7
8
|
* Session control buttons: **Stop / Resume** and **New Conversation**.
|
|
8
9
|
*
|
|
@@ -20,12 +21,14 @@ import { jsx, jsxs } from "preact/jsx-runtime";
|
|
|
20
21
|
*/
|
|
21
22
|
function Controls({ className }) {
|
|
22
23
|
const { running, toggle, reset } = useSession();
|
|
24
|
+
const theme = useTheme();
|
|
23
25
|
return /* @__PURE__ */ jsxs("div", {
|
|
24
|
-
|
|
26
|
+
className: clsx("flex gap-2 px-4 py-3 border-t shrink-0", className),
|
|
27
|
+
style: { borderColor: theme.border },
|
|
25
28
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
26
29
|
variant: "secondary",
|
|
27
30
|
onClick: toggle,
|
|
28
|
-
children: running
|
|
31
|
+
children: running ? "Stop" : "Resume"
|
|
29
32
|
}), /* @__PURE__ */ jsx(Button, {
|
|
30
33
|
variant: "ghost",
|
|
31
34
|
onClick: reset,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* streaming agent utterance, and a thinking indicator.
|
|
4
4
|
*
|
|
5
5
|
* Messages and tool calls are interleaved in the correct order. The list
|
|
6
|
-
* auto-scrolls to the latest content
|
|
6
|
+
* auto-scrolls to the latest content.
|
|
7
7
|
*
|
|
8
8
|
* Must be rendered inside a {@link SessionProvider}.
|
|
9
9
|
*
|
|
@@ -18,4 +18,4 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function MessageList({ className }: {
|
|
20
20
|
className?: string;
|
|
21
|
-
}): import("
|
|
21
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,18 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ThinkingIndicator } from "./thinking-indicator.js";
|
|
4
|
-
import { ToolCallBlock } from "./tool-call-block.js";
|
|
5
|
-
import { Transcript } from "./transcript.js";
|
|
1
|
+
import { useSession, useTheme } from "../context.js";
|
|
2
|
+
import { t as ToolCallBlock } from "../tool-call-block-CCuChsFm.js";
|
|
6
3
|
import clsx from "clsx";
|
|
7
|
-
import {
|
|
8
|
-
import { jsx, jsxs } from "
|
|
4
|
+
import { useEffect, useMemo, useRef } from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
6
|
//#region components/message-list.tsx
|
|
7
|
+
/** @jsxImportSource react */
|
|
8
|
+
const DOT_STYLES = [
|
|
9
|
+
0,
|
|
10
|
+
.16,
|
|
11
|
+
.32
|
|
12
|
+
].map((delay) => ({
|
|
13
|
+
animation: "aai-bounce 1.4s infinite ease-in-out both",
|
|
14
|
+
animationDelay: `${delay}s`
|
|
15
|
+
}));
|
|
16
|
+
/** Animated three-dot "thinking" indicator. */
|
|
17
|
+
function ThinkingDots() {
|
|
18
|
+
return /* @__PURE__ */ jsx("div", {
|
|
19
|
+
className: "flex items-center gap-2 text-sm font-medium min-h-5",
|
|
20
|
+
style: { color: "rgba(255,255,255,0.422)" },
|
|
21
|
+
children: DOT_STYLES.map((style, i) => /* @__PURE__ */ jsx("div", {
|
|
22
|
+
className: "w-1.5 h-1.5 rounded-full",
|
|
23
|
+
style: {
|
|
24
|
+
...style,
|
|
25
|
+
background: "rgba(255,255,255,0.422)"
|
|
26
|
+
}
|
|
27
|
+
}, i))
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Renders a single chat message as a styled bubble. */
|
|
31
|
+
function MessageBubble({ message, theme }) {
|
|
32
|
+
if (message.role === "user") return /* @__PURE__ */ jsx("div", {
|
|
33
|
+
className: "flex flex-col w-full items-end",
|
|
34
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
35
|
+
className: "max-w-[min(82%,64ch)] border px-3 py-2 rounded-aai whitespace-pre-wrap wrap-break-word text-sm font-normal leading-[150%]",
|
|
36
|
+
style: {
|
|
37
|
+
background: "rgba(255,255,255,0.031)",
|
|
38
|
+
borderColor: theme.border,
|
|
39
|
+
color: theme.text
|
|
40
|
+
},
|
|
41
|
+
children: message.content
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
return /* @__PURE__ */ jsx("div", {
|
|
45
|
+
className: "whitespace-pre-wrap wrap-break-word text-sm font-normal leading-[150%]",
|
|
46
|
+
style: { color: theme.text },
|
|
47
|
+
children: message.content
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function useAutoScroll() {
|
|
51
|
+
const ref = useRef(null);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
ref.current?.scrollIntoView({
|
|
54
|
+
behavior: "smooth",
|
|
55
|
+
block: "end"
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
return ref;
|
|
59
|
+
}
|
|
10
60
|
/**
|
|
11
61
|
* Scrollable list of all chat messages, tool-call blocks, live transcript,
|
|
12
62
|
* streaming agent utterance, and a thinking indicator.
|
|
13
63
|
*
|
|
14
64
|
* Messages and tool calls are interleaved in the correct order. The list
|
|
15
|
-
* auto-scrolls to the latest content
|
|
65
|
+
* auto-scrolls to the latest content.
|
|
16
66
|
*
|
|
17
67
|
* Must be rendered inside a {@link SessionProvider}.
|
|
18
68
|
*
|
|
@@ -26,47 +76,70 @@ import { jsx, jsxs } from "preact/jsx-runtime";
|
|
|
26
76
|
* @public
|
|
27
77
|
*/
|
|
28
78
|
function MessageList({ className }) {
|
|
29
|
-
const
|
|
79
|
+
const session = useSession();
|
|
80
|
+
const theme = useTheme();
|
|
30
81
|
const scrollRef = useAutoScroll();
|
|
31
|
-
const showThinking =
|
|
32
|
-
if (session.state
|
|
33
|
-
const last = session.toolCalls.
|
|
82
|
+
const showThinking = useMemo(() => {
|
|
83
|
+
if (session.state !== "thinking") return false;
|
|
84
|
+
const last = session.toolCalls.at(-1);
|
|
34
85
|
if (last?.status === "pending") return false;
|
|
35
|
-
const lastMsg = session.messages.
|
|
86
|
+
const lastMsg = session.messages.at(-1);
|
|
36
87
|
return !lastMsg || lastMsg.role === "user" || Boolean(last);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
}, [
|
|
89
|
+
session.state,
|
|
90
|
+
session.toolCalls,
|
|
91
|
+
session.messages
|
|
92
|
+
]);
|
|
93
|
+
const { messages, toolCalls } = session;
|
|
40
94
|
const items = [];
|
|
41
95
|
let tci = 0;
|
|
42
96
|
for (const [i, msg] of messages.entries()) {
|
|
43
|
-
items.push(/* @__PURE__ */ jsx(MessageBubble, {
|
|
97
|
+
items.push(/* @__PURE__ */ jsx(MessageBubble, {
|
|
98
|
+
message: msg,
|
|
99
|
+
theme
|
|
100
|
+
}, `msg-${i}`));
|
|
44
101
|
let tc = toolCalls[tci];
|
|
45
102
|
while (tc && tc.afterMessageIndex <= i) {
|
|
46
|
-
items.push(/* @__PURE__ */ jsx(ToolCallBlock, { toolCall: tc }, tc.
|
|
103
|
+
items.push(/* @__PURE__ */ jsx(ToolCallBlock, { toolCall: tc }, tc.callId));
|
|
47
104
|
tci++;
|
|
48
105
|
tc = toolCalls[tci];
|
|
49
106
|
}
|
|
50
107
|
}
|
|
51
108
|
let tc = toolCalls[tci];
|
|
52
109
|
while (tc) {
|
|
53
|
-
items.push(/* @__PURE__ */ jsx(ToolCallBlock, { toolCall: tc }, tc.
|
|
110
|
+
items.push(/* @__PURE__ */ jsx(ToolCallBlock, { toolCall: tc }, tc.callId));
|
|
54
111
|
tci++;
|
|
55
112
|
tc = toolCalls[tci];
|
|
56
113
|
}
|
|
114
|
+
const userTranscript = session.userTranscript;
|
|
57
115
|
return /* @__PURE__ */ jsx("div", {
|
|
58
116
|
role: "log",
|
|
59
|
-
|
|
117
|
+
className: clsx("flex-1 overflow-y-auto [scrollbar-width:none]", className),
|
|
118
|
+
style: { background: theme.surface },
|
|
60
119
|
children: /* @__PURE__ */ jsxs("div", {
|
|
61
|
-
|
|
120
|
+
className: "flex flex-col gap-4.5 p-4",
|
|
62
121
|
children: [
|
|
63
122
|
items,
|
|
64
|
-
session.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
123
|
+
session.agentTranscript && /* @__PURE__ */ jsx(MessageBubble, {
|
|
124
|
+
message: {
|
|
125
|
+
role: "assistant",
|
|
126
|
+
content: session.agentTranscript
|
|
127
|
+
},
|
|
128
|
+
theme
|
|
129
|
+
}),
|
|
130
|
+
userTranscript !== null && /* @__PURE__ */ jsx("div", {
|
|
131
|
+
className: "flex flex-col items-end w-full",
|
|
132
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
133
|
+
className: "max-w-[min(82%,64ch)] whitespace-pre-wrap wrap-break-word text-sm leading-[150%] border px-3 py-2 rounded-aai ml-auto",
|
|
134
|
+
style: {
|
|
135
|
+
color: "rgba(255,255,255,0.284)",
|
|
136
|
+
background: "rgba(255,255,255,0.031)",
|
|
137
|
+
borderColor: theme.border
|
|
138
|
+
},
|
|
139
|
+
children: userTranscript || /* @__PURE__ */ jsx(ThinkingDots, {})
|
|
140
|
+
})
|
|
141
|
+
}),
|
|
142
|
+
showThinking && /* @__PURE__ */ jsx(ThinkingDots, {}),
|
|
70
143
|
/* @__PURE__ */ jsx("div", { ref: scrollRef })
|
|
71
144
|
]
|
|
72
145
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
/**
|
|
3
3
|
* A two-column layout with a fixed-width sidebar and a flexible main area.
|
|
4
4
|
* Commonly used to pair a custom sidebar (cart, dashboard) with `<ChatView />`.
|
|
@@ -12,10 +12,10 @@ import type { ComponentChildren } from "preact";
|
|
|
12
12
|
*
|
|
13
13
|
* @public
|
|
14
14
|
*/
|
|
15
|
-
export declare function SidebarLayout({ sidebar, children,
|
|
16
|
-
sidebar:
|
|
17
|
-
children:
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
export declare function SidebarLayout({ sidebar, children, sidebarWidth, sidebarPosition, className, }: {
|
|
16
|
+
sidebar: ReactNode;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
sidebarWidth?: string | undefined;
|
|
19
|
+
sidebarPosition?: "left" | "right" | undefined;
|
|
20
20
|
className?: string;
|
|
21
|
-
}): import("
|
|
21
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { useTheme } from "../context.js";
|
|
1
2
|
import clsx from "clsx";
|
|
2
|
-
import { jsx, jsxs } from "
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
//#region components/sidebar-layout.tsx
|
|
5
|
+
/** @jsxImportSource react */
|
|
4
6
|
/**
|
|
5
7
|
* A two-column layout with a fixed-width sidebar and a flexible main area.
|
|
6
8
|
* Commonly used to pair a custom sidebar (cart, dashboard) with `<ChatView />`.
|
|
@@ -14,21 +16,26 @@ import { jsx, jsxs } from "preact/jsx-runtime";
|
|
|
14
16
|
*
|
|
15
17
|
* @public
|
|
16
18
|
*/
|
|
17
|
-
function SidebarLayout({ sidebar, children,
|
|
19
|
+
function SidebarLayout({ sidebar, children, sidebarWidth = "18rem", sidebarPosition = "left", className }) {
|
|
20
|
+
const theme = useTheme();
|
|
18
21
|
const sidebarEl = /* @__PURE__ */ jsx("div", {
|
|
19
|
-
|
|
20
|
-
style: {
|
|
22
|
+
className: "shrink-0 flex flex-col overflow-y-auto",
|
|
23
|
+
style: {
|
|
24
|
+
width: sidebarWidth,
|
|
25
|
+
...sidebarPosition === "left" ? { borderRight: `1px solid ${theme.border}` } : { borderLeft: `1px solid ${theme.border}` }
|
|
26
|
+
},
|
|
21
27
|
children: sidebar
|
|
22
28
|
});
|
|
23
29
|
return /* @__PURE__ */ jsxs("div", {
|
|
24
|
-
|
|
30
|
+
className: clsx("flex h-screen", className),
|
|
31
|
+
style: { background: theme.bg },
|
|
25
32
|
children: [
|
|
26
|
-
|
|
33
|
+
sidebarPosition === "left" && sidebarEl,
|
|
27
34
|
/* @__PURE__ */ jsx("div", {
|
|
28
|
-
|
|
35
|
+
className: "flex-1 min-w-0",
|
|
29
36
|
children
|
|
30
37
|
}),
|
|
31
|
-
|
|
38
|
+
sidebarPosition === "right" && sidebarEl
|
|
32
39
|
]
|
|
33
40
|
});
|
|
34
41
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
2
|
/**
|
|
3
3
|
* A centered start screen with icon, title, subtitle, and a start button.
|
|
4
4
|
* Renders `children` (the main app) once the session has started.
|
|
@@ -17,10 +17,10 @@ import type { ComponentChildren } from "preact";
|
|
|
17
17
|
* @public
|
|
18
18
|
*/
|
|
19
19
|
export declare function StartScreen({ children, icon, title, subtitle, buttonText, className, }: {
|
|
20
|
-
children:
|
|
21
|
-
icon?:
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
icon?: ReactNode | undefined;
|
|
22
22
|
title?: string | undefined;
|
|
23
23
|
subtitle?: string | undefined;
|
|
24
24
|
buttonText?: string | undefined;
|
|
25
25
|
className?: string | undefined;
|
|
26
|
-
}): import("
|
|
26
|
+
}): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
|