@devicai/ui 0.60.0 → 0.61.1
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 +4 -0
- package/dist/cjs/api/client.js +38 -5
- package/dist/cjs/api/client.js.map +1 -1
- package/dist/cjs/api/types.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js +9 -2
- package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js +2 -1
- package/dist/cjs/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/cjs/components/ChatDrawer/LiveVoicePanel.js +106 -0
- package/dist/cjs/components/ChatDrawer/LiveVoicePanel.js.map +1 -0
- package/dist/cjs/components/ChatDrawer/LiveVoicePrompter.js +148 -0
- package/dist/cjs/components/ChatDrawer/LiveVoicePrompter.js.map +1 -0
- package/dist/cjs/hooks/useDevicChat.js +68 -17
- package/dist/cjs/hooks/useDevicChat.js.map +1 -1
- package/dist/cjs/hooks/useDevicLiveVoice.js +73 -0
- package/dist/cjs/hooks/useDevicLiveVoice.js.map +1 -0
- package/dist/cjs/hooks/usePolling.js +1 -1
- package/dist/cjs/hooks/usePolling.js.map +1 -1
- package/dist/cjs/index.js +4 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/styles.css +1 -1
- package/dist/cjs/voice/LiveVoiceController.js +287 -0
- package/dist/cjs/voice/LiveVoiceController.js.map +1 -0
- package/dist/esm/api/client.d.ts +12 -3
- package/dist/esm/api/client.js +38 -5
- package/dist/esm/api/client.js.map +1 -1
- package/dist/esm/api/liveVoice.types.d.ts +49 -0
- package/dist/esm/api/types.d.ts +3 -0
- package/dist/esm/api/types.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.js +11 -4
- package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
- package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +6 -0
- package/dist/esm/components/ChatDrawer/ChatMessages.js +2 -1
- package/dist/esm/components/ChatDrawer/ChatMessages.js.map +1 -1
- package/dist/esm/components/ChatDrawer/LiveVoicePanel.d.ts +21 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePanel.js +102 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePanel.js.map +1 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePrompter.d.ts +12 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePrompter.js +145 -0
- package/dist/esm/components/ChatDrawer/LiveVoicePrompter.js.map +1 -0
- package/dist/esm/hooks/index.d.ts +2 -0
- package/dist/esm/hooks/useDevicChat.d.ts +6 -0
- package/dist/esm/hooks/useDevicChat.js +68 -17
- package/dist/esm/hooks/useDevicChat.js.map +1 -1
- package/dist/esm/hooks/useDevicLiveVoice.d.ts +20 -0
- package/dist/esm/hooks/useDevicLiveVoice.js +71 -0
- package/dist/esm/hooks/useDevicLiveVoice.js.map +1 -0
- package/dist/esm/hooks/usePolling.d.ts +1 -1
- package/dist/esm/hooks/usePolling.js +1 -1
- package/dist/esm/hooks/usePolling.js.map +1 -1
- package/dist/esm/index.d.ts +6 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/styles.css +1 -1
- package/dist/esm/voice/LiveVoiceController.d.ts +38 -0
- package/dist/esm/voice/LiveVoiceController.js +284 -0
- package/dist/esm/voice/LiveVoiceController.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var useTranslations = require('../../i18n/useTranslations.js');
|
|
6
|
+
|
|
7
|
+
/** Level readings kept per wave: one bar each, spread over the whole width. */
|
|
8
|
+
const WAVE_HISTORY = 160;
|
|
9
|
+
/** A new reading this often, so 160 bars are eight seconds of conversation. */
|
|
10
|
+
const WAVE_TICK_MS = 50;
|
|
11
|
+
/** Silence draws a dotted baseline rather than nothing, so the row reads as "live, quiet". */
|
|
12
|
+
const WAVE_FLOOR = 0.06;
|
|
13
|
+
/** Loudness of the last analyser window, 0..1: RMS rather than peak, so a click does not spike. */
|
|
14
|
+
function waveLevel(samples) {
|
|
15
|
+
let sum = 0;
|
|
16
|
+
for (let i = 0; i < samples.length; i++) {
|
|
17
|
+
const value = (samples[i] - 128) / 128;
|
|
18
|
+
sum += value * value;
|
|
19
|
+
}
|
|
20
|
+
return Math.min(1, Math.sqrt(sum / samples.length) * 4);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A full-width level history for one speaker: newest at the right, sliding
|
|
24
|
+
* left. Drawn in the row's CSS `color`, so the user's wave takes the muted grey
|
|
25
|
+
* and the assistant's the accent, and the two rows in parallel show who spoke
|
|
26
|
+
* when without a legend. Analyses existing streams only: never acquires a
|
|
27
|
+
* microphone or routes audio.
|
|
28
|
+
*/
|
|
29
|
+
function VoiceWave({ stream, role }) {
|
|
30
|
+
const t = useTranslations.useTranslations();
|
|
31
|
+
const canvasRef = React.useRef(null);
|
|
32
|
+
React.useEffect(() => {
|
|
33
|
+
const canvas = canvasRef.current;
|
|
34
|
+
if (!canvas)
|
|
35
|
+
return;
|
|
36
|
+
let drawing;
|
|
37
|
+
try {
|
|
38
|
+
drawing = canvas.getContext('2d');
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!drawing)
|
|
44
|
+
return;
|
|
45
|
+
let context;
|
|
46
|
+
let source;
|
|
47
|
+
let analyser;
|
|
48
|
+
let frame = 0;
|
|
49
|
+
let tick = -Infinity;
|
|
50
|
+
let color = '';
|
|
51
|
+
const levels = new Float32Array(WAVE_HISTORY);
|
|
52
|
+
const samples = new Uint8Array(256);
|
|
53
|
+
try {
|
|
54
|
+
if (stream && typeof AudioContext !== 'undefined') {
|
|
55
|
+
context = new AudioContext();
|
|
56
|
+
source = context.createMediaStreamSource(stream);
|
|
57
|
+
analyser = context.createAnalyser();
|
|
58
|
+
analyser.fftSize = 256;
|
|
59
|
+
source.connect(analyser);
|
|
60
|
+
void context.resume().catch(() => { });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { /* A visualisation failure must not interrupt the call. */ }
|
|
64
|
+
const fit = () => {
|
|
65
|
+
const ratio = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;
|
|
66
|
+
canvas.width = Math.max(1, Math.round((canvas.clientWidth || canvas.width) * ratio));
|
|
67
|
+
canvas.height = Math.max(1, Math.round((canvas.clientHeight || canvas.height) * ratio));
|
|
68
|
+
};
|
|
69
|
+
const draw = (now) => {
|
|
70
|
+
if (now - tick >= WAVE_TICK_MS) {
|
|
71
|
+
tick = now;
|
|
72
|
+
samples.fill(128);
|
|
73
|
+
try {
|
|
74
|
+
analyser?.getByteTimeDomainData(samples);
|
|
75
|
+
}
|
|
76
|
+
catch { /* Closed stream. */ }
|
|
77
|
+
levels.copyWithin(0, 1);
|
|
78
|
+
levels[WAVE_HISTORY - 1] = waveLevel(samples);
|
|
79
|
+
color = typeof getComputedStyle === 'function' ? getComputedStyle(canvas).color : '';
|
|
80
|
+
}
|
|
81
|
+
const { width, height } = canvas;
|
|
82
|
+
drawing.clearRect(0, 0, width, height);
|
|
83
|
+
drawing.strokeStyle = color || '#8c8c8c';
|
|
84
|
+
drawing.lineCap = 'round';
|
|
85
|
+
const step = width / WAVE_HISTORY;
|
|
86
|
+
const bar = Math.max(1, step * 0.5);
|
|
87
|
+
drawing.lineWidth = bar;
|
|
88
|
+
const middle = height / 2;
|
|
89
|
+
for (let i = 0; i < WAVE_HISTORY; i++) {
|
|
90
|
+
const tall = Math.max(0, Math.max(WAVE_FLOOR, levels[i]) * (height - bar) - bar);
|
|
91
|
+
const x = i * step + step / 2;
|
|
92
|
+
drawing.globalAlpha = 0.3 + 0.7 * (i / WAVE_HISTORY);
|
|
93
|
+
drawing.beginPath();
|
|
94
|
+
drawing.moveTo(x, middle - tall / 2);
|
|
95
|
+
drawing.lineTo(x, middle + tall / 2);
|
|
96
|
+
drawing.stroke();
|
|
97
|
+
}
|
|
98
|
+
drawing.globalAlpha = 1;
|
|
99
|
+
frame = requestAnimationFrame(draw);
|
|
100
|
+
};
|
|
101
|
+
fit();
|
|
102
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(fit) : undefined;
|
|
103
|
+
observer?.observe(canvas);
|
|
104
|
+
frame = requestAnimationFrame(draw);
|
|
105
|
+
return () => {
|
|
106
|
+
cancelAnimationFrame(frame);
|
|
107
|
+
observer?.disconnect();
|
|
108
|
+
source?.disconnect();
|
|
109
|
+
analyser?.disconnect();
|
|
110
|
+
if (context)
|
|
111
|
+
void context.close().catch(() => { });
|
|
112
|
+
try {
|
|
113
|
+
drawing.clearRect(0, 0, canvas.width, canvas.height);
|
|
114
|
+
}
|
|
115
|
+
catch { /* Detached canvas. */ }
|
|
116
|
+
};
|
|
117
|
+
}, [stream, role]);
|
|
118
|
+
return jsxRuntime.jsxs("div", { className: `devic-voice-wave-row devic-voice-wave-row--${role}`, children: [jsxRuntime.jsx("span", { children: role === 'user' ? t('You') : t('Assistant') }), jsxRuntime.jsx("canvas", { ref: canvasRef, className: "devic-voice-wave-canvas", width: 640, height: 28, role: "img", "aria-label": role === 'user' ? t('Your audio level') : t('Assistant audio level') })] });
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The transcript as a teleprompter, matching Active Chat: the newest words sit
|
|
122
|
+
* at the bottom of a fixed-height viewport and everything earlier slides up and
|
|
123
|
+
* fades out through the top edge; below it, one level wave per speaker.
|
|
124
|
+
*/
|
|
125
|
+
function LiveVoicePrompter({ voice }) {
|
|
126
|
+
const t = useTranslations.useTranslations();
|
|
127
|
+
const viewport = React.useRef(null);
|
|
128
|
+
const text = React.useRef(null);
|
|
129
|
+
React.useLayoutEffect(() => {
|
|
130
|
+
const update = () => {
|
|
131
|
+
if (text.current && viewport.current)
|
|
132
|
+
text.current.style.transform = `translateY(-${Math.max(0, text.current.scrollHeight - viewport.current.clientHeight)}px)`;
|
|
133
|
+
};
|
|
134
|
+
update();
|
|
135
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(update) : undefined;
|
|
136
|
+
if (viewport.current)
|
|
137
|
+
observer?.observe(viewport.current);
|
|
138
|
+
if (text.current)
|
|
139
|
+
observer?.observe(text.current);
|
|
140
|
+
return () => observer?.disconnect();
|
|
141
|
+
}, [voice.transcript]);
|
|
142
|
+
const placeholder = voice.state === 'connecting' ? t('Connecting voice…') : voice.state === 'closing' ? t('Closing voice…') : voice.state === 'reconnecting' ? t('Restoring voice…') : t('Listening…');
|
|
143
|
+
return jsxRuntime.jsxs("div", { className: "devic-voice-prompter", "aria-label": t('Live voice conversation'), children: [jsxRuntime.jsx("div", { ref: viewport, className: "devic-voice-prompter-viewport", "aria-live": "polite", children: jsxRuntime.jsx("div", { ref: text, className: "devic-voice-prompter-text", children: voice.transcript.length ? voice.transcript.slice(-4).map((turn, i, turns) => jsxRuntime.jsxs("p", { className: `devic-voice-turn${i === turns.length - 1 ? ' devic-voice-turn--current' : ''}`, children: [jsxRuntime.jsx("span", { children: turn.role === 'user' ? t('You') : t('Assistant') }), turn.text.replace(/\s+/g, ' ')] }, i)) : jsxRuntime.jsx("p", { className: "devic-voice-turn devic-voice-turn--placeholder", children: placeholder }) }) }), jsxRuntime.jsxs("div", { className: "devic-voice-waves", children: [jsxRuntime.jsx(VoiceWave, { role: "user", stream: voice.muted ? undefined : voice.input }), jsxRuntime.jsx(VoiceWave, { role: "assistant", stream: voice.output })] })] });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
exports.LiveVoicePrompter = LiveVoicePrompter;
|
|
147
|
+
exports.waveLevel = waveLevel;
|
|
148
|
+
//# sourceMappingURL=LiveVoicePrompter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LiveVoicePrompter.js","sources":["../../../../../src/components/ChatDrawer/LiveVoicePrompter.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef } from 'react';\nimport { useTranslations } from '../../i18n';\nimport type { UseDevicLiveVoiceResult } from '../../hooks/useDevicLiveVoice';\n\nexport interface LiveVoicePrompterProps {\n voice: Pick<UseDevicLiveVoiceResult, 'transcript' | 'input' | 'output' | 'muted' | 'state'>;\n}\n\n/** Level readings kept per wave: one bar each, spread over the whole width. */\nconst WAVE_HISTORY = 160;\n/** A new reading this often, so 160 bars are eight seconds of conversation. */\nconst WAVE_TICK_MS = 50;\n/** Silence draws a dotted baseline rather than nothing, so the row reads as \"live, quiet\". */\nconst WAVE_FLOOR = 0.06;\n\n/** Loudness of the last analyser window, 0..1: RMS rather than peak, so a click does not spike. */\nexport function waveLevel(samples: Uint8Array): number {\n let sum = 0;\n for (let i = 0; i < samples.length; i++) {\n const value = (samples[i] - 128) / 128;\n sum += value * value;\n }\n return Math.min(1, Math.sqrt(sum / samples.length) * 4);\n}\n\n/**\n * A full-width level history for one speaker: newest at the right, sliding\n * left. Drawn in the row's CSS `color`, so the user's wave takes the muted grey\n * and the assistant's the accent, and the two rows in parallel show who spoke\n * when without a legend. Analyses existing streams only: never acquires a\n * microphone or routes audio.\n */\nfunction VoiceWave({ stream, role }: { stream?: MediaStream; role: 'user' | 'assistant' }) {\n const t = useTranslations();\n const canvasRef = useRef<HTMLCanvasElement>(null);\n useEffect(() => {\n const canvas = canvasRef.current;\n if (!canvas) return;\n let drawing: CanvasRenderingContext2D | null;\n try { drawing = canvas.getContext('2d'); } catch { return; }\n if (!drawing) return;\n let context: AudioContext | undefined;\n let source: MediaStreamAudioSourceNode | undefined;\n let analyser: AnalyserNode | undefined;\n let frame = 0;\n let tick = -Infinity;\n let color = '';\n const levels = new Float32Array(WAVE_HISTORY);\n const samples = new Uint8Array(256);\n try {\n if (stream && typeof AudioContext !== 'undefined') {\n context = new AudioContext(); source = context.createMediaStreamSource(stream);\n analyser = context.createAnalyser(); analyser.fftSize = 256;\n source.connect(analyser); void context.resume().catch(() => {});\n }\n } catch { /* A visualisation failure must not interrupt the call. */ }\n const fit = () => {\n const ratio = typeof devicePixelRatio === 'number' && devicePixelRatio > 0 ? devicePixelRatio : 1;\n canvas.width = Math.max(1, Math.round((canvas.clientWidth || canvas.width) * ratio));\n canvas.height = Math.max(1, Math.round((canvas.clientHeight || canvas.height) * ratio));\n };\n const draw = (now: number) => {\n if (now - tick >= WAVE_TICK_MS) {\n tick = now; samples.fill(128);\n try { analyser?.getByteTimeDomainData(samples); } catch { /* Closed stream. */ }\n levels.copyWithin(0, 1); levels[WAVE_HISTORY - 1] = waveLevel(samples);\n color = typeof getComputedStyle === 'function' ? getComputedStyle(canvas).color : '';\n }\n const { width, height } = canvas;\n drawing!.clearRect(0, 0, width, height);\n drawing!.strokeStyle = color || '#8c8c8c';\n drawing!.lineCap = 'round';\n const step = width / WAVE_HISTORY;\n const bar = Math.max(1, step * 0.5);\n drawing!.lineWidth = bar;\n const middle = height / 2;\n for (let i = 0; i < WAVE_HISTORY; i++) {\n const tall = Math.max(0, Math.max(WAVE_FLOOR, levels[i]) * (height - bar) - bar);\n const x = i * step + step / 2;\n drawing!.globalAlpha = 0.3 + 0.7 * (i / WAVE_HISTORY);\n drawing!.beginPath(); drawing!.moveTo(x, middle - tall / 2);\n drawing!.lineTo(x, middle + tall / 2); drawing!.stroke();\n }\n drawing!.globalAlpha = 1;\n frame = requestAnimationFrame(draw);\n };\n fit();\n const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(fit) : undefined;\n observer?.observe(canvas); frame = requestAnimationFrame(draw);\n return () => {\n cancelAnimationFrame(frame); observer?.disconnect(); source?.disconnect(); analyser?.disconnect();\n if (context) void context.close().catch(() => {});\n try { drawing!.clearRect(0, 0, canvas.width, canvas.height); } catch { /* Detached canvas. */ }\n };\n }, [stream, role]);\n return <div className={`devic-voice-wave-row devic-voice-wave-row--${role}`}>\n <span>{role === 'user' ? t('You') : t('Assistant')}</span>\n <canvas ref={canvasRef} className=\"devic-voice-wave-canvas\" width={640} height={28} role=\"img\"\n aria-label={role === 'user' ? t('Your audio level') : t('Assistant audio level')} />\n </div>;\n}\n\n/**\n * The transcript as a teleprompter, matching Active Chat: the newest words sit\n * at the bottom of a fixed-height viewport and everything earlier slides up and\n * fades out through the top edge; below it, one level wave per speaker.\n */\nexport function LiveVoicePrompter({ voice }: LiveVoicePrompterProps) {\n const t = useTranslations();\n const viewport = useRef<HTMLDivElement>(null);\n const text = useRef<HTMLDivElement>(null);\n useLayoutEffect(() => {\n const update = () => {\n if (text.current && viewport.current) text.current.style.transform = `translateY(-${Math.max(0, text.current.scrollHeight - viewport.current.clientHeight)}px)`;\n };\n update();\n const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(update) : undefined;\n if (viewport.current) observer?.observe(viewport.current);\n if (text.current) observer?.observe(text.current);\n return () => observer?.disconnect();\n }, [voice.transcript]);\n const placeholder = voice.state === 'connecting' ? t('Connecting voice…') : voice.state === 'closing' ? t('Closing voice…') : voice.state === 'reconnecting' ? t('Restoring voice…') : t('Listening…');\n return <div className=\"devic-voice-prompter\" aria-label={t('Live voice conversation')}>\n <div ref={viewport} className=\"devic-voice-prompter-viewport\" aria-live=\"polite\">\n <div ref={text} className=\"devic-voice-prompter-text\">\n {voice.transcript.length ? voice.transcript.slice(-4).map((turn, i, turns) =>\n <p key={i} className={`devic-voice-turn${i === turns.length - 1 ? ' devic-voice-turn--current' : ''}`}>\n <span>{turn.role === 'user' ? t('You') : t('Assistant')}</span>{turn.text.replace(/\\s+/g, ' ')}\n </p>) : <p className=\"devic-voice-turn devic-voice-turn--placeholder\">{placeholder}</p>}\n </div>\n </div>\n <div className=\"devic-voice-waves\">\n <VoiceWave role=\"user\" stream={voice.muted ? undefined : voice.input} />\n <VoiceWave role=\"assistant\" stream={voice.output} />\n </div>\n </div>;\n}\n"],"names":["useTranslations","useRef","useEffect","_jsxs","_jsx","useLayoutEffect"],"mappings":"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"}
|
|
@@ -9,6 +9,7 @@ var usePolling = require('./usePolling.js');
|
|
|
9
9
|
var useModelInterface = require('./useModelInterface.js');
|
|
10
10
|
var assistantInfo = require('../api/assistantInfo.js');
|
|
11
11
|
var useTranslations = require('../i18n/useTranslations.js');
|
|
12
|
+
var useDevicLiveVoice = require('./useDevicLiveVoice.js');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Cadence for the handoff watch, which only waits for the parent thread to
|
|
@@ -143,16 +144,12 @@ function useDevicChat(options) {
|
|
|
143
144
|
onChatCreatedRef.current = onChatCreated;
|
|
144
145
|
});
|
|
145
146
|
// Create API client
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
if (clientRef.current && apiKey) {
|
|
153
|
-
clientRef.current.setConfig({ apiKey, baseUrl });
|
|
154
|
-
}
|
|
155
|
-
}, [apiKey, baseUrl]);
|
|
147
|
+
const client$1 = React.useMemo(() => apiKey || getTenantSession
|
|
148
|
+
? new client.DevicApiClient({ apiKey, baseUrl, getTenantSession, onSessionExpired }) : null, [apiKey, baseUrl, getTenantSession, onSessionExpired]);
|
|
149
|
+
const clientRef = React.useRef(client$1);
|
|
150
|
+
// A voice controller keeps its original client for teardown. Mutating its
|
|
151
|
+
// credentials on account switch would close the old session as the new user.
|
|
152
|
+
clientRef.current = client$1;
|
|
156
153
|
// --- Message queue --------------------------------------------------------
|
|
157
154
|
/**
|
|
158
155
|
* Messages accepted by this conversation that the model has not seen yet.
|
|
@@ -264,7 +261,13 @@ function useDevicChat(options) {
|
|
|
264
261
|
// Load initial chat history if chatUid prop is provided
|
|
265
262
|
// This runs once on mount (or when initialChatUid changes) to fetch existing conversation
|
|
266
263
|
const initialChatLoadedRef = React.useRef(false);
|
|
264
|
+
const previousInitialChatRef = React.useRef(initialChatUid);
|
|
267
265
|
React.useEffect(() => {
|
|
266
|
+
if (previousInitialChatRef.current !== initialChatUid) {
|
|
267
|
+
previousInitialChatRef.current = initialChatUid;
|
|
268
|
+
initialChatLoadedRef.current = false;
|
|
269
|
+
void voiceRef.current.stop();
|
|
270
|
+
}
|
|
268
271
|
if (initialChatUid && clientRef.current && !initialChatLoadedRef.current) {
|
|
269
272
|
initialChatLoadedRef.current = true;
|
|
270
273
|
const loadInitialChat = async () => {
|
|
@@ -272,6 +275,8 @@ function useDevicChat(options) {
|
|
|
272
275
|
setError(null);
|
|
273
276
|
try {
|
|
274
277
|
const history = await clientRef.current.getChatHistory(assistantId, initialChatUid, { tenantId: resolvedTenantId });
|
|
278
|
+
if (previousInitialChatRef.current !== initialChatUid)
|
|
279
|
+
return;
|
|
275
280
|
setMessages(history.chatContent);
|
|
276
281
|
mergeRecalledMemories(history.recalledMemories);
|
|
277
282
|
mergeCompactions(history.compactions);
|
|
@@ -294,6 +299,34 @@ function useDevicChat(options) {
|
|
|
294
299
|
tools: modelInterfaceTools,
|
|
295
300
|
onToolExecute: onToolCall,
|
|
296
301
|
});
|
|
302
|
+
const voice = useDevicLiveVoice.useDevicLiveVoice({
|
|
303
|
+
client: clientRef.current, assistantId, chatUid, enabled: options.liveVoice?.enabled,
|
|
304
|
+
context: { tenantId: resolvedTenantId, subtenantId: resolvedSubtenantId,
|
|
305
|
+
metadata: { ...resolvedTenantMetadata, ...(Object.keys(resolvedSubtenantMetadata).length ? { subtenantMetadata: resolvedSubtenantMetadata } : {}) },
|
|
306
|
+
tags: resolvedTags, tools: toolSchemas, enabledTools, disabledIntegrations },
|
|
307
|
+
onChatCreated: uid => {
|
|
308
|
+
if (chatUidRef.current !== uid) {
|
|
309
|
+
chatUidRef.current = uid;
|
|
310
|
+
setChatUid(uid);
|
|
311
|
+
onChatCreatedRef.current?.(uid);
|
|
312
|
+
}
|
|
313
|
+
setShouldPoll(true);
|
|
314
|
+
},
|
|
315
|
+
onError: error => onErrorRef.current?.(error),
|
|
316
|
+
});
|
|
317
|
+
const observingVoice = voice.active && !!chatUid && voice.chatUid === chatUid;
|
|
318
|
+
const observing = shouldPoll || observingVoice;
|
|
319
|
+
const voiceRef = React.useRef(voice);
|
|
320
|
+
voiceRef.current = voice;
|
|
321
|
+
const wasVoiceActive = React.useRef(false);
|
|
322
|
+
React.useEffect(() => {
|
|
323
|
+
if (wasVoiceActive.current && !voice.active && status === 'completed' && !queuedCount)
|
|
324
|
+
setShouldPoll(false);
|
|
325
|
+
wasVoiceActive.current = voice.active;
|
|
326
|
+
}, [voice.active, status, queuedCount]);
|
|
327
|
+
const handledClientCalls = React.useRef(new Set());
|
|
328
|
+
const notifiedMessages = React.useRef(new Map());
|
|
329
|
+
React.useEffect(() => { handledClientCalls.current.clear(); notifiedMessages.current.clear(); }, [chatUid, assistantId]);
|
|
297
330
|
// Pending widget calls awaiting user interaction
|
|
298
331
|
const [pendingWidgetCalls, setPendingWidgetCalls] = React.useState([]);
|
|
299
332
|
const pendingWidgetCallsRef = React.useRef([]);
|
|
@@ -302,7 +335,7 @@ function useDevicChat(options) {
|
|
|
302
335
|
}, [pendingWidgetCalls]);
|
|
303
336
|
// Polling hook - uses callbacks for side effects, return value not needed
|
|
304
337
|
logRef.current.log('[useDevicChat] Render - shouldPoll:', shouldPoll, 'chatUid:', chatUid);
|
|
305
|
-
usePolling.usePolling(
|
|
338
|
+
usePolling.usePolling(observing ? chatUid : null, async () => {
|
|
306
339
|
logRef.current.log('[useDevicChat] fetchFn called, chatUid:', chatUid);
|
|
307
340
|
if (!clientRef.current || !chatUid) {
|
|
308
341
|
throw new Error(t('Cannot poll without client or chatUid'));
|
|
@@ -313,10 +346,10 @@ function useDevicChat(options) {
|
|
|
313
346
|
}, {
|
|
314
347
|
interval: pollingInterval,
|
|
315
348
|
// Only when asked for: the poll is the default until the flag flips.
|
|
316
|
-
streamFn: streaming
|
|
349
|
+
streamFn: (streaming || observingVoice)
|
|
317
350
|
? (onSnapshot, signal, onActivity) => clientRef.current.streamRealtimeHistory(assistantId, chatUid, onSnapshot, signal, onActivity)
|
|
318
351
|
: undefined,
|
|
319
|
-
enabled:
|
|
352
|
+
enabled: observing,
|
|
320
353
|
stopStatuses: [
|
|
321
354
|
'completed',
|
|
322
355
|
'error',
|
|
@@ -325,6 +358,13 @@ function useDevicChat(options) {
|
|
|
325
358
|
'limit_exceeded',
|
|
326
359
|
],
|
|
327
360
|
onUpdate: async (data) => {
|
|
361
|
+
if (observingVoice) {
|
|
362
|
+
setIsLoading(data.status === 'processing' || data.status === 'buffering');
|
|
363
|
+
setHandedOff(data.status === 'handed_off');
|
|
364
|
+
setHandedOffSubThreadId(data.status === 'handed_off' ? data.handedOffSubThreadId || null : null);
|
|
365
|
+
if (data.status === 'error' || data.status === 'limit_exceeded')
|
|
366
|
+
void voiceRef.current.stop();
|
|
367
|
+
}
|
|
328
368
|
setStreamingMessage(data.status === 'processing' ? data.streamingMessage || null : null);
|
|
329
369
|
logRef.current.log('[useDevicChat] onUpdate called, status:', data.status);
|
|
330
370
|
// An assistant message written after something was queued from here is
|
|
@@ -415,7 +455,9 @@ function useDevicChat(options) {
|
|
|
415
455
|
setStatus(data.status);
|
|
416
456
|
// Notify about new messages
|
|
417
457
|
const lastMessage = data.chatHistory[data.chatHistory.length - 1];
|
|
418
|
-
|
|
458
|
+
const messageRevision = lastMessage ? JSON.stringify(lastMessage) : '';
|
|
459
|
+
if (lastMessage && lastMessage.role === 'assistant' && notifiedMessages.current.get(lastMessage.uid) !== messageRevision) {
|
|
460
|
+
notifiedMessages.current.set(lastMessage.uid, messageRevision);
|
|
419
461
|
onMessageReceivedRef.current?.(lastMessage);
|
|
420
462
|
}
|
|
421
463
|
// Handle model interface - check for pending tool calls
|
|
@@ -424,6 +466,8 @@ function useDevicChat(options) {
|
|
|
424
466
|
}
|
|
425
467
|
},
|
|
426
468
|
holdOpen: (data) => {
|
|
469
|
+
if (observingVoice && ['completed', 'waiting_for_tool_response', 'handed_off'].includes(data.status))
|
|
470
|
+
return true;
|
|
427
471
|
// Only `completed` is worth waiting on. An error, a usage limit or a
|
|
428
472
|
// gate mean something else is going on, and holding the poll open would
|
|
429
473
|
// just be watching a conversation that is not coming back.
|
|
@@ -494,9 +538,13 @@ function useDevicChat(options) {
|
|
|
494
538
|
if (!clientRef.current || !chatUid)
|
|
495
539
|
return;
|
|
496
540
|
// Get pending tool calls
|
|
497
|
-
const pendingCalls = data.pendingToolCalls || extractPendingToolCalls(data.chatHistory)
|
|
541
|
+
const pendingCalls = (data.pendingToolCalls || extractPendingToolCalls(data.chatHistory))
|
|
542
|
+
.filter(call => !handledClientCalls.current.has(call.id));
|
|
498
543
|
if (pendingCalls.length === 0)
|
|
499
544
|
return;
|
|
545
|
+
// Reserve before awaiting callbacks: repeated snapshots must not execute
|
|
546
|
+
// side effects twice. An ambiguous tool-response failure is not replayed.
|
|
547
|
+
pendingCalls.forEach(call => handledClientCalls.current.add(call.id));
|
|
500
548
|
try {
|
|
501
549
|
// Execute client-side tools (partitioned into immediate responses and widget-driven)
|
|
502
550
|
const { responses, widgetCalls } = await handleToolCalls(pendingCalls);
|
|
@@ -742,6 +790,7 @@ function useDevicChat(options) {
|
|
|
742
790
|
]);
|
|
743
791
|
// Clear chat
|
|
744
792
|
const clearChat = React.useCallback(() => {
|
|
793
|
+
void voiceRef.current.stop();
|
|
745
794
|
setShouldPoll(false);
|
|
746
795
|
setHandedOff(false);
|
|
747
796
|
setHandedOffSubThreadId(null);
|
|
@@ -764,6 +813,7 @@ function useDevicChat(options) {
|
|
|
764
813
|
}, [resetQueueState]);
|
|
765
814
|
// Load existing chat
|
|
766
815
|
const loadChat = React.useCallback(async (loadChatUid) => {
|
|
816
|
+
await voiceRef.current.stop();
|
|
767
817
|
if (!clientRef.current) {
|
|
768
818
|
const err = new Error(t('API client not configured'));
|
|
769
819
|
setError(err);
|
|
@@ -814,7 +864,7 @@ function useDevicChat(options) {
|
|
|
814
864
|
// 5s (or the configured cadence) to detect when the parent thread is no
|
|
815
865
|
// longer in handed_off state.
|
|
816
866
|
React.useEffect(() => {
|
|
817
|
-
if (!handedOff || !chatUid || !clientRef.current)
|
|
867
|
+
if (observingVoice || !handedOff || !chatUid || !clientRef.current)
|
|
818
868
|
return;
|
|
819
869
|
const pollHandoff = async () => {
|
|
820
870
|
try {
|
|
@@ -841,7 +891,7 @@ function useDevicChat(options) {
|
|
|
841
891
|
handoffPollRef.current = null;
|
|
842
892
|
}
|
|
843
893
|
};
|
|
844
|
-
}, [handedOff, chatUid, assistantId, handoffPollingInterval]);
|
|
894
|
+
}, [handedOff, chatUid, assistantId, handoffPollingInterval, observingVoice]);
|
|
845
895
|
// Called by HandoffSubagentWidget when the subthread reaches a terminal state
|
|
846
896
|
const onHandoffCompleted = React.useCallback(() => {
|
|
847
897
|
logRef.current.log('[useDevicChat] onHandoffCompleted called');
|
|
@@ -929,6 +979,7 @@ function useDevicChat(options) {
|
|
|
929
979
|
};
|
|
930
980
|
}, [assistantId, resetQueueState]);
|
|
931
981
|
return {
|
|
982
|
+
voice,
|
|
932
983
|
messages: streamingMessage && isLoading ? [...messages, streamingMessage] : messages,
|
|
933
984
|
chatUid,
|
|
934
985
|
isLoading,
|