@lunora/react 1.0.0-alpha.3 → 1.0.0-alpha.31
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/LICENSE.md +6 -0
- package/README.md +2 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +915 -276
- package/dist/index.d.ts +915 -276
- package/dist/index.mjs +27 -15
- package/dist/packem_shared/{AuthLoading-DtKgZT2Z.mjs → AuthLoading-DezUWNB2.mjs} +1 -1
- package/dist/packem_shared/{CheckoutButton-CVSry8U1.mjs → CheckoutButton-DUite8jJ.mjs} +8 -4
- package/dist/packem_shared/{LunoraProvider-D38Xp16l.mjs → LunoraProvider-BsuiW4Lk.mjs} +3 -2
- package/dist/packem_shared/{cache-CItk3fgN.mjs → cache-JeuDAXfE.mjs} +6 -1
- package/dist/packem_shared/hydratePreloaded-2omLtadR.mjs +54 -0
- package/dist/packem_shared/{lunoraQueryOptions-CsuWzjg1.mjs → lunoraQueryOptions-CefbPBId.mjs} +1 -1
- package/dist/packem_shared/query-key-LGnArBTB.mjs +13 -0
- package/dist/packem_shared/query-options.d-CdgGQ9s4.d.mts +38 -0
- package/dist/packem_shared/query-options.d-CdgGQ9s4.d.ts +38 -0
- package/dist/packem_shared/stable-key-DePnevIy.mjs +38 -0
- package/dist/packem_shared/{use-paginated-core-CoOfcc-p.mjs → use-paginated-core-xxvd1YA0.mjs} +37 -19
- package/dist/packem_shared/useAgent-BNLlIYHz.mjs +101 -0
- package/dist/packem_shared/useAgentChat-0cebQe7-.mjs +304 -0
- package/dist/packem_shared/useAgentState-Cqyd3Dfy.mjs +37 -0
- package/dist/packem_shared/useAgentToolEvents-gF7U4_KY.mjs +129 -0
- package/dist/packem_shared/{useAuth-CNUKtOOp.mjs → useAuth-BSutjJaa.mjs} +1 -1
- package/dist/packem_shared/{useAuthState-BiGhtSCs.mjs → useAuthState-CyF35qZR.mjs} +1 -1
- package/dist/packem_shared/useClientQuery-DIkNT4QA.mjs +17 -0
- package/dist/packem_shared/{useConnectionStatus-DRSY9ldm.mjs → useConnectionStatus-PJ7vyCHy.mjs} +1 -1
- package/dist/packem_shared/useFlag-Beyeiq2b.mjs +128 -0
- package/dist/packem_shared/useHttpStream-oc4s1pMy.mjs +127 -0
- package/dist/packem_shared/{useInfiniteQuery-MH0x4l8h.mjs → useInfiniteQuery-CJNw1XaZ.mjs} +1 -1
- package/dist/packem_shared/{useMutation-CrvMXRsk.mjs → useMutation-BeqeIjTr.mjs} +1 -1
- package/dist/packem_shared/useMutator-IIrLtDiM.mjs +47 -0
- package/dist/packem_shared/{usePaginatedQuery-D3PTDRGS.mjs → usePaginatedQuery-DWzydaVR.mjs} +1 -1
- package/dist/packem_shared/{usePresence-D7jLuxj0.mjs → usePresence-Rnx1Fznc.mjs} +13 -6
- package/dist/packem_shared/useQuery-CE8_QUgh.mjs +77 -0
- package/dist/packem_shared/{useStream-BRY9nemd.mjs → useStream-GYNgc9J5.mjs} +6 -3
- package/dist/packem_shared/{useSubscription-CHMCjyQg.mjs → useSubscription-DcDlvVuI.mjs} +3 -3
- package/dist/packem_shared/useVoiceAgent-w7fzAMSB.mjs +422 -0
- package/dist/packem_shared/wire-key-Bg8BJ8YM.mjs +101 -0
- package/dist/server.d.mts +29 -29
- package/dist/server.d.ts +29 -29
- package/dist/server.mjs +2 -2
- package/dist/upload.d.mts +2 -0
- package/dist/upload.d.ts +2 -0
- package/dist/upload.mjs +3 -0
- package/package.json +11 -5
- package/dist/packem_shared/hydratePreloaded-BlFL9FGq.mjs +0 -46
- package/dist/packem_shared/query-key-C5rufkEE.mjs +0 -21
- package/dist/packem_shared/query-options.d-D4okOpO8.d.mts +0 -38
- package/dist/packem_shared/query-options.d-D4okOpO8.d.ts +0 -38
- package/dist/packem_shared/useQuery-C5S0W-7K.mjs +0 -41
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { useLunora } from './LunoraProvider-BsuiW4Lk.mjs';
|
|
4
|
+
|
|
5
|
+
const TARGET_SAMPLE_RATE = 16e3;
|
|
6
|
+
const blockRms = (samples) => {
|
|
7
|
+
if (samples.length === 0) {
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
let sum = 0;
|
|
11
|
+
for (const sample of samples) {
|
|
12
|
+
sum += sample * sample;
|
|
13
|
+
}
|
|
14
|
+
return Math.sqrt(sum / samples.length);
|
|
15
|
+
};
|
|
16
|
+
const toPcm16 = (samples, inputSampleRate) => {
|
|
17
|
+
const ratio = inputSampleRate / TARGET_SAMPLE_RATE;
|
|
18
|
+
const outLength = ratio > 1 ? Math.floor(samples.length / ratio) : samples.length;
|
|
19
|
+
const buffer = new ArrayBuffer(outLength * 2);
|
|
20
|
+
const view = new DataView(buffer);
|
|
21
|
+
for (let index = 0; index < outLength; index += 1) {
|
|
22
|
+
const sample = samples[Math.floor(index * ratio)] ?? 0;
|
|
23
|
+
const clamped = Math.max(-1, Math.min(1, sample));
|
|
24
|
+
view.setInt16(index * 2, clamped < 0 ? clamped * 32768 : clamped * 32767, true);
|
|
25
|
+
}
|
|
26
|
+
return new Uint8Array(buffer);
|
|
27
|
+
};
|
|
28
|
+
const createBrowserMicrophone = async (config) => {
|
|
29
|
+
const media = globalThis;
|
|
30
|
+
const getUserMedia = media.navigator?.mediaDevices?.getUserMedia.bind(media.navigator.mediaDevices);
|
|
31
|
+
const AudioContextClass = media.AudioContext ?? media.webkitAudioContext;
|
|
32
|
+
if (!getUserMedia || !AudioContextClass) {
|
|
33
|
+
throw new Error("useVoiceAgent: microphone capture requires getUserMedia + AudioContext (no browser audio available)");
|
|
34
|
+
}
|
|
35
|
+
const stream = await getUserMedia({
|
|
36
|
+
audio: {
|
|
37
|
+
channelCount: 1,
|
|
38
|
+
echoCancellation: true,
|
|
39
|
+
noiseSuppression: true
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const context = new AudioContextClass();
|
|
43
|
+
const source = context.createMediaStreamSource(stream);
|
|
44
|
+
const processor = context.createScriptProcessor(4096, 1, 1);
|
|
45
|
+
let muted = false;
|
|
46
|
+
let sawSpeech = false;
|
|
47
|
+
let silentFor = 0;
|
|
48
|
+
let loudChunks = 0;
|
|
49
|
+
processor.onaudioprocess = (event) => {
|
|
50
|
+
const samples = event.inputBuffer.getChannelData(0);
|
|
51
|
+
const rms = muted ? 0 : blockRms(samples);
|
|
52
|
+
config.onLevel(rms);
|
|
53
|
+
if (muted) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
config.onAudio(toPcm16(samples, context.sampleRate));
|
|
57
|
+
if (config.isSpeaking()) {
|
|
58
|
+
loudChunks = rms >= config.interruptThreshold ? loudChunks + 1 : 0;
|
|
59
|
+
if (loudChunks >= config.interruptChunks) {
|
|
60
|
+
loudChunks = 0;
|
|
61
|
+
config.onInterrupt();
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
loudChunks = 0;
|
|
66
|
+
const chunkMs = samples.length / context.sampleRate * 1e3;
|
|
67
|
+
if (rms >= config.silenceThreshold) {
|
|
68
|
+
sawSpeech = true;
|
|
69
|
+
silentFor = 0;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (sawSpeech) {
|
|
73
|
+
silentFor += chunkMs;
|
|
74
|
+
if (silentFor >= config.silenceDurationMs) {
|
|
75
|
+
sawSpeech = false;
|
|
76
|
+
silentFor = 0;
|
|
77
|
+
config.onSilence();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
source.connect(processor);
|
|
82
|
+
processor.connect(context.destination);
|
|
83
|
+
return {
|
|
84
|
+
setMuted: (next) => {
|
|
85
|
+
muted = next;
|
|
86
|
+
},
|
|
87
|
+
stop: () => {
|
|
88
|
+
processor.disconnect();
|
|
89
|
+
source.disconnect();
|
|
90
|
+
for (const track of stream.getTracks()) {
|
|
91
|
+
track.stop();
|
|
92
|
+
}
|
|
93
|
+
void context.close();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const createBrowserSpeaker = () => {
|
|
98
|
+
const media = globalThis;
|
|
99
|
+
const AudioContextClass = media.AudioContext ?? media.webkitAudioContext;
|
|
100
|
+
if (!AudioContextClass) {
|
|
101
|
+
throw new Error("useVoiceAgent: audio playback requires AudioContext (no browser audio available)");
|
|
102
|
+
}
|
|
103
|
+
const context = new AudioContextClass();
|
|
104
|
+
const sources = /* @__PURE__ */ new Set();
|
|
105
|
+
let playHead = 0;
|
|
106
|
+
let chain = Promise.resolve();
|
|
107
|
+
let generation = 0;
|
|
108
|
+
const scheduleChunk = async (bytes, scheduledGeneration) => {
|
|
109
|
+
if (scheduledGeneration !== generation) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let decoded;
|
|
113
|
+
try {
|
|
114
|
+
decoded = await context.decodeAudioData(bytes.buffer);
|
|
115
|
+
} catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (scheduledGeneration !== generation) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const node = context.createBufferSource();
|
|
122
|
+
node.buffer = decoded;
|
|
123
|
+
node.connect(context.destination);
|
|
124
|
+
const startAt = Math.max(context.currentTime, playHead);
|
|
125
|
+
node.start(startAt);
|
|
126
|
+
playHead = startAt + decoded.duration;
|
|
127
|
+
sources.add(node);
|
|
128
|
+
node.onended = () => {
|
|
129
|
+
sources.delete(node);
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
const enqueue = (audio) => {
|
|
133
|
+
const bytes = Uint8Array.from(audio);
|
|
134
|
+
const scheduledGeneration = generation;
|
|
135
|
+
chain = chain.then(() => scheduleChunk(bytes, scheduledGeneration));
|
|
136
|
+
};
|
|
137
|
+
const interrupt = () => {
|
|
138
|
+
generation += 1;
|
|
139
|
+
for (const node of sources) {
|
|
140
|
+
try {
|
|
141
|
+
node.stop();
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
sources.clear();
|
|
146
|
+
playHead = context.currentTime;
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
enqueue,
|
|
150
|
+
interrupt,
|
|
151
|
+
stop: () => {
|
|
152
|
+
interrupt();
|
|
153
|
+
void context.close();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const WS_OPEN = 1;
|
|
159
|
+
const DEFAULT_SILENCE_THRESHOLD = 0.01;
|
|
160
|
+
const DEFAULT_SILENCE_DURATION_MS = 1200;
|
|
161
|
+
const DEFAULT_INTERRUPT_THRESHOLD = 0.15;
|
|
162
|
+
const DEFAULT_INTERRUPT_CHUNKS = 3;
|
|
163
|
+
const deriveWebSocketUrl = (url) => {
|
|
164
|
+
if (url.startsWith("https://")) {
|
|
165
|
+
return `wss://${url.slice("https://".length)}`;
|
|
166
|
+
}
|
|
167
|
+
if (url.startsWith("http://")) {
|
|
168
|
+
return `ws://${url.slice("http://".length)}`;
|
|
169
|
+
}
|
|
170
|
+
return url;
|
|
171
|
+
};
|
|
172
|
+
const agentNameFromReference = (voice) => {
|
|
173
|
+
const reference = voice["__lunoraRef"];
|
|
174
|
+
const withoutNamespace = reference.startsWith("agents:") ? reference.slice("agents:".length) : reference;
|
|
175
|
+
return withoutNamespace.endsWith("Voice") ? withoutNamespace.slice(0, -"Voice".length) : withoutNamespace;
|
|
176
|
+
};
|
|
177
|
+
const voiceSocketUrl = (baseUrl, agent, threadKey) => {
|
|
178
|
+
const base = deriveWebSocketUrl(baseUrl);
|
|
179
|
+
const trimmed = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
180
|
+
const search = new URLSearchParams({
|
|
181
|
+
threadKey
|
|
182
|
+
});
|
|
183
|
+
return `${trimmed}/_lunora/voice/${encodeURIComponent(agent)}?${search.toString()}`;
|
|
184
|
+
};
|
|
185
|
+
const useVoiceAgent = (options) => {
|
|
186
|
+
const {
|
|
187
|
+
createMicrophone = createBrowserMicrophone,
|
|
188
|
+
createSpeaker = createBrowserSpeaker,
|
|
189
|
+
createSocket,
|
|
190
|
+
interruptChunks = DEFAULT_INTERRUPT_CHUNKS,
|
|
191
|
+
interruptThreshold = DEFAULT_INTERRUPT_THRESHOLD,
|
|
192
|
+
silenceDurationMs = DEFAULT_SILENCE_DURATION_MS,
|
|
193
|
+
silenceThreshold = DEFAULT_SILENCE_THRESHOLD,
|
|
194
|
+
threadKey,
|
|
195
|
+
voice
|
|
196
|
+
} = options;
|
|
197
|
+
const client = useLunora();
|
|
198
|
+
const [status, setStatus] = useState("idle");
|
|
199
|
+
const [connected, setConnected] = useState(false);
|
|
200
|
+
const [transcript, setTranscript] = useState("");
|
|
201
|
+
const [interimTranscript, setInterimTranscript] = useState("");
|
|
202
|
+
const [audioLevel, setAudioLevel] = useState(0);
|
|
203
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
204
|
+
const [error, setError] = useState(void 0);
|
|
205
|
+
const connectionRef = useRef(void 0);
|
|
206
|
+
const startingRef = useRef(false);
|
|
207
|
+
const sendFrame = useCallback((frame) => {
|
|
208
|
+
const socket = connectionRef.current?.socket;
|
|
209
|
+
if (socket?.readyState === WS_OPEN) {
|
|
210
|
+
socket.send(JSON.stringify(frame));
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}, []);
|
|
215
|
+
const teardown = useCallback(() => {
|
|
216
|
+
const connection = connectionRef.current;
|
|
217
|
+
connectionRef.current = void 0;
|
|
218
|
+
if (connection) {
|
|
219
|
+
connection.microphone?.stop();
|
|
220
|
+
connection.speaker?.stop();
|
|
221
|
+
try {
|
|
222
|
+
connection.socket.close();
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
startingRef.current = false;
|
|
227
|
+
setConnected(false);
|
|
228
|
+
setStatus("idle");
|
|
229
|
+
setAudioLevel(0);
|
|
230
|
+
}, []);
|
|
231
|
+
const endCall = useCallback(() => {
|
|
232
|
+
teardown();
|
|
233
|
+
}, [teardown]);
|
|
234
|
+
const handleServerFrame = useCallback((frame_0) => {
|
|
235
|
+
const connection_0 = connectionRef.current;
|
|
236
|
+
switch (frame_0.type) {
|
|
237
|
+
case "assistant_delta": {
|
|
238
|
+
if (connection_0) {
|
|
239
|
+
connection_0.speaking = true;
|
|
240
|
+
}
|
|
241
|
+
setStatus("speaking");
|
|
242
|
+
setInterimTranscript((previous) => previous + frame_0.text);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "assistant_done": {
|
|
246
|
+
if (connection_0) {
|
|
247
|
+
connection_0.speaking = false;
|
|
248
|
+
}
|
|
249
|
+
setInterimTranscript(frame_0.text);
|
|
250
|
+
setStatus("listening");
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "error": {
|
|
254
|
+
if (connection_0) {
|
|
255
|
+
connection_0.speaking = false;
|
|
256
|
+
}
|
|
257
|
+
setError(new Error(frame_0.message));
|
|
258
|
+
setStatus("listening");
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case "interrupted": {
|
|
262
|
+
if (connection_0) {
|
|
263
|
+
connection_0.speaking = false;
|
|
264
|
+
connection_0.suppressAudio = false;
|
|
265
|
+
}
|
|
266
|
+
connection_0?.speaker?.interrupt();
|
|
267
|
+
setStatus("listening");
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
case "ready": {
|
|
271
|
+
if (connection_0) {
|
|
272
|
+
connection_0.audioFormat = frame_0.audioFormat;
|
|
273
|
+
connection_0.suppressAudio = false;
|
|
274
|
+
}
|
|
275
|
+
setConnected(true);
|
|
276
|
+
setStatus("listening");
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "user_transcript": {
|
|
280
|
+
if (connection_0) {
|
|
281
|
+
connection_0.suppressAudio = false;
|
|
282
|
+
}
|
|
283
|
+
setTranscript(frame_0.text);
|
|
284
|
+
setInterimTranscript("");
|
|
285
|
+
setStatus("thinking");
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}, []);
|
|
290
|
+
const handleAudioChunk = useCallback((audio) => {
|
|
291
|
+
const connection_1 = connectionRef.current;
|
|
292
|
+
if (!connection_1 || connection_1.suppressAudio) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
connection_1.speaker ??= createSpeaker({
|
|
296
|
+
audioFormat: connection_1.audioFormat
|
|
297
|
+
});
|
|
298
|
+
connection_1.speaking = true;
|
|
299
|
+
setStatus("speaking");
|
|
300
|
+
connection_1.speaker.enqueue(audio);
|
|
301
|
+
}, [createSpeaker]);
|
|
302
|
+
const startCall = useCallback(async () => {
|
|
303
|
+
if (connectionRef.current || startingRef.current) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
startingRef.current = true;
|
|
307
|
+
setError(void 0);
|
|
308
|
+
setTranscript("");
|
|
309
|
+
setInterimTranscript("");
|
|
310
|
+
try {
|
|
311
|
+
const url = voiceSocketUrl(client.url, agentNameFromReference(voice), threadKey);
|
|
312
|
+
const openSocket = createSocket ?? ((target) => {
|
|
313
|
+
const WebSocketImpl = client.getWebSocketImpl();
|
|
314
|
+
if (!WebSocketImpl) {
|
|
315
|
+
throw new Error("useVoiceAgent: no WebSocket implementation available (pass createSocket explicitly)");
|
|
316
|
+
}
|
|
317
|
+
return new WebSocketImpl(target);
|
|
318
|
+
});
|
|
319
|
+
const socket_0 = openSocket(url);
|
|
320
|
+
socket_0.binaryType = "arraybuffer";
|
|
321
|
+
const connection_2 = {
|
|
322
|
+
audioFormat: "mp3",
|
|
323
|
+
microphone: void 0,
|
|
324
|
+
socket: socket_0,
|
|
325
|
+
speaker: void 0,
|
|
326
|
+
speaking: false,
|
|
327
|
+
suppressAudio: false
|
|
328
|
+
};
|
|
329
|
+
connectionRef.current = connection_2;
|
|
330
|
+
socket_0.onmessage = (event) => {
|
|
331
|
+
if (typeof event.data === "string") {
|
|
332
|
+
try {
|
|
333
|
+
handleServerFrame(JSON.parse(event.data));
|
|
334
|
+
} catch {
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
handleAudioChunk(new Uint8Array(event.data));
|
|
339
|
+
};
|
|
340
|
+
socket_0.onerror = () => {
|
|
341
|
+
setError(new Error("useVoiceAgent: voice socket error"));
|
|
342
|
+
};
|
|
343
|
+
socket_0.onclose = () => {
|
|
344
|
+
if (connectionRef.current === connection_2) {
|
|
345
|
+
teardown();
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const microphone = await createMicrophone({
|
|
349
|
+
interruptChunks,
|
|
350
|
+
interruptThreshold,
|
|
351
|
+
isSpeaking: () => connectionRef.current?.speaking ?? false,
|
|
352
|
+
onAudio: (pcm) => {
|
|
353
|
+
if (socket_0.readyState === WS_OPEN) {
|
|
354
|
+
socket_0.send(pcm);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
onInterrupt: () => {
|
|
358
|
+
sendFrame({
|
|
359
|
+
type: "interrupt"
|
|
360
|
+
});
|
|
361
|
+
connectionRef.current?.speaker?.interrupt();
|
|
362
|
+
if (connectionRef.current) {
|
|
363
|
+
connectionRef.current.speaking = false;
|
|
364
|
+
connectionRef.current.suppressAudio = true;
|
|
365
|
+
}
|
|
366
|
+
setStatus("listening");
|
|
367
|
+
},
|
|
368
|
+
onLevel: setAudioLevel,
|
|
369
|
+
onSilence: () => {
|
|
370
|
+
sendFrame({
|
|
371
|
+
type: "commit"
|
|
372
|
+
});
|
|
373
|
+
setStatus("thinking");
|
|
374
|
+
},
|
|
375
|
+
silenceDurationMs,
|
|
376
|
+
silenceThreshold
|
|
377
|
+
});
|
|
378
|
+
if (connectionRef.current === connection_2) {
|
|
379
|
+
connection_2.microphone = microphone;
|
|
380
|
+
setIsMuted(false);
|
|
381
|
+
setStatus("listening");
|
|
382
|
+
} else {
|
|
383
|
+
microphone.stop();
|
|
384
|
+
}
|
|
385
|
+
} catch (error_) {
|
|
386
|
+
setError(error_ instanceof Error ? error_ : new Error(String(error_)));
|
|
387
|
+
teardown();
|
|
388
|
+
} finally {
|
|
389
|
+
startingRef.current = false;
|
|
390
|
+
}
|
|
391
|
+
}, [client, createMicrophone, createSocket, handleAudioChunk, handleServerFrame, interruptChunks, interruptThreshold, sendFrame, silenceDurationMs, silenceThreshold, teardown, threadKey, voice]);
|
|
392
|
+
const toggleMute = useCallback(() => {
|
|
393
|
+
const next = !isMuted;
|
|
394
|
+
connectionRef.current?.microphone?.setMuted(next);
|
|
395
|
+
setIsMuted(next);
|
|
396
|
+
return next;
|
|
397
|
+
}, [isMuted]);
|
|
398
|
+
const sendText = useCallback((text) => {
|
|
399
|
+
if (sendFrame({
|
|
400
|
+
text,
|
|
401
|
+
type: "text"
|
|
402
|
+
})) {
|
|
403
|
+
setStatus("thinking");
|
|
404
|
+
}
|
|
405
|
+
}, [sendFrame]);
|
|
406
|
+
useEffect(() => teardown, [teardown]);
|
|
407
|
+
return {
|
|
408
|
+
audioLevel,
|
|
409
|
+
connected,
|
|
410
|
+
endCall,
|
|
411
|
+
error,
|
|
412
|
+
interimTranscript,
|
|
413
|
+
isMuted,
|
|
414
|
+
sendText,
|
|
415
|
+
startCall,
|
|
416
|
+
status,
|
|
417
|
+
toggleMute,
|
|
418
|
+
transcript
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export { useVoiceAgent };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { s as stableStringify } from './stable-key-DePnevIy.mjs';
|
|
2
|
+
|
|
3
|
+
const toBase64 = (bytes) => {
|
|
4
|
+
let binary = "";
|
|
5
|
+
const chunk = 32768;
|
|
6
|
+
for (let index = 0; index < bytes.length; index += chunk) {
|
|
7
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + chunk));
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const TAG = "$lunora.wire$";
|
|
13
|
+
const MAX_DEPTH = 64;
|
|
14
|
+
const encodeWire = (value, depth = 0) => {
|
|
15
|
+
if (depth > MAX_DEPTH) {
|
|
16
|
+
throw new RangeError(`wire-codec: value nesting exceeds the ${MAX_DEPTH}-level limit`);
|
|
17
|
+
}
|
|
18
|
+
if (value === void 0) {
|
|
19
|
+
return [TAG, "undefined"];
|
|
20
|
+
}
|
|
21
|
+
if (value === null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const kind = typeof value;
|
|
25
|
+
if (kind === "bigint") {
|
|
26
|
+
return [TAG, "bigint", value.toString()];
|
|
27
|
+
}
|
|
28
|
+
if (kind === "number") {
|
|
29
|
+
const numeric = value;
|
|
30
|
+
if (Number.isNaN(numeric)) {
|
|
31
|
+
return [TAG, "nan"];
|
|
32
|
+
}
|
|
33
|
+
if (numeric === Infinity) {
|
|
34
|
+
return [TAG, "inf"];
|
|
35
|
+
}
|
|
36
|
+
if (numeric === -Infinity) {
|
|
37
|
+
return [TAG, "-inf"];
|
|
38
|
+
}
|
|
39
|
+
return numeric;
|
|
40
|
+
}
|
|
41
|
+
if (kind !== "object") {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (value instanceof Date) {
|
|
45
|
+
return [TAG, "date", encodeWire(value.getTime(), depth + 1)];
|
|
46
|
+
}
|
|
47
|
+
if (value instanceof Error) {
|
|
48
|
+
const error = value;
|
|
49
|
+
const properties = {};
|
|
50
|
+
for (const key of Object.keys(error)) {
|
|
51
|
+
if (error[key] !== void 0) {
|
|
52
|
+
properties[key] = encodeWire(error[key], depth + 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const encodedError = [TAG, "error", error.name, error.message, properties];
|
|
56
|
+
if (error.cause !== void 0) {
|
|
57
|
+
encodedError.push(encodeWire(error.cause, depth + 1));
|
|
58
|
+
}
|
|
59
|
+
return encodedError;
|
|
60
|
+
}
|
|
61
|
+
if (value instanceof URL) {
|
|
62
|
+
return [TAG, "url", value.href];
|
|
63
|
+
}
|
|
64
|
+
if (value instanceof Map) {
|
|
65
|
+
return [TAG, "map", [...value.entries()].map(([k, v]) => [encodeWire(k, depth + 1), encodeWire(v, depth + 1)])];
|
|
66
|
+
}
|
|
67
|
+
if (value instanceof Set) {
|
|
68
|
+
return [TAG, "set", [...value].map((item) => encodeWire(item, depth + 1))];
|
|
69
|
+
}
|
|
70
|
+
if (value instanceof ArrayBuffer) {
|
|
71
|
+
return [TAG, "bytes", toBase64(new Uint8Array(value)), "ArrayBuffer"];
|
|
72
|
+
}
|
|
73
|
+
if (ArrayBuffer.isView(value)) {
|
|
74
|
+
const view = value;
|
|
75
|
+
const ctorName = view.constructor.name;
|
|
76
|
+
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength);
|
|
77
|
+
return ctorName === "Uint8Array" ? [TAG, "bytes", toBase64(bytes)] : [TAG, "bytes", toBase64(bytes), ctorName];
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
const encoded = value.map((item) => encodeWire(item, depth + 1));
|
|
81
|
+
return encoded.length > 0 && encoded[0] === TAG ? [TAG, "arr", encoded] : encoded;
|
|
82
|
+
}
|
|
83
|
+
const proto = Object.getPrototypeOf(value);
|
|
84
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
85
|
+
const name = value.constructor?.name ?? "value";
|
|
86
|
+
throw new TypeError(`wire-codec: cannot encode a ${name} over the Lunora wire — only plain objects, arrays, and the supported built-ins (Date, Error, URL, Map, Set, ArrayBuffer/typed arrays, bigint) round-trip`);
|
|
87
|
+
}
|
|
88
|
+
const source = value;
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const key of Object.keys(source)) {
|
|
91
|
+
const field = source[key];
|
|
92
|
+
if (field !== void 0) {
|
|
93
|
+
result[key] = encodeWire(field, depth + 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const stableWireKey = (value) => stableStringify(encodeWire(value));
|
|
100
|
+
|
|
101
|
+
export { stableWireKey as s };
|
package/dist/server.d.mts
CHANGED
|
@@ -4,20 +4,20 @@ import { ServerClientOptions } from '@lunora/client/ssr';
|
|
|
4
4
|
export { type AuthLike, type HeadersSource, type ServerClientOptions, type ServerSession, createServerClient, deserializePreloaded, getServerSession, serializePreloaded } from '@lunora/client/ssr';
|
|
5
5
|
import { QueryClient } from '@tanstack/react-query';
|
|
6
6
|
export { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
|
7
|
-
export { type L as LunoraQueryOptions, l as lunoraQueryOptions } from "./packem_shared/query-options.d-
|
|
7
|
+
export { type L as LunoraQueryOptions, l as lunoraQueryOptions } from "./packem_shared/query-options.d-CdgGQ9s4.mjs";
|
|
8
8
|
/**
|
|
9
|
-
* Run a query on the server and seed `queryClient` with the result under the
|
|
10
|
-
* same key the client hooks use (see `lunoraQueryKey`), so a later
|
|
11
|
-
* `useQuery(fn, args)` reads it straight from the hydrated cache — no loading
|
|
12
|
-
* flash, no duplicate fetch on the client.
|
|
13
|
-
*
|
|
14
|
-
* Pair it with TanStack's `dehydrate` + `HydrationBoundary` (both re-exported
|
|
15
|
-
* from this module): prefetch into a fresh `QueryClient`, `dehydrate` it, wrap
|
|
16
|
-
* the client subtree in `HydrationBoundary`, and the client hooks pick the value
|
|
17
|
-
* up from cache. Errors propagate — wrap the call if you'd rather render a
|
|
18
|
-
* fallback than fail the server render. Drop the `await` for fire-and-forget
|
|
19
|
-
* prefetch when you don't need the data present on the very first paint.
|
|
20
|
-
*/
|
|
9
|
+
* Run a query on the server and seed `queryClient` with the result under the
|
|
10
|
+
* same key the client hooks use (see `lunoraQueryKey`), so a later
|
|
11
|
+
* `useQuery(fn, args)` reads it straight from the hydrated cache — no loading
|
|
12
|
+
* flash, no duplicate fetch on the client.
|
|
13
|
+
*
|
|
14
|
+
* Pair it with TanStack's `dehydrate` + `HydrationBoundary` (both re-exported
|
|
15
|
+
* from this module): prefetch into a fresh `QueryClient`, `dehydrate` it, wrap
|
|
16
|
+
* the client subtree in `HydrationBoundary`, and the client hooks pick the value
|
|
17
|
+
* up from cache. Errors propagate — wrap the call if you'd rather render a
|
|
18
|
+
* fallback than fail the server render. Drop the `await` for fire-and-forget
|
|
19
|
+
* prefetch when you don't need the data present on the very first paint.
|
|
20
|
+
*/
|
|
21
21
|
declare const prefetchQuery: <F extends FunctionReference>(queryClient: QueryClient, client: LunoraClient, function_: F, args: ArgsOf<F>, options?: {
|
|
22
22
|
shardKey?: string;
|
|
23
23
|
}) => Promise<void>;
|
|
@@ -27,25 +27,25 @@ interface ServerCallOptions {
|
|
|
27
27
|
shardKey?: string;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Run a query once on the server and return its result — the standalone
|
|
31
|
-
* counterpart to `prefetchQuery`/`preloadQuery` for when you just want the data
|
|
32
|
-
* inline in a Server Component (e.g. to compute metadata or branch on a value)
|
|
33
|
-
* rather than seed a cache or hand a `Preloaded` to the client.
|
|
34
|
-
*
|
|
35
|
-
* Builds a fresh request-scoped client per call (see `createServerClient`), so
|
|
36
|
-
* pass `token` to run as the signed-in user. For several reads in one request,
|
|
37
|
-
* prefer holding one `createServerClient` and calling `.query()` yourself to
|
|
38
|
-
* avoid rebuilding the transport each time. Errors propagate.
|
|
39
|
-
*/
|
|
30
|
+
* Run a query once on the server and return its result — the standalone
|
|
31
|
+
* counterpart to `prefetchQuery`/`preloadQuery` for when you just want the data
|
|
32
|
+
* inline in a Server Component (e.g. to compute metadata or branch on a value)
|
|
33
|
+
* rather than seed a cache or hand a `Preloaded` to the client.
|
|
34
|
+
*
|
|
35
|
+
* Builds a fresh request-scoped client per call (see `createServerClient`), so
|
|
36
|
+
* pass `token` to run as the signed-in user. For several reads in one request,
|
|
37
|
+
* prefer holding one `createServerClient` and calling `.query()` yourself to
|
|
38
|
+
* avoid rebuilding the transport each time. Errors propagate.
|
|
39
|
+
*/
|
|
40
40
|
declare const fetchQuery: <F extends FunctionReference>(options: ServerClientOptions, function_: F, args: ArgsOf<F>, callOptions?: ServerCallOptions) => Promise<ReturnOf<F>>;
|
|
41
41
|
/**
|
|
42
|
-
* Run a mutation once on the server and return its result. Server-side calls go
|
|
43
|
-
* straight over HTTP RPC — the offline queue and optimistic-update machinery are
|
|
44
|
-
* client-only and never engage here. Errors propagate.
|
|
45
|
-
*/
|
|
42
|
+
* Run a mutation once on the server and return its result. Server-side calls go
|
|
43
|
+
* straight over HTTP RPC — the offline queue and optimistic-update machinery are
|
|
44
|
+
* client-only and never engage here. Errors propagate.
|
|
45
|
+
*/
|
|
46
46
|
declare const fetchMutation: <F extends FunctionReference>(options: ServerClientOptions, function_: F, args: ArgsOf<F>, callOptions?: ServerCallOptions) => Promise<ReturnOf<F>>;
|
|
47
47
|
/**
|
|
48
|
-
* Run an action once on the server and return its result. Errors propagate.
|
|
49
|
-
*/
|
|
48
|
+
* Run an action once on the server and return its result. Errors propagate.
|
|
49
|
+
*/
|
|
50
50
|
declare const fetchAction: <F extends FunctionReference>(options: ServerClientOptions, function_: F, args: ArgsOf<F>, callOptions?: ServerCallOptions) => Promise<ReturnOf<F>>;
|
|
51
51
|
export { ServerCallOptions, fetchAction, fetchMutation, fetchQuery, prefetchQuery };
|
package/dist/server.d.ts
CHANGED
|
@@ -4,20 +4,20 @@ import { ServerClientOptions } from '@lunora/client/ssr';
|
|
|
4
4
|
export { type AuthLike, type HeadersSource, type ServerClientOptions, type ServerSession, createServerClient, deserializePreloaded, getServerSession, serializePreloaded } from '@lunora/client/ssr';
|
|
5
5
|
import { QueryClient } from '@tanstack/react-query';
|
|
6
6
|
export { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
|
7
|
-
export { type L as LunoraQueryOptions, l as lunoraQueryOptions } from "./packem_shared/query-options.d-
|
|
7
|
+
export { type L as LunoraQueryOptions, l as lunoraQueryOptions } from "./packem_shared/query-options.d-CdgGQ9s4.js";
|
|
8
8
|
/**
|
|
9
|
-
* Run a query on the server and seed `queryClient` with the result under the
|
|
10
|
-
* same key the client hooks use (see `lunoraQueryKey`), so a later
|
|
11
|
-
* `useQuery(fn, args)` reads it straight from the hydrated cache — no loading
|
|
12
|
-
* flash, no duplicate fetch on the client.
|
|
13
|
-
*
|
|
14
|
-
* Pair it with TanStack's `dehydrate` + `HydrationBoundary` (both re-exported
|
|
15
|
-
* from this module): prefetch into a fresh `QueryClient`, `dehydrate` it, wrap
|
|
16
|
-
* the client subtree in `HydrationBoundary`, and the client hooks pick the value
|
|
17
|
-
* up from cache. Errors propagate — wrap the call if you'd rather render a
|
|
18
|
-
* fallback than fail the server render. Drop the `await` for fire-and-forget
|
|
19
|
-
* prefetch when you don't need the data present on the very first paint.
|
|
20
|
-
*/
|
|
9
|
+
* Run a query on the server and seed `queryClient` with the result under the
|
|
10
|
+
* same key the client hooks use (see `lunoraQueryKey`), so a later
|
|
11
|
+
* `useQuery(fn, args)` reads it straight from the hydrated cache — no loading
|
|
12
|
+
* flash, no duplicate fetch on the client.
|
|
13
|
+
*
|
|
14
|
+
* Pair it with TanStack's `dehydrate` + `HydrationBoundary` (both re-exported
|
|
15
|
+
* from this module): prefetch into a fresh `QueryClient`, `dehydrate` it, wrap
|
|
16
|
+
* the client subtree in `HydrationBoundary`, and the client hooks pick the value
|
|
17
|
+
* up from cache. Errors propagate — wrap the call if you'd rather render a
|
|
18
|
+
* fallback than fail the server render. Drop the `await` for fire-and-forget
|
|
19
|
+
* prefetch when you don't need the data present on the very first paint.
|
|
20
|
+
*/
|
|
21
21
|
declare const prefetchQuery: <F extends FunctionReference>(queryClient: QueryClient, client: LunoraClient, function_: F, args: ArgsOf<F>, options?: {
|
|
22
22
|
shardKey?: string;
|
|
23
23
|
}) => Promise<void>;
|
|
@@ -27,25 +27,25 @@ interface ServerCallOptions {
|
|
|
27
27
|
shardKey?: string;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Run a query once on the server and return its result — the standalone
|
|
31
|
-
* counterpart to `prefetchQuery`/`preloadQuery` for when you just want the data
|
|
32
|
-
* inline in a Server Component (e.g. to compute metadata or branch on a value)
|
|
33
|
-
* rather than seed a cache or hand a `Preloaded` to the client.
|
|
34
|
-
*
|
|
35
|
-
* Builds a fresh request-scoped client per call (see `createServerClient`), so
|
|
36
|
-
* pass `token` to run as the signed-in user. For several reads in one request,
|
|
37
|
-
* prefer holding one `createServerClient` and calling `.query()` yourself to
|
|
38
|
-
* avoid rebuilding the transport each time. Errors propagate.
|
|
39
|
-
*/
|
|
30
|
+
* Run a query once on the server and return its result — the standalone
|
|
31
|
+
* counterpart to `prefetchQuery`/`preloadQuery` for when you just want the data
|
|
32
|
+
* inline in a Server Component (e.g. to compute metadata or branch on a value)
|
|
33
|
+
* rather than seed a cache or hand a `Preloaded` to the client.
|
|
34
|
+
*
|
|
35
|
+
* Builds a fresh request-scoped client per call (see `createServerClient`), so
|
|
36
|
+
* pass `token` to run as the signed-in user. For several reads in one request,
|
|
37
|
+
* prefer holding one `createServerClient` and calling `.query()` yourself to
|
|
38
|
+
* avoid rebuilding the transport each time. Errors propagate.
|
|
39
|
+
*/
|
|
40
40
|
declare const fetchQuery: <F extends FunctionReference>(options: ServerClientOptions, function_: F, args: ArgsOf<F>, callOptions?: ServerCallOptions) => Promise<ReturnOf<F>>;
|
|
41
41
|
/**
|
|
42
|
-
* Run a mutation once on the server and return its result. Server-side calls go
|
|
43
|
-
* straight over HTTP RPC — the offline queue and optimistic-update machinery are
|
|
44
|
-
* client-only and never engage here. Errors propagate.
|
|
45
|
-
*/
|
|
42
|
+
* Run a mutation once on the server and return its result. Server-side calls go
|
|
43
|
+
* straight over HTTP RPC — the offline queue and optimistic-update machinery are
|
|
44
|
+
* client-only and never engage here. Errors propagate.
|
|
45
|
+
*/
|
|
46
46
|
declare const fetchMutation: <F extends FunctionReference>(options: ServerClientOptions, function_: F, args: ArgsOf<F>, callOptions?: ServerCallOptions) => Promise<ReturnOf<F>>;
|
|
47
47
|
/**
|
|
48
|
-
* Run an action once on the server and return its result. Errors propagate.
|
|
49
|
-
*/
|
|
48
|
+
* Run an action once on the server and return its result. Errors propagate.
|
|
49
|
+
*/
|
|
50
50
|
declare const fetchAction: <F extends FunctionReference>(options: ServerClientOptions, function_: F, args: ArgsOf<F>, callOptions?: ServerCallOptions) => Promise<ReturnOf<F>>;
|
|
51
51
|
export { ServerCallOptions, fetchAction, fetchMutation, fetchQuery, prefetchQuery };
|
package/dist/server.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServerClient } from '@lunora/client/ssr';
|
|
2
2
|
export { createServerClient, deserializePreloaded, getServerSession, serializePreloaded } from '@lunora/client/ssr';
|
|
3
|
-
import { l as lunoraQueryKey } from './packem_shared/query-key-
|
|
4
|
-
export { lunoraQueryOptions } from './packem_shared/lunoraQueryOptions-
|
|
3
|
+
import { l as lunoraQueryKey } from './packem_shared/query-key-LGnArBTB.mjs';
|
|
4
|
+
export { lunoraQueryOptions } from './packem_shared/lunoraQueryOptions-CefbPBId.mjs';
|
|
5
5
|
export { preloadQuery, preloadedQueryResult } from '@lunora/client';
|
|
6
6
|
export { HydrationBoundary, dehydrate } from '@tanstack/react-query';
|
|
7
7
|
|