@assistant-ui/react 0.15.32 → 0.15.34
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/adapters/GeminiLiveVoiceAdapter.d.ts +26 -0
- package/dist/adapters/GeminiLiveVoiceAdapter.d.ts.map +1 -0
- package/dist/adapters/GeminiLiveVoiceAdapter.js +346 -0
- package/dist/adapters/GeminiLiveVoiceAdapter.js.map +1 -0
- package/dist/adapters/gemini-live/audio.d.ts +27 -0
- package/dist/adapters/gemini-live/audio.d.ts.map +1 -0
- package/dist/adapters/gemini-live/audio.js +291 -0
- package/dist/adapters/gemini-live/audio.js.map +1 -0
- package/dist/context/stores/ThreadViewport.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/internal.js +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +1 -1
- package/dist/mcp-apps/app-frame.js +1 -1
- package/dist/primitives/actionBar/ActionBarRefreshRun.d.ts +26 -0
- package/dist/primitives/actionBar/ActionBarRefreshRun.d.ts.map +1 -0
- package/dist/primitives/actionBar/ActionBarRefreshRun.js +26 -0
- package/dist/primitives/actionBar/ActionBarRefreshRun.js.map +1 -0
- package/dist/primitives/actionBar/ActionBarResume.d.ts +30 -0
- package/dist/primitives/actionBar/ActionBarResume.d.ts.map +1 -0
- package/dist/primitives/actionBar/ActionBarResume.js +30 -0
- package/dist/primitives/actionBar/ActionBarResume.js.map +1 -0
- package/dist/primitives/actionBar/ActionBarRoot.d.ts +4 -2
- package/dist/primitives/actionBar/ActionBarRoot.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
- package/dist/primitives/actionBar.d.ts +2 -0
- package/dist/primitives/actionBar.d.ts.map +1 -1
- package/dist/primitives/actionBar.js +5 -1
- package/dist/primitives/actionBar.js.map +1 -1
- package/dist/primitives/error/ErrorRoot.d.ts +5 -0
- package/dist/primitives/error/ErrorRoot.d.ts.map +1 -1
- package/dist/primitives/error/ErrorRoot.js +30 -10
- package/dist/primitives/error/ErrorRoot.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js +1 -1
- package/dist/sandbox-host/SandboxHost.js +1 -1
- package/dist/unstable/useComposerInputHistory.js +1 -1
- package/package.json +6 -6
- package/src/adapters/GeminiLiveVoiceAdapter.test.ts +992 -0
- package/src/adapters/GeminiLiveVoiceAdapter.ts +454 -0
- package/src/adapters/gemini-live/audio.test.ts +880 -0
- package/src/adapters/gemini-live/audio.ts +333 -0
- package/src/index.test.ts +34 -0
- package/src/index.ts +4 -0
- package/src/primitives/actionBar/ActionBarRefreshRun.ts +37 -0
- package/src/primitives/actionBar/ActionBarResume.ts +41 -0
- package/src/primitives/actionBar/ActionBarRoot.tsx +2 -1
- package/src/primitives/actionBar/useActionBarFloatStatus.ts +5 -1
- package/src/primitives/actionBar.ts +2 -0
- package/src/primitives/error/ErrorRoot.tsx +15 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bytesToBase64,
|
|
3
|
+
raceWithAbortSignal,
|
|
4
|
+
} from "@assistant-ui/core/internal";
|
|
5
|
+
|
|
6
|
+
export const encodePcm16 = (samples: Float32Array): string => {
|
|
7
|
+
const bytes = new Uint8Array(samples.length * 2);
|
|
8
|
+
const view = new DataView(bytes.buffer);
|
|
9
|
+
for (let i = 0; i < samples.length; i++) {
|
|
10
|
+
const s = Math.max(-1, Math.min(1, samples[i]!));
|
|
11
|
+
view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);
|
|
12
|
+
}
|
|
13
|
+
return bytesToBase64(bytes);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const decodePcm16 = (base64: string): Float32Array<ArrayBuffer> => {
|
|
17
|
+
const binary = atob(base64);
|
|
18
|
+
const samples = new Float32Array(new ArrayBuffer((binary.length >> 1) * 4));
|
|
19
|
+
const view = new DataView(new ArrayBuffer(samples.length * 2));
|
|
20
|
+
for (let i = 0; i < samples.length * 2; i++)
|
|
21
|
+
view.setUint8(i, binary.charCodeAt(i));
|
|
22
|
+
for (let i = 0; i < samples.length; i++)
|
|
23
|
+
samples[i] = view.getInt16(i * 2, true) / 0x8000;
|
|
24
|
+
return samples;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const readLevel = (analyser: AnalyserNode): number => {
|
|
28
|
+
const data = new Uint8Array(analyser.fftSize);
|
|
29
|
+
analyser.getByteTimeDomainData(data);
|
|
30
|
+
let sum = 0;
|
|
31
|
+
for (const v of data) {
|
|
32
|
+
const centered = (v - 128) / 128;
|
|
33
|
+
sum += centered * centered;
|
|
34
|
+
}
|
|
35
|
+
return Math.min(1, Math.sqrt(sum / data.length) * 2);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type MicrophoneCapture = {
|
|
39
|
+
setMuted: (muted: boolean) => void;
|
|
40
|
+
level: () => number;
|
|
41
|
+
close: () => Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const createLowPass = (ratio: number) => {
|
|
45
|
+
const omega = (Math.PI * 0.9) / ratio;
|
|
46
|
+
const cosine = Math.cos(omega);
|
|
47
|
+
const sine = Math.sin(omega);
|
|
48
|
+
const sections = [0.5176380902, 0.7071067812, 1.9318516526].map((q) => {
|
|
49
|
+
const alpha = sine / (2 * q);
|
|
50
|
+
const a0 = 1 + alpha;
|
|
51
|
+
const b = (1 - cosine) / (2 * a0);
|
|
52
|
+
return { b, a1: (-2 * cosine) / a0, a2: (1 - alpha) / a0, z1: 0, z2: 0 };
|
|
53
|
+
});
|
|
54
|
+
return (samples: Float32Array) => {
|
|
55
|
+
for (let i = 0; i < samples.length; i++) {
|
|
56
|
+
let x = samples[i]!;
|
|
57
|
+
for (const s of sections) {
|
|
58
|
+
const y = s.b * x + s.z1;
|
|
59
|
+
s.z1 = 2 * s.b * x - s.a1 * y + s.z2;
|
|
60
|
+
s.z2 = s.b * x - s.a2 * y;
|
|
61
|
+
x = y;
|
|
62
|
+
}
|
|
63
|
+
samples[i] = x;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const WORKLET_SOURCE = `
|
|
69
|
+
class PcmCapture extends AudioWorkletProcessor {
|
|
70
|
+
constructor(options) {
|
|
71
|
+
super();
|
|
72
|
+
this.ratio = sampleRate / options.processorOptions.targetRate;
|
|
73
|
+
this.chunk = Math.round(options.processorOptions.targetRate / 50);
|
|
74
|
+
this.position = 0;
|
|
75
|
+
this.last = 0;
|
|
76
|
+
this.pending = [];
|
|
77
|
+
this.sections = [];
|
|
78
|
+
if (this.ratio > 1) {
|
|
79
|
+
const omega = (Math.PI * 0.9) / this.ratio;
|
|
80
|
+
const cosine = Math.cos(omega);
|
|
81
|
+
const sine = Math.sin(omega);
|
|
82
|
+
for (const q of [0.5176380902, 0.7071067812, 1.9318516526]) {
|
|
83
|
+
const alpha = sine / (2 * q);
|
|
84
|
+
const a0 = 1 + alpha;
|
|
85
|
+
const b = (1 - cosine) / (2 * a0);
|
|
86
|
+
this.sections.push({ b, a1: (-2 * cosine) / a0, a2: (1 - alpha) / a0, z1: 0, z2: 0 });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process(inputs) {
|
|
91
|
+
const channels = inputs[0];
|
|
92
|
+
if (!channels || channels.length === 0 || channels[0].length === 0) return true;
|
|
93
|
+
const input = channels[0].slice();
|
|
94
|
+
for (let c = 1; c < channels.length; c++)
|
|
95
|
+
for (let i = 0; i < input.length; i++) input[i] += channels[c][i];
|
|
96
|
+
if (channels.length > 1)
|
|
97
|
+
for (let i = 0; i < input.length; i++) input[i] /= channels.length;
|
|
98
|
+
for (let i = 0; i < input.length; i++) {
|
|
99
|
+
let x = input[i];
|
|
100
|
+
for (const s of this.sections) {
|
|
101
|
+
const y = s.b * x + s.z1;
|
|
102
|
+
s.z1 = 2 * s.b * x - s.a1 * y + s.z2;
|
|
103
|
+
s.z2 = s.b * x - s.a2 * y;
|
|
104
|
+
x = y;
|
|
105
|
+
}
|
|
106
|
+
input[i] = x;
|
|
107
|
+
}
|
|
108
|
+
while (this.position < input.length - 1) {
|
|
109
|
+
const index = Math.floor(this.position);
|
|
110
|
+
const prev = index < 0 ? this.last : input[index];
|
|
111
|
+
const next = input[index + 1];
|
|
112
|
+
this.pending.push(prev + (next - prev) * (this.position - index));
|
|
113
|
+
this.position += this.ratio;
|
|
114
|
+
}
|
|
115
|
+
this.position -= input.length;
|
|
116
|
+
this.last = input[input.length - 1];
|
|
117
|
+
if (this.pending.length >= this.chunk) {
|
|
118
|
+
this.port.postMessage(Float32Array.from(this.pending));
|
|
119
|
+
this.pending = [];
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
registerProcessor("pcm-capture", PcmCapture);
|
|
125
|
+
`;
|
|
126
|
+
|
|
127
|
+
export const createResampler = (ratio: number) => {
|
|
128
|
+
let position = 0;
|
|
129
|
+
let last = 0;
|
|
130
|
+
const lowPass = ratio > 1 ? createLowPass(ratio) : undefined;
|
|
131
|
+
return (input: Float32Array): Float32Array => {
|
|
132
|
+
if (input.length === 0) return new Float32Array(0);
|
|
133
|
+
const samples = lowPass ? input.slice() : input;
|
|
134
|
+
lowPass?.(samples);
|
|
135
|
+
const out: number[] = [];
|
|
136
|
+
while (position < samples.length - 1) {
|
|
137
|
+
const index = Math.floor(position);
|
|
138
|
+
const prev = index < 0 ? last : samples[index]!;
|
|
139
|
+
const next = samples[index + 1]!;
|
|
140
|
+
out.push(prev + (next - prev) * (position - index));
|
|
141
|
+
position += ratio;
|
|
142
|
+
}
|
|
143
|
+
position -= samples.length;
|
|
144
|
+
last = samples[samples.length - 1]!;
|
|
145
|
+
return Float32Array.from(out);
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const openMicrophone = async (options: {
|
|
150
|
+
sampleRate: number;
|
|
151
|
+
onChunk: (base64: string) => void;
|
|
152
|
+
signal?: AbortSignal;
|
|
153
|
+
}): Promise<MicrophoneCapture> => {
|
|
154
|
+
let stream: MediaStream | undefined;
|
|
155
|
+
const stopTracks = () => {
|
|
156
|
+
stream?.getTracks().forEach((track) => track.stop());
|
|
157
|
+
stream = undefined;
|
|
158
|
+
};
|
|
159
|
+
const throwIfAborted = () => {
|
|
160
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
161
|
+
};
|
|
162
|
+
let context: AudioContext | undefined;
|
|
163
|
+
try {
|
|
164
|
+
const openedStream = await raceWithAbortSignal(options.signal, async () => {
|
|
165
|
+
const acquired = await navigator.mediaDevices.getUserMedia({
|
|
166
|
+
audio: true,
|
|
167
|
+
});
|
|
168
|
+
stream = acquired;
|
|
169
|
+
if (options.signal?.aborted) {
|
|
170
|
+
stopTracks();
|
|
171
|
+
throw options.signal.reason;
|
|
172
|
+
}
|
|
173
|
+
return acquired;
|
|
174
|
+
});
|
|
175
|
+
throwIfAborted();
|
|
176
|
+
context = new AudioContext();
|
|
177
|
+
if (context.state === "suspended")
|
|
178
|
+
await raceWithAbortSignal(options.signal, () => context!.resume());
|
|
179
|
+
throwIfAborted();
|
|
180
|
+
const source = context.createMediaStreamSource(openedStream);
|
|
181
|
+
const analyser = context.createAnalyser();
|
|
182
|
+
analyser.fftSize = 256;
|
|
183
|
+
source.connect(analyser);
|
|
184
|
+
|
|
185
|
+
let muted = false;
|
|
186
|
+
let closed = false;
|
|
187
|
+
let closing: Promise<void> | undefined;
|
|
188
|
+
const push = (samples: Float32Array) => {
|
|
189
|
+
if (!muted && !closed && samples.length)
|
|
190
|
+
options.onChunk(encodePcm16(samples));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
let node: AudioNode | undefined;
|
|
194
|
+
let detach = () => {};
|
|
195
|
+
if (context.audioWorklet) {
|
|
196
|
+
const url = URL.createObjectURL(
|
|
197
|
+
new Blob([WORKLET_SOURCE], { type: "text/javascript" }),
|
|
198
|
+
);
|
|
199
|
+
try {
|
|
200
|
+
await raceWithAbortSignal(options.signal, () =>
|
|
201
|
+
context!.audioWorklet.addModule(url),
|
|
202
|
+
);
|
|
203
|
+
throwIfAborted();
|
|
204
|
+
const worklet = new AudioWorkletNode(context, "pcm-capture", {
|
|
205
|
+
processorOptions: { targetRate: options.sampleRate },
|
|
206
|
+
});
|
|
207
|
+
worklet.port.onmessage = (event) => push(event.data as Float32Array);
|
|
208
|
+
detach = () => {
|
|
209
|
+
worklet.port.onmessage = null;
|
|
210
|
+
};
|
|
211
|
+
node = worklet;
|
|
212
|
+
} catch {
|
|
213
|
+
// A CSP can block blob worklets even when AudioWorklet is available.
|
|
214
|
+
throwIfAborted();
|
|
215
|
+
} finally {
|
|
216
|
+
URL.revokeObjectURL(url);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!node) {
|
|
220
|
+
const resample = createResampler(context.sampleRate / options.sampleRate);
|
|
221
|
+
const processor = context.createScriptProcessor(1024, 1, 1);
|
|
222
|
+
processor.onaudioprocess = (event) =>
|
|
223
|
+
push(resample(event.inputBuffer.getChannelData(0)));
|
|
224
|
+
detach = () => {
|
|
225
|
+
processor.onaudioprocess = null;
|
|
226
|
+
};
|
|
227
|
+
node = processor;
|
|
228
|
+
}
|
|
229
|
+
analyser.connect(node);
|
|
230
|
+
node.connect(context.destination);
|
|
231
|
+
const opened = context;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
setMuted: (value) => {
|
|
235
|
+
muted = value;
|
|
236
|
+
openedStream.getAudioTracks().forEach((track) => {
|
|
237
|
+
track.enabled = !value;
|
|
238
|
+
});
|
|
239
|
+
},
|
|
240
|
+
level: () => readLevel(analyser),
|
|
241
|
+
close: () => {
|
|
242
|
+
if (closing) return closing;
|
|
243
|
+
closed = true;
|
|
244
|
+
detach();
|
|
245
|
+
node.disconnect();
|
|
246
|
+
source.disconnect();
|
|
247
|
+
stopTracks();
|
|
248
|
+
return (closing = opened.close());
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
} catch (error) {
|
|
252
|
+
stopTracks();
|
|
253
|
+
await context?.close().catch(() => {});
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export type PcmPlayer = {
|
|
259
|
+
enqueue: (base64: string) => void;
|
|
260
|
+
clear: () => void;
|
|
261
|
+
isPlaying: () => boolean;
|
|
262
|
+
level: () => number;
|
|
263
|
+
close: () => Promise<void>;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const IDLE_GRACE_MS = 100;
|
|
267
|
+
|
|
268
|
+
export const createPcmPlayer = (options: {
|
|
269
|
+
sampleRate: number;
|
|
270
|
+
onIdle?: () => void;
|
|
271
|
+
}): PcmPlayer => {
|
|
272
|
+
const context = new AudioContext({ sampleRate: options.sampleRate });
|
|
273
|
+
const analyser = context.createAnalyser();
|
|
274
|
+
analyser.fftSize = 256;
|
|
275
|
+
analyser.connect(context.destination);
|
|
276
|
+
const sources = new Set<AudioBufferSourceNode>();
|
|
277
|
+
let cursor = 0;
|
|
278
|
+
let closing: Promise<void> | undefined;
|
|
279
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
280
|
+
const scheduleIdle = () => {
|
|
281
|
+
clearTimeout(idleTimer);
|
|
282
|
+
idleTimer = setTimeout(() => {
|
|
283
|
+
if (sources.size === 0) options.onIdle?.();
|
|
284
|
+
}, IDLE_GRACE_MS);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
enqueue: (base64) => {
|
|
289
|
+
const samples = decodePcm16(base64);
|
|
290
|
+
if (samples.length === 0) return;
|
|
291
|
+
const buffer = context.createBuffer(
|
|
292
|
+
1,
|
|
293
|
+
samples.length,
|
|
294
|
+
options.sampleRate,
|
|
295
|
+
);
|
|
296
|
+
buffer.copyToChannel(samples, 0);
|
|
297
|
+
const source = context.createBufferSource();
|
|
298
|
+
source.buffer = buffer;
|
|
299
|
+
source.connect(analyser);
|
|
300
|
+
const start = Math.max(context.currentTime, cursor);
|
|
301
|
+
source.start(start);
|
|
302
|
+
cursor = start + buffer.duration;
|
|
303
|
+
sources.add(source);
|
|
304
|
+
source.onended = () => {
|
|
305
|
+
sources.delete(source);
|
|
306
|
+
if (sources.size === 0) scheduleIdle();
|
|
307
|
+
};
|
|
308
|
+
clearTimeout(idleTimer);
|
|
309
|
+
if (context.state === "suspended") void context.resume().catch(() => {});
|
|
310
|
+
},
|
|
311
|
+
clear: () => {
|
|
312
|
+
clearTimeout(idleTimer);
|
|
313
|
+
for (const source of sources) {
|
|
314
|
+
source.onended = null;
|
|
315
|
+
source.stop();
|
|
316
|
+
}
|
|
317
|
+
sources.clear();
|
|
318
|
+
cursor = 0;
|
|
319
|
+
},
|
|
320
|
+
isPlaying: () => sources.size > 0,
|
|
321
|
+
level: () => readLevel(analyser),
|
|
322
|
+
close: () => {
|
|
323
|
+
if (closing) return closing;
|
|
324
|
+
clearTimeout(idleTimer);
|
|
325
|
+
for (const source of sources) {
|
|
326
|
+
source.onended = null;
|
|
327
|
+
source.stop();
|
|
328
|
+
}
|
|
329
|
+
sources.clear();
|
|
330
|
+
return (closing = context.close());
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const PACKAGE_DIR = fileURLToPath(new URL("..", import.meta.url));
|
|
7
|
+
|
|
8
|
+
// The built entry imports React through @assistant-ui/tap/react-shim. Vite
|
|
9
|
+
// dev pre-bundles that shim with esbuild, whose CommonJS interop hands it
|
|
10
|
+
// React's default export only; the loader below recreates that for the shim.
|
|
11
|
+
const script = `
|
|
12
|
+
import { registerHooks } from "node:module";
|
|
13
|
+
registerHooks({
|
|
14
|
+
resolve(specifier, context, nextResolve) {
|
|
15
|
+
const resolved = nextResolve(specifier, context);
|
|
16
|
+
if (specifier !== "react" || !context.parentURL?.endsWith("/react-shim/index.js")) return resolved;
|
|
17
|
+
const source = "import React from " + JSON.stringify(resolved.url) + "; export default React;";
|
|
18
|
+
return { url: "data:text/javascript," + encodeURIComponent(source), shortCircuit: true };
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const entry = await import("@assistant-ui/react");
|
|
22
|
+
console.log(typeof entry.ComposerPrimitive.Root, typeof entry.CloudRendererHost);
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
it("loads the public entry when the react-shim only sees React's default export", () => {
|
|
26
|
+
const result = spawnSync(
|
|
27
|
+
process.execPath,
|
|
28
|
+
[...process.execArgv, "--input-type=module", "--eval", script],
|
|
29
|
+
{ cwd: PACKAGE_DIR, encoding: "utf8" },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(result.status, result.stderr).toBe(0);
|
|
33
|
+
expect(result.stdout.trim()).toBe("object function");
|
|
34
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -69,6 +69,10 @@ export {
|
|
|
69
69
|
OpenAIRealtimeVoiceAdapter,
|
|
70
70
|
type OpenAIRealtimeVoiceAdapterOptions,
|
|
71
71
|
} from "./adapters/OpenAIRealtimeVoiceAdapter";
|
|
72
|
+
export {
|
|
73
|
+
GeminiLiveVoiceAdapter,
|
|
74
|
+
type GeminiLiveVoiceAdapterOptions,
|
|
75
|
+
} from "./adapters/GeminiLiveVoiceAdapter";
|
|
72
76
|
export type {
|
|
73
77
|
VoiceSessionControls,
|
|
74
78
|
VoiceSessionHelpers,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ActionButtonElement,
|
|
5
|
+
type ActionButtonProps,
|
|
6
|
+
createActionButton,
|
|
7
|
+
} from "../../utils/createActionButton";
|
|
8
|
+
import { useActionBarRefreshRun as useActionBarRefreshRunBehavior } from "@assistant-ui/core/react";
|
|
9
|
+
|
|
10
|
+
const useActionBarRefreshRun = () => {
|
|
11
|
+
const { disabled, refresh } = useActionBarRefreshRunBehavior();
|
|
12
|
+
if (disabled) return null;
|
|
13
|
+
return refresh;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export namespace ActionBarPrimitiveRefreshRun {
|
|
17
|
+
export type Element = ActionButtonElement;
|
|
18
|
+
export type Props = ActionButtonProps<typeof useActionBarRefreshRun>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A button that re-reads an assistant message whose run another tab or
|
|
23
|
+
* session is still writing, so the thread shows its latest stored state
|
|
24
|
+
* without a page reload. The thread also re-reads it on its own around the
|
|
25
|
+
* moment the run would cross `staleAfter`. It renders disabled unless
|
|
26
|
+
* Assistant Cloud reported the run in progress elsewhere, and while a
|
|
27
|
+
* refresh is in flight.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* <ActionBarPrimitive.RefreshRun>Refresh</ActionBarPrimitive.RefreshRun>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const ActionBarPrimitiveRefreshRun = createActionButton(
|
|
35
|
+
"ActionBarPrimitive.RefreshRun",
|
|
36
|
+
useActionBarRefreshRun,
|
|
37
|
+
);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type ActionButtonElement,
|
|
5
|
+
type ActionButtonProps,
|
|
6
|
+
createActionButton,
|
|
7
|
+
} from "../../utils/createActionButton";
|
|
8
|
+
import { useActionBarResume as useActionBarResumeBehavior } from "@assistant-ui/core/react";
|
|
9
|
+
|
|
10
|
+
const useActionBarResume = () => {
|
|
11
|
+
const { disabled, resume } = useActionBarResumeBehavior();
|
|
12
|
+
if (disabled) return null;
|
|
13
|
+
return resume;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export namespace ActionBarPrimitiveResume {
|
|
17
|
+
export type Element = ActionButtonElement;
|
|
18
|
+
export type Props = ActionButtonProps<typeof useActionBarResume>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A button that answers again in place of an assistant message whose run
|
|
23
|
+
* stopped before it finished (its tab closed or lost its connection). Like
|
|
24
|
+
* Regenerate, it starts a new branch beside the interrupted reply, which
|
|
25
|
+
* stays as it is. It renders disabled unless the message is the last one on
|
|
26
|
+
* its branch and its run went `staleAfter` without a heartbeat.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <MessagePrimitive.Error>
|
|
31
|
+
* <ErrorPrimitive.Root>
|
|
32
|
+
* <ErrorPrimitive.Message />
|
|
33
|
+
* <ActionBarPrimitive.Resume>Resume</ActionBarPrimitive.Resume>
|
|
34
|
+
* </ErrorPrimitive.Root>
|
|
35
|
+
* </MessagePrimitive.Error>
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const ActionBarPrimitiveResume = createActionButton(
|
|
39
|
+
"ActionBarPrimitive.Resume",
|
|
40
|
+
useActionBarResume,
|
|
41
|
+
);
|
|
@@ -21,7 +21,8 @@ export namespace ActionBarPrimitiveRoot {
|
|
|
21
21
|
export type Element = ComponentRef<typeof Primitive.div>;
|
|
22
22
|
export type Props = PrimitiveDivProps & {
|
|
23
23
|
/**
|
|
24
|
-
* Whether to hide the action bar when the thread is running
|
|
24
|
+
* Whether to hide the action bar when the thread is running, or while
|
|
25
|
+
* another tab or session is still writing this message.
|
|
25
26
|
* @default false
|
|
26
27
|
*/
|
|
27
28
|
hideWhenRunning?: boolean | undefined;
|
|
@@ -24,7 +24,11 @@ export const useActionBarFloatStatus = ({
|
|
|
24
24
|
forceVisible,
|
|
25
25
|
}: UseActionBarFloatStatusProps) => {
|
|
26
26
|
return useAuiState((s) => {
|
|
27
|
-
if (
|
|
27
|
+
if (
|
|
28
|
+
hideWhenRunning &&
|
|
29
|
+
(s.thread.isRunning || s.message.runState === "in-progress")
|
|
30
|
+
)
|
|
31
|
+
return HideAndFloatStatus.Hidden;
|
|
28
32
|
|
|
29
33
|
const autohideEnabled =
|
|
30
34
|
autohide === "always" || (autohide === "not-last" && !s.message.isLast);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { ActionBarPrimitiveRoot as Root } from "./actionBar/ActionBarRoot";
|
|
2
2
|
export { ActionBarPrimitiveCopy as Copy } from "./actionBar/ActionBarCopy";
|
|
3
3
|
export { ActionBarPrimitiveReload as Reload } from "./actionBar/ActionBarReload";
|
|
4
|
+
export { ActionBarPrimitiveResume as Resume } from "./actionBar/ActionBarResume";
|
|
5
|
+
export { ActionBarPrimitiveRefreshRun as RefreshRun } from "./actionBar/ActionBarRefreshRun";
|
|
4
6
|
export { ActionBarPrimitiveEdit as Edit } from "./actionBar/ActionBarEdit";
|
|
5
7
|
export { ActionBarPrimitiveSpeak as Speak } from "./actionBar/ActionBarSpeak";
|
|
6
8
|
export { ActionBarPrimitiveStopSpeaking as StopSpeaking } from "./actionBar/ActionBarStopSpeaking";
|
|
@@ -6,17 +6,31 @@ import {
|
|
|
6
6
|
forwardRef,
|
|
7
7
|
type ComponentPropsWithoutRef,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { useAuiState } from "@assistant-ui/store";
|
|
9
10
|
|
|
10
11
|
export namespace ErrorPrimitiveRoot {
|
|
11
12
|
export type Element = ComponentRef<typeof Primitive.div>;
|
|
12
13
|
export type Props = ComponentPropsWithoutRef<typeof Primitive.div>;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The container of a message's error. It carries `data-run-state` (`stale` or
|
|
18
|
+
* `in-progress`) when the error stands for a run another tab or session
|
|
19
|
+
* drove, and announces an in-progress run as a status instead of an alert.
|
|
20
|
+
*/
|
|
15
21
|
export const ErrorPrimitiveRoot = forwardRef<
|
|
16
22
|
ErrorPrimitiveRoot.Element,
|
|
17
23
|
ErrorPrimitiveRoot.Props
|
|
18
24
|
>((props, forwardRef) => {
|
|
19
|
-
|
|
25
|
+
const runState = useAuiState((s) => s.optional.message?.runState);
|
|
26
|
+
return (
|
|
27
|
+
<Primitive.div
|
|
28
|
+
role={runState === "in-progress" ? "status" : "alert"}
|
|
29
|
+
{...(runState ? { "data-run-state": runState } : undefined)}
|
|
30
|
+
{...props}
|
|
31
|
+
ref={forwardRef}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
20
34
|
});
|
|
21
35
|
|
|
22
36
|
ErrorPrimitiveRoot.displayName = "ErrorPrimitive.Root";
|