@personaai/react 0.6.0 → 0.7.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 +73 -9
- package/dist/index.cjs +462 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -2
- package/dist/index.d.ts +74 -2
- package/dist/index.js +460 -31
- package/dist/index.js.map +1 -1
- package/package.json +82 -82
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @personaai/react
|
|
|
13
13
|
## Quickstart
|
|
14
14
|
|
|
15
15
|
```tsx
|
|
16
|
-
import { PersonaProvider, useChat } from
|
|
16
|
+
import { PersonaProvider, useChat } from "@personaai/react";
|
|
17
17
|
|
|
18
18
|
function App() {
|
|
19
19
|
return (
|
|
@@ -46,14 +46,78 @@ function Chat() {
|
|
|
46
46
|
|
|
47
47
|
## Hooks
|
|
48
48
|
|
|
49
|
-
| Hook
|
|
50
|
-
|
|
|
51
|
-
| `useChat`
|
|
52
|
-
| `
|
|
53
|
-
| `
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
49
|
+
| Hook | Purpose |
|
|
50
|
+
| --------------- | -------------------------------------------------------------------------- |
|
|
51
|
+
| `useChat` | Streaming chat — messages, send, stop, reload, interrupts, workspace files |
|
|
52
|
+
| `useVoice` | Real-time voice calls (Gemini Live) — start/stop/mute, live transcript, tool calls |
|
|
53
|
+
| `useThreads` | Thread CRUD — list, create, delete, rename, reset, archive |
|
|
54
|
+
| `useFiles` | Upload management — list, upload, delete |
|
|
55
|
+
| `useMemory` | Persistent memory — read, write, delete |
|
|
56
|
+
| `useAgents` | Agent discovery — list available agents |
|
|
57
|
+
| `useConnection` | Health check — backend connectivity status |
|
|
58
|
+
|
|
59
|
+
## Voice
|
|
60
|
+
|
|
61
|
+
Real-time voice calls with an Agent, powered by Gemini Live. Requires your backend to be running
|
|
62
|
+
`@personaai/runtime@^0.7.0` (or a framework adapter on top of it) so `POST /voice/sessions` exists
|
|
63
|
+
to mint tickets — see that package's README for why the actual call bypasses your backend and
|
|
64
|
+
goes straight to Persona instead of relaying through it like `useChat` does.
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { useVoice } from "@personaai/react";
|
|
68
|
+
|
|
69
|
+
function VoiceCall() {
|
|
70
|
+
const { state, transcript, partial, toolCalls, start, stop, mute, isMuted } = useVoice();
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div>
|
|
74
|
+
<p>State: {state}</p>
|
|
75
|
+
{state === "idle" ? (
|
|
76
|
+
<button onClick={start}>Start call</button>
|
|
77
|
+
) : (
|
|
78
|
+
<>
|
|
79
|
+
<button onClick={() => mute(!isMuted)}>{isMuted ? "Unmute" : "Mute"}</button>
|
|
80
|
+
<button onClick={stop}>End call</button>
|
|
81
|
+
</>
|
|
82
|
+
)}
|
|
83
|
+
{transcript.map((line) => (
|
|
84
|
+
<p key={line.id}>
|
|
85
|
+
<b>{line.speaker}:</b> {line.text}
|
|
86
|
+
</p>
|
|
87
|
+
))}
|
|
88
|
+
{partial && <p style={{ opacity: 0.6 }}>{partial.text}</p>}
|
|
89
|
+
</div>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Requires a browser with `AudioWorklet` support (every current evergreen browser). Needs an HTTPS
|
|
95
|
+
page in production — browsers refuse an insecure `ws://` connection from an `https:` page.
|
|
96
|
+
|
|
97
|
+
## Devtools
|
|
98
|
+
|
|
99
|
+
Floating panel for local debugging — inspect hooks, messages and threads with zero runtime changes. Dev-only, not bundled to production unless imported.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm install -D @personaai/devtools
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { PersonaDevtools } from "@personaai/devtools/react";
|
|
107
|
+
import { useChat, useThreads } from "@personaai/react";
|
|
108
|
+
|
|
109
|
+
function Devtools() {
|
|
110
|
+
const { threads } = useThreads();
|
|
111
|
+
const { messages, files, todos } = useChat();
|
|
112
|
+
return <PersonaDevtools clientState={{ threads, messages, files, todos }} />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
process.env.NODE_ENV === "development" && <Devtools />;
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
See `@personaai/devtools` `sdk/devtools/README.md` for `core`/`react`/`nextjs` subpaths and `baseUrl` polling option.
|
|
57
121
|
|
|
58
122
|
## Full documentation
|
|
59
123
|
|
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,8 @@ __export(index_exports, {
|
|
|
36
36
|
useMcpConnections: () => useMcpConnections,
|
|
37
37
|
useMemory: () => useMemory,
|
|
38
38
|
usePersonaContext: () => usePersonaContext,
|
|
39
|
-
useThreads: () => useThreads
|
|
39
|
+
useThreads: () => useThreads,
|
|
40
|
+
useVoice: () => useVoice
|
|
40
41
|
});
|
|
41
42
|
module.exports = __toCommonJS(index_exports);
|
|
42
43
|
|
|
@@ -1252,15 +1253,443 @@ function useThreads(autoFetch = true) {
|
|
|
1252
1253
|
};
|
|
1253
1254
|
}
|
|
1254
1255
|
|
|
1255
|
-
// src/hooks/
|
|
1256
|
+
// src/hooks/useVoice.ts
|
|
1256
1257
|
var import_react5 = require("react");
|
|
1258
|
+
|
|
1259
|
+
// src/hooks/voiceWorklets.ts
|
|
1260
|
+
var RECORDER_WORKLET_SOURCE = `
|
|
1261
|
+
class PCMRecorderProcessor extends AudioWorkletProcessor {
|
|
1262
|
+
constructor() {
|
|
1263
|
+
super();
|
|
1264
|
+
this._buffer = [];
|
|
1265
|
+
this._chunkSamples = 320; // 20ms @ 16kHz
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
process(inputs) {
|
|
1269
|
+
const input = inputs[0];
|
|
1270
|
+
const channel = input && input[0];
|
|
1271
|
+
if (channel) {
|
|
1272
|
+
for (let i = 0; i < channel.length; i++) {
|
|
1273
|
+
this._buffer.push(channel[i]);
|
|
1274
|
+
}
|
|
1275
|
+
while (this._buffer.length >= this._chunkSamples) {
|
|
1276
|
+
const chunk = this._buffer.splice(0, this._chunkSamples);
|
|
1277
|
+
const int16 = new Int16Array(chunk.length);
|
|
1278
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
1279
|
+
const s = Math.max(-1, Math.min(1, chunk[i]));
|
|
1280
|
+
int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
|
1281
|
+
}
|
|
1282
|
+
this.port.postMessage(int16.buffer, [int16.buffer]);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
registerProcessor('pcm-recorder-processor', PCMRecorderProcessor);
|
|
1290
|
+
`;
|
|
1291
|
+
var PLAYER_WORKLET_SOURCE = `
|
|
1292
|
+
class PCMPlayerProcessor extends AudioWorkletProcessor {
|
|
1293
|
+
constructor() {
|
|
1294
|
+
super();
|
|
1295
|
+
this._queue = [];
|
|
1296
|
+
this._readOffset = 0;
|
|
1297
|
+
this.port.onmessage = (event) => {
|
|
1298
|
+
const msg = event.data;
|
|
1299
|
+
if (msg && msg.type === 'flush') {
|
|
1300
|
+
this._queue = [];
|
|
1301
|
+
this._readOffset = 0;
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const int16 = new Int16Array(msg);
|
|
1305
|
+
const float32 = new Float32Array(int16.length);
|
|
1306
|
+
for (let i = 0; i < int16.length; i++) {
|
|
1307
|
+
float32[i] = int16[i] / 0x8000;
|
|
1308
|
+
}
|
|
1309
|
+
this._queue.push(float32);
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
process(_inputs, outputs) {
|
|
1314
|
+
const output = outputs[0][0];
|
|
1315
|
+
let outIdx = 0;
|
|
1316
|
+
while (outIdx < output.length) {
|
|
1317
|
+
if (this._queue.length === 0) {
|
|
1318
|
+
output[outIdx++] = 0;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const current = this._queue[0];
|
|
1322
|
+
const remaining = current.length - this._readOffset;
|
|
1323
|
+
const toCopy = Math.min(remaining, output.length - outIdx);
|
|
1324
|
+
output.set(current.subarray(this._readOffset, this._readOffset + toCopy), outIdx);
|
|
1325
|
+
outIdx += toCopy;
|
|
1326
|
+
this._readOffset += toCopy;
|
|
1327
|
+
if (this._readOffset >= current.length) {
|
|
1328
|
+
this._queue.shift();
|
|
1329
|
+
this._readOffset = 0;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return true;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
registerProcessor('pcm-player-processor', PCMPlayerProcessor);
|
|
1337
|
+
`;
|
|
1338
|
+
function toModuleUrl(source) {
|
|
1339
|
+
const blob = new Blob([source], { type: "application/javascript" });
|
|
1340
|
+
return URL.createObjectURL(blob);
|
|
1341
|
+
}
|
|
1342
|
+
function recorderWorkletUrl() {
|
|
1343
|
+
return toModuleUrl(RECORDER_WORKLET_SOURCE);
|
|
1344
|
+
}
|
|
1345
|
+
function playerWorkletUrl() {
|
|
1346
|
+
return toModuleUrl(PLAYER_WORKLET_SOURCE);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/hooks/useVoice.ts
|
|
1350
|
+
function useVoice(options = {}) {
|
|
1351
|
+
const { defaultAgentId, fetchWithAuth, logger } = usePersonaContext();
|
|
1352
|
+
const voiceLogger = (0, import_react5.useMemo)(() => logger.child("voice"), [logger]);
|
|
1353
|
+
const agentId = options.agentId || defaultAgentId;
|
|
1354
|
+
const threadId = options.threadId;
|
|
1355
|
+
const [state, setState] = (0, import_react5.useState)("idle");
|
|
1356
|
+
const [isMuted, setIsMuted] = (0, import_react5.useState)(false);
|
|
1357
|
+
const [transcript, setTranscript] = (0, import_react5.useState)(
|
|
1358
|
+
[]
|
|
1359
|
+
);
|
|
1360
|
+
const [partial, setPartial] = (0, import_react5.useState)(
|
|
1361
|
+
null
|
|
1362
|
+
);
|
|
1363
|
+
const [toolCalls, setToolCalls] = (0, import_react5.useState)([]);
|
|
1364
|
+
const [error, setError] = (0, import_react5.useState)(null);
|
|
1365
|
+
const [endReason, setEndReason] = (0, import_react5.useState)(
|
|
1366
|
+
null
|
|
1367
|
+
);
|
|
1368
|
+
const wsRef = (0, import_react5.useRef)(null);
|
|
1369
|
+
const streamRef = (0, import_react5.useRef)(null);
|
|
1370
|
+
const inputCtxRef = (0, import_react5.useRef)(null);
|
|
1371
|
+
const outputCtxRef = (0, import_react5.useRef)(null);
|
|
1372
|
+
const recorderNodeRef = (0, import_react5.useRef)(null);
|
|
1373
|
+
const playerNodeRef = (0, import_react5.useRef)(null);
|
|
1374
|
+
const acceptedTurnSeqRef = (0, import_react5.useRef)(0);
|
|
1375
|
+
const mountedRef = (0, import_react5.useRef)(true);
|
|
1376
|
+
(0, import_react5.useEffect)(() => {
|
|
1377
|
+
mountedRef.current = true;
|
|
1378
|
+
return () => {
|
|
1379
|
+
mountedRef.current = false;
|
|
1380
|
+
};
|
|
1381
|
+
}, []);
|
|
1382
|
+
const teardownAudio = (0, import_react5.useCallback)(() => {
|
|
1383
|
+
try {
|
|
1384
|
+
streamRef.current?.getTracks().forEach((track) => track.stop());
|
|
1385
|
+
} catch {
|
|
1386
|
+
}
|
|
1387
|
+
streamRef.current = null;
|
|
1388
|
+
try {
|
|
1389
|
+
recorderNodeRef.current?.disconnect();
|
|
1390
|
+
} catch {
|
|
1391
|
+
}
|
|
1392
|
+
recorderNodeRef.current = null;
|
|
1393
|
+
try {
|
|
1394
|
+
playerNodeRef.current?.disconnect();
|
|
1395
|
+
} catch {
|
|
1396
|
+
}
|
|
1397
|
+
playerNodeRef.current = null;
|
|
1398
|
+
try {
|
|
1399
|
+
void inputCtxRef.current?.close();
|
|
1400
|
+
} catch {
|
|
1401
|
+
}
|
|
1402
|
+
inputCtxRef.current = null;
|
|
1403
|
+
try {
|
|
1404
|
+
void outputCtxRef.current?.close();
|
|
1405
|
+
} catch {
|
|
1406
|
+
}
|
|
1407
|
+
outputCtxRef.current = null;
|
|
1408
|
+
}, []);
|
|
1409
|
+
const stop = (0, import_react5.useCallback)(() => {
|
|
1410
|
+
voiceLogger.debug("stop() called");
|
|
1411
|
+
try {
|
|
1412
|
+
wsRef.current?.close(1e3, "client_stop");
|
|
1413
|
+
} catch {
|
|
1414
|
+
}
|
|
1415
|
+
wsRef.current = null;
|
|
1416
|
+
teardownAudio();
|
|
1417
|
+
if (mountedRef.current) setState("idle");
|
|
1418
|
+
}, [teardownAudio, voiceLogger]);
|
|
1419
|
+
(0, import_react5.useEffect)(() => stop, [stop]);
|
|
1420
|
+
const startCapture = (0, import_react5.useCallback)(async (inputSampleRate) => {
|
|
1421
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1422
|
+
audio: {
|
|
1423
|
+
echoCancellation: true,
|
|
1424
|
+
noiseSuppression: true,
|
|
1425
|
+
autoGainControl: true,
|
|
1426
|
+
channelCount: 1
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
streamRef.current = stream;
|
|
1430
|
+
const inputCtx = new AudioContext({ sampleRate: inputSampleRate });
|
|
1431
|
+
inputCtxRef.current = inputCtx;
|
|
1432
|
+
await inputCtx.audioWorklet.addModule(recorderWorkletUrl());
|
|
1433
|
+
const source = inputCtx.createMediaStreamSource(stream);
|
|
1434
|
+
const recorderNode = new AudioWorkletNode(
|
|
1435
|
+
inputCtx,
|
|
1436
|
+
"pcm-recorder-processor"
|
|
1437
|
+
);
|
|
1438
|
+
recorderNode.port.onmessage = (event) => {
|
|
1439
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
1440
|
+
wsRef.current.send(event.data);
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
recorderNodeRef.current = recorderNode;
|
|
1444
|
+
source.connect(recorderNode);
|
|
1445
|
+
}, []);
|
|
1446
|
+
const setupPlayback = (0, import_react5.useCallback)(async (outputSampleRate) => {
|
|
1447
|
+
const outputCtx = new AudioContext({ sampleRate: outputSampleRate });
|
|
1448
|
+
outputCtxRef.current = outputCtx;
|
|
1449
|
+
await outputCtx.audioWorklet.addModule(playerWorkletUrl());
|
|
1450
|
+
const playerNode = new AudioWorkletNode(outputCtx, "pcm-player-processor");
|
|
1451
|
+
playerNode.connect(outputCtx.destination);
|
|
1452
|
+
playerNodeRef.current = playerNode;
|
|
1453
|
+
}, []);
|
|
1454
|
+
const upsertToolCall = (0, import_react5.useCallback)(
|
|
1455
|
+
(id, patch) => {
|
|
1456
|
+
setToolCalls((prev) => {
|
|
1457
|
+
const idx = prev.findIndex((t) => t.id === id);
|
|
1458
|
+
if (idx === -1) return [...prev, { id, status: "running", ...patch }];
|
|
1459
|
+
const next = [...prev];
|
|
1460
|
+
next[idx] = { ...next[idx], ...patch };
|
|
1461
|
+
return next;
|
|
1462
|
+
});
|
|
1463
|
+
},
|
|
1464
|
+
[]
|
|
1465
|
+
);
|
|
1466
|
+
const handleTranscript = (0, import_react5.useCallback)(
|
|
1467
|
+
(value) => {
|
|
1468
|
+
const { speaker, text, isFinal } = value;
|
|
1469
|
+
if (isFinal) {
|
|
1470
|
+
setPartial((prev) => prev?.speaker === speaker ? null : prev);
|
|
1471
|
+
setTranscript((prev) => [
|
|
1472
|
+
...prev,
|
|
1473
|
+
{ id: `${speaker}-${prev.length}`, speaker, text }
|
|
1474
|
+
]);
|
|
1475
|
+
} else {
|
|
1476
|
+
setPartial({ id: "partial", speaker, text });
|
|
1477
|
+
}
|
|
1478
|
+
},
|
|
1479
|
+
[]
|
|
1480
|
+
);
|
|
1481
|
+
const handleCustomEvent = (0, import_react5.useCallback)(
|
|
1482
|
+
(name, value) => {
|
|
1483
|
+
switch (name) {
|
|
1484
|
+
case "voice_session_ready":
|
|
1485
|
+
voiceLogger.debug("voice_session_ready", value);
|
|
1486
|
+
Promise.all([
|
|
1487
|
+
setupPlayback(value.outputSampleRate),
|
|
1488
|
+
startCapture(value.inputSampleRate)
|
|
1489
|
+
]).then(() => {
|
|
1490
|
+
if (mountedRef.current) setState("listening");
|
|
1491
|
+
}).catch((err) => {
|
|
1492
|
+
voiceLogger.error("audio graph setup failed", {
|
|
1493
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1494
|
+
});
|
|
1495
|
+
if (mountedRef.current) {
|
|
1496
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
1497
|
+
setState("error");
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1500
|
+
break;
|
|
1501
|
+
case "voice_activity":
|
|
1502
|
+
if (value.speaker === "user" && value.state === "start") {
|
|
1503
|
+
setState("listening");
|
|
1504
|
+
} else if (value.speaker === "user" && value.state === "end") {
|
|
1505
|
+
setState("thinking");
|
|
1506
|
+
} else if (value.speaker === "agent" && value.state === "start") {
|
|
1507
|
+
setState("speaking");
|
|
1508
|
+
} else if (value.speaker === "agent" && value.state === "end") {
|
|
1509
|
+
setState("listening");
|
|
1510
|
+
}
|
|
1511
|
+
break;
|
|
1512
|
+
case "voice_transcript":
|
|
1513
|
+
handleTranscript(
|
|
1514
|
+
value
|
|
1515
|
+
);
|
|
1516
|
+
break;
|
|
1517
|
+
case "voice_interrupted":
|
|
1518
|
+
acceptedTurnSeqRef.current = value.turnSeq;
|
|
1519
|
+
playerNodeRef.current?.port.postMessage({ type: "flush" });
|
|
1520
|
+
break;
|
|
1521
|
+
case "voice_session_resumed":
|
|
1522
|
+
voiceLogger.info("session transparently resumed");
|
|
1523
|
+
break;
|
|
1524
|
+
case "voice_session_ended":
|
|
1525
|
+
voiceLogger.info("session ended", { reason: value.reason });
|
|
1526
|
+
setState("ended");
|
|
1527
|
+
setEndReason(value.reason ?? null);
|
|
1528
|
+
teardownAudio();
|
|
1529
|
+
try {
|
|
1530
|
+
wsRef.current?.close();
|
|
1531
|
+
} catch {
|
|
1532
|
+
}
|
|
1533
|
+
wsRef.current = null;
|
|
1534
|
+
break;
|
|
1535
|
+
default:
|
|
1536
|
+
break;
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
[handleTranscript, setupPlayback, startCapture, teardownAudio, voiceLogger]
|
|
1540
|
+
);
|
|
1541
|
+
const handleMessage = (0, import_react5.useCallback)(
|
|
1542
|
+
(event) => {
|
|
1543
|
+
if (typeof event.data !== "string") {
|
|
1544
|
+
const buf = event.data;
|
|
1545
|
+
const view = new DataView(buf);
|
|
1546
|
+
const turnSeq = view.getUint32(0, true);
|
|
1547
|
+
if (turnSeq >= acceptedTurnSeqRef.current) {
|
|
1548
|
+
acceptedTurnSeqRef.current = turnSeq;
|
|
1549
|
+
const pcmBytes = buf.slice(4);
|
|
1550
|
+
playerNodeRef.current?.port.postMessage(pcmBytes, [pcmBytes]);
|
|
1551
|
+
}
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
let msg;
|
|
1555
|
+
try {
|
|
1556
|
+
msg = JSON.parse(event.data);
|
|
1557
|
+
} catch {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
switch (msg.type) {
|
|
1561
|
+
case "CUSTOM":
|
|
1562
|
+
handleCustomEvent(
|
|
1563
|
+
msg.name,
|
|
1564
|
+
msg.value
|
|
1565
|
+
);
|
|
1566
|
+
break;
|
|
1567
|
+
case "TOOL_CALL_CHUNK":
|
|
1568
|
+
upsertToolCall(msg.toolCallId, {
|
|
1569
|
+
name: msg.toolCallName,
|
|
1570
|
+
status: "running"
|
|
1571
|
+
});
|
|
1572
|
+
break;
|
|
1573
|
+
case "TOOL_CALL_RESULT": {
|
|
1574
|
+
let parsed = null;
|
|
1575
|
+
try {
|
|
1576
|
+
parsed = JSON.parse(msg.content);
|
|
1577
|
+
} catch {
|
|
1578
|
+
parsed = { output: msg.content };
|
|
1579
|
+
}
|
|
1580
|
+
upsertToolCall(msg.toolCallId, {
|
|
1581
|
+
status: parsed?.error ? "error" : "done",
|
|
1582
|
+
summary: parsed?.error || (typeof parsed?.output === "string" ? parsed.output : JSON.stringify(parsed?.output ?? ""))
|
|
1583
|
+
});
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1586
|
+
case "RUN_ERROR":
|
|
1587
|
+
voiceLogger.warn("RUN_ERROR", { message: msg.message });
|
|
1588
|
+
setError(
|
|
1589
|
+
new Error(msg.message || "The agent run failed.")
|
|
1590
|
+
);
|
|
1591
|
+
setState("error");
|
|
1592
|
+
break;
|
|
1593
|
+
default:
|
|
1594
|
+
break;
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
[handleCustomEvent, upsertToolCall, voiceLogger]
|
|
1598
|
+
);
|
|
1599
|
+
const start = (0, import_react5.useCallback)(async () => {
|
|
1600
|
+
if (!agentId) {
|
|
1601
|
+
const err = new Error(
|
|
1602
|
+
"useVoice: no agentId provided and no defaultAgentId set on PersonaProvider"
|
|
1603
|
+
);
|
|
1604
|
+
setError(err);
|
|
1605
|
+
setState("error");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
setError(null);
|
|
1609
|
+
setEndReason(null);
|
|
1610
|
+
setTranscript([]);
|
|
1611
|
+
setPartial(null);
|
|
1612
|
+
setToolCalls([]);
|
|
1613
|
+
acceptedTurnSeqRef.current = 0;
|
|
1614
|
+
setState("connecting");
|
|
1615
|
+
voiceLogger.debug("start() called", { agentId, threadId });
|
|
1616
|
+
try {
|
|
1617
|
+
const res = await fetchWithAuth("/voice/sessions", {
|
|
1618
|
+
method: "POST",
|
|
1619
|
+
headers: { "Content-Type": "application/json" },
|
|
1620
|
+
body: JSON.stringify({
|
|
1621
|
+
agentId,
|
|
1622
|
+
...threadId ? { threadId } : {}
|
|
1623
|
+
})
|
|
1624
|
+
});
|
|
1625
|
+
if (!res.ok) {
|
|
1626
|
+
throw new Error(`Failed to start voice session: ${res.statusText}`);
|
|
1627
|
+
}
|
|
1628
|
+
const data = await res.json();
|
|
1629
|
+
const ticket = data?.data ?? data;
|
|
1630
|
+
if (!ticket?.wsUrl) {
|
|
1631
|
+
throw new Error("Server did not return a voice session URL.");
|
|
1632
|
+
}
|
|
1633
|
+
const ws = new WebSocket(ticket.wsUrl);
|
|
1634
|
+
ws.binaryType = "arraybuffer";
|
|
1635
|
+
wsRef.current = ws;
|
|
1636
|
+
ws.onmessage = handleMessage;
|
|
1637
|
+
ws.onerror = () => {
|
|
1638
|
+
voiceLogger.warn("WebSocket error");
|
|
1639
|
+
if (mountedRef.current) {
|
|
1640
|
+
setError(new Error("Voice connection error."));
|
|
1641
|
+
setState("error");
|
|
1642
|
+
}
|
|
1643
|
+
};
|
|
1644
|
+
ws.onclose = () => {
|
|
1645
|
+
if (mountedRef.current && wsRef.current === ws) {
|
|
1646
|
+
setState((prev) => prev === "error" ? prev : "idle");
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
} catch (err) {
|
|
1650
|
+
const errorObj = err instanceof Error ? err : new Error(String(err));
|
|
1651
|
+
voiceLogger.error("start() failed", { error: errorObj.message });
|
|
1652
|
+
if (mountedRef.current) {
|
|
1653
|
+
setError(errorObj);
|
|
1654
|
+
setState("error");
|
|
1655
|
+
}
|
|
1656
|
+
teardownAudio();
|
|
1657
|
+
}
|
|
1658
|
+
}, [agentId, threadId, fetchWithAuth, handleMessage, teardownAudio, voiceLogger]);
|
|
1659
|
+
const mute = (0, import_react5.useCallback)((muted) => {
|
|
1660
|
+
setIsMuted(muted);
|
|
1661
|
+
streamRef.current?.getAudioTracks().forEach((track) => {
|
|
1662
|
+
track.enabled = !muted;
|
|
1663
|
+
});
|
|
1664
|
+
}, []);
|
|
1665
|
+
const sendText = (0, import_react5.useCallback)((text) => {
|
|
1666
|
+
if (!text?.trim() || wsRef.current?.readyState !== WebSocket.OPEN) return;
|
|
1667
|
+
wsRef.current.send(JSON.stringify({ type: "voice.text", text }));
|
|
1668
|
+
}, []);
|
|
1669
|
+
return {
|
|
1670
|
+
state,
|
|
1671
|
+
isMuted,
|
|
1672
|
+
transcript,
|
|
1673
|
+
partial,
|
|
1674
|
+
toolCalls,
|
|
1675
|
+
error,
|
|
1676
|
+
endReason,
|
|
1677
|
+
start,
|
|
1678
|
+
stop,
|
|
1679
|
+
mute,
|
|
1680
|
+
sendText
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// src/hooks/useFiles.ts
|
|
1685
|
+
var import_react6 = require("react");
|
|
1257
1686
|
function useFiles(autoFetch = true) {
|
|
1258
1687
|
const { fetchWithAuth } = usePersonaContext();
|
|
1259
|
-
const [files, setFiles] = (0,
|
|
1260
|
-
const [isLoading, setIsLoading] = (0,
|
|
1261
|
-
const [isUploading, setIsUploading] = (0,
|
|
1262
|
-
const [error, setError] = (0,
|
|
1263
|
-
const fetchFiles = (0,
|
|
1688
|
+
const [files, setFiles] = (0, import_react6.useState)([]);
|
|
1689
|
+
const [isLoading, setIsLoading] = (0, import_react6.useState)(false);
|
|
1690
|
+
const [isUploading, setIsUploading] = (0, import_react6.useState)(false);
|
|
1691
|
+
const [error, setError] = (0, import_react6.useState)(null);
|
|
1692
|
+
const fetchFiles = (0, import_react6.useCallback)(async () => {
|
|
1264
1693
|
setIsLoading(true);
|
|
1265
1694
|
setError(null);
|
|
1266
1695
|
try {
|
|
@@ -1278,7 +1707,7 @@ function useFiles(autoFetch = true) {
|
|
|
1278
1707
|
setIsLoading(false);
|
|
1279
1708
|
}
|
|
1280
1709
|
}, [fetchWithAuth]);
|
|
1281
|
-
const uploadFile = (0,
|
|
1710
|
+
const uploadFile = (0, import_react6.useCallback)(
|
|
1282
1711
|
async (fileOrFormData) => {
|
|
1283
1712
|
setIsUploading(true);
|
|
1284
1713
|
setError(null);
|
|
@@ -1311,7 +1740,7 @@ function useFiles(autoFetch = true) {
|
|
|
1311
1740
|
},
|
|
1312
1741
|
[fetchWithAuth, fetchFiles]
|
|
1313
1742
|
);
|
|
1314
|
-
const deleteFile = (0,
|
|
1743
|
+
const deleteFile = (0, import_react6.useCallback)(
|
|
1315
1744
|
async (fileId) => {
|
|
1316
1745
|
const res = await fetchWithAuth(`/files/${fileId}`, { method: "DELETE" });
|
|
1317
1746
|
if (!res.ok) throw new Error(`Failed to delete file: ${res.statusText}`);
|
|
@@ -1319,7 +1748,7 @@ function useFiles(autoFetch = true) {
|
|
|
1319
1748
|
},
|
|
1320
1749
|
[fetchWithAuth]
|
|
1321
1750
|
);
|
|
1322
|
-
const bulkDeleteFiles = (0,
|
|
1751
|
+
const bulkDeleteFiles = (0, import_react6.useCallback)(
|
|
1323
1752
|
async (fileIds) => {
|
|
1324
1753
|
const res = await fetchWithAuth("/files/bulk-delete", {
|
|
1325
1754
|
method: "POST",
|
|
@@ -1334,10 +1763,10 @@ function useFiles(autoFetch = true) {
|
|
|
1334
1763
|
},
|
|
1335
1764
|
[fetchWithAuth]
|
|
1336
1765
|
);
|
|
1337
|
-
const getDownloadUrl = (0,
|
|
1766
|
+
const getDownloadUrl = (0, import_react6.useCallback)((fileId) => {
|
|
1338
1767
|
return `/files/${fileId}`;
|
|
1339
1768
|
}, []);
|
|
1340
|
-
(0,
|
|
1769
|
+
(0, import_react6.useEffect)(() => {
|
|
1341
1770
|
if (autoFetch) {
|
|
1342
1771
|
void fetchFiles();
|
|
1343
1772
|
}
|
|
@@ -1356,13 +1785,13 @@ function useFiles(autoFetch = true) {
|
|
|
1356
1785
|
}
|
|
1357
1786
|
|
|
1358
1787
|
// src/hooks/useAgents.ts
|
|
1359
|
-
var
|
|
1788
|
+
var import_react7 = require("react");
|
|
1360
1789
|
function useAgents(autoFetch = true) {
|
|
1361
1790
|
const { fetchWithAuth } = usePersonaContext();
|
|
1362
|
-
const [agents, setAgents] = (0,
|
|
1363
|
-
const [isLoading, setIsLoading] = (0,
|
|
1364
|
-
const [error, setError] = (0,
|
|
1365
|
-
const fetchAgents = (0,
|
|
1791
|
+
const [agents, setAgents] = (0, import_react7.useState)([]);
|
|
1792
|
+
const [isLoading, setIsLoading] = (0, import_react7.useState)(false);
|
|
1793
|
+
const [error, setError] = (0, import_react7.useState)(null);
|
|
1794
|
+
const fetchAgents = (0, import_react7.useCallback)(async () => {
|
|
1366
1795
|
setIsLoading(true);
|
|
1367
1796
|
setError(null);
|
|
1368
1797
|
try {
|
|
@@ -1380,7 +1809,7 @@ function useAgents(autoFetch = true) {
|
|
|
1380
1809
|
setIsLoading(false);
|
|
1381
1810
|
}
|
|
1382
1811
|
}, [fetchWithAuth]);
|
|
1383
|
-
(0,
|
|
1812
|
+
(0, import_react7.useEffect)(() => {
|
|
1384
1813
|
if (autoFetch) {
|
|
1385
1814
|
void fetchAgents();
|
|
1386
1815
|
}
|
|
@@ -1394,13 +1823,13 @@ function useAgents(autoFetch = true) {
|
|
|
1394
1823
|
}
|
|
1395
1824
|
|
|
1396
1825
|
// src/hooks/useConnection.ts
|
|
1397
|
-
var
|
|
1826
|
+
var import_react8 = require("react");
|
|
1398
1827
|
function useConnection(autoCheck = true) {
|
|
1399
1828
|
const { fetchWithAuth } = usePersonaContext();
|
|
1400
|
-
const [health, setHealth] = (0,
|
|
1401
|
-
const [isConnected, setIsConnected] = (0,
|
|
1402
|
-
const [isLoading, setIsLoading] = (0,
|
|
1403
|
-
const checkHealth = (0,
|
|
1829
|
+
const [health, setHealth] = (0, import_react8.useState)(null);
|
|
1830
|
+
const [isConnected, setIsConnected] = (0, import_react8.useState)(false);
|
|
1831
|
+
const [isLoading, setIsLoading] = (0, import_react8.useState)(false);
|
|
1832
|
+
const checkHealth = (0, import_react8.useCallback)(async () => {
|
|
1404
1833
|
setIsLoading(true);
|
|
1405
1834
|
try {
|
|
1406
1835
|
const res = await fetchWithAuth("/health");
|
|
@@ -1419,7 +1848,7 @@ function useConnection(autoCheck = true) {
|
|
|
1419
1848
|
setIsLoading(false);
|
|
1420
1849
|
}
|
|
1421
1850
|
}, [fetchWithAuth]);
|
|
1422
|
-
(0,
|
|
1851
|
+
(0, import_react8.useEffect)(() => {
|
|
1423
1852
|
if (autoCheck) {
|
|
1424
1853
|
void checkHealth();
|
|
1425
1854
|
}
|
|
@@ -1433,15 +1862,15 @@ function useConnection(autoCheck = true) {
|
|
|
1433
1862
|
}
|
|
1434
1863
|
|
|
1435
1864
|
// src/hooks/useMcpConnections.ts
|
|
1436
|
-
var
|
|
1865
|
+
var import_react9 = require("react");
|
|
1437
1866
|
function useMcpConnections(options = {}) {
|
|
1438
1867
|
const { defaultAgentId, fetchWithAuth } = usePersonaContext();
|
|
1439
1868
|
const agentId = options.agentId ?? defaultAgentId;
|
|
1440
1869
|
const autoFetch = options.autoFetch ?? true;
|
|
1441
|
-
const [connections, setConnections] = (0,
|
|
1442
|
-
const [isLoading, setIsLoading] = (0,
|
|
1443
|
-
const [error, setError] = (0,
|
|
1444
|
-
const fetchConnections = (0,
|
|
1870
|
+
const [connections, setConnections] = (0, import_react9.useState)([]);
|
|
1871
|
+
const [isLoading, setIsLoading] = (0, import_react9.useState)(false);
|
|
1872
|
+
const [error, setError] = (0, import_react9.useState)(null);
|
|
1873
|
+
const fetchConnections = (0, import_react9.useCallback)(async () => {
|
|
1445
1874
|
if (!agentId) return [];
|
|
1446
1875
|
setIsLoading(true);
|
|
1447
1876
|
setError(null);
|
|
@@ -1465,7 +1894,7 @@ function useMcpConnections(options = {}) {
|
|
|
1465
1894
|
setIsLoading(false);
|
|
1466
1895
|
}
|
|
1467
1896
|
}, [agentId, fetchWithAuth, options.returnTo]);
|
|
1468
|
-
(0,
|
|
1897
|
+
(0, import_react9.useEffect)(() => {
|
|
1469
1898
|
if (autoFetch) void fetchConnections();
|
|
1470
1899
|
}, [autoFetch, fetchConnections]);
|
|
1471
1900
|
return {
|
|
@@ -1480,7 +1909,7 @@ function useMcpConnections(options = {}) {
|
|
|
1480
1909
|
|
|
1481
1910
|
// src/index.ts
|
|
1482
1911
|
var import_logger2 = require("@personaai/logger");
|
|
1483
|
-
var VERSION = "0.
|
|
1912
|
+
var VERSION = "0.7.0";
|
|
1484
1913
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1485
1914
|
0 && (module.exports = {
|
|
1486
1915
|
PersonaProvider,
|
|
@@ -1499,6 +1928,7 @@ var VERSION = "0.6.0";
|
|
|
1499
1928
|
useMcpConnections,
|
|
1500
1929
|
useMemory,
|
|
1501
1930
|
usePersonaContext,
|
|
1502
|
-
useThreads
|
|
1931
|
+
useThreads,
|
|
1932
|
+
useVoice
|
|
1503
1933
|
});
|
|
1504
1934
|
//# sourceMappingURL=index.cjs.map
|