@athenaintel/react 0.22.1 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/packages/agent-runtime/src/use-deep-agent-edit-submit.cjs +79 -28
- package/dist/packages/agent-runtime/src/use-deep-agent-edit-submit.cjs.map +1 -1
- package/dist/packages/agent-runtime/src/use-deep-agent-edit-submit.js +81 -30
- package/dist/packages/agent-runtime/src/use-deep-agent-edit-submit.js.map +1 -1
- package/dist/packages/react/package.json.cjs +1 -1
- package/dist/packages/react/package.json.js +1 -1
- package/dist/voice/use-athena-voice-session.cjs +1 -1
- package/dist/voice/use-athena-voice-session.cjs.map +1 -1
- package/dist/voice/use-athena-voice-session.d.ts +30 -31
- package/dist/voice/use-athena-voice-session.js +1 -1
- package/dist/voice/use-athena-voice-session.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,13 +160,13 @@ unless a collab agent supplies one.
|
|
|
160
160
|
## Voice Sessions (`@athenaintel/react/voice`)
|
|
161
161
|
|
|
162
162
|
`useAthenaVoiceSession` runs a real-time voice conversation with an Athena
|
|
163
|
-
voice agent
|
|
164
|
-
|
|
163
|
+
voice agent and streams the transcript into React state. It owns the whole
|
|
164
|
+
browser side — session creation, room connect, mic,
|
|
165
165
|
agent audio, live/interim transcript turns, reconnects, StrictMode-safe
|
|
166
166
|
teardown — so an app is a `start()` button and a transcript list.
|
|
167
167
|
|
|
168
168
|
```bash
|
|
169
|
-
bun add @athenaintel/react livekit-client #
|
|
169
|
+
bun add @athenaintel/react livekit-client # optional peer dep, only needed for voice
|
|
170
170
|
```
|
|
171
171
|
|
|
172
172
|
```tsx
|
|
@@ -191,15 +191,15 @@ const voice = useAthenaVoiceSession({
|
|
|
191
191
|
|
|
192
192
|
**Voice tools.** `enabled_tools` takes voice tool names (`AthenaVoiceToolId`),
|
|
193
193
|
not the chat `Toolkits` ids — `'web_search_browse_toolkit'` is not a voice tool,
|
|
194
|
-
`'web_search'` is. Available: `web_search` (
|
|
194
|
+
`'web_search'` is. Available: `web_search` (`search_web` is a legacy alias),
|
|
195
195
|
`browse` (read a URL), `search_knowledge_base` and `search_knowledge_base_lr`
|
|
196
196
|
(the workspace knowledge base; `_lr` is the thorough long-running retrieval),
|
|
197
197
|
`read_full_asset` and `search_assets` (as the viewer), `ask_agent` (hand a
|
|
198
198
|
question to the general agent), `save_note`, `get_current_time_est`. Tools that
|
|
199
|
-
call back into the client over
|
|
199
|
+
call back into the client over room RPC (`write_to_clipboard`, the desktop and
|
|
200
200
|
mobile tools) are not part of the hook's contract. Omit the field for the
|
|
201
|
-
server's default set; pass `[]` for pure conversation.
|
|
202
|
-
names with a 422
|
|
201
|
+
server's default set; pass `[]` for pure conversation. The server refuses
|
|
202
|
+
unknown names with a 422.
|
|
203
203
|
|
|
204
204
|
**Authentication.** Served from a Marathon computer-asset URL, the hook is
|
|
205
205
|
zero-config: it takes the viewer bearer from `useParentBridge()` and creates
|
|
@@ -234,6 +234,10 @@ Both use the same bearer / API key as session creation. The full recipe
|
|
|
234
234
|
template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
|
|
235
235
|
exported if you hold the bridge state yourself.
|
|
236
236
|
|
|
237
|
+
## What's new in 0.22.2
|
|
238
|
+
|
|
239
|
+
Public voice docs, JSDoc/`.d.ts` and the session-creation error message now use product-neutral wording (no internal service or transport names). No API changes.
|
|
240
|
+
|
|
237
241
|
## What's new in 0.22.1
|
|
238
242
|
|
|
239
243
|
Tailwind Typography colors now follow theme tokens inside SDK roots; `userBubbleForeground` / `assistantForeground` apply to message text, headings, bold, inline code and blockquotes.
|
|
@@ -251,7 +255,7 @@ carries into titles. Admin-only. See
|
|
|
251
255
|
## What's new in 0.21.0
|
|
252
256
|
|
|
253
257
|
0.21.0 adds the `@athenaintel/react/voice` entry: `useAthenaVoiceSession`
|
|
254
|
-
(
|
|
258
|
+
(room lifecycle, mic, agent audio, live transcript turns, reconnect,
|
|
255
259
|
StrictMode-safe) and `createAthenaVoiceSession`. On Marathon it is
|
|
256
260
|
zero-config and attributes the session to the viewer (workspace members and
|
|
257
261
|
external SSO viewers); elsewhere pass `createSession`. See
|
|
@@ -5,39 +5,90 @@ const react = require("react");
|
|
|
5
5
|
const editCommand = require("./edit-command.cjs");
|
|
6
6
|
function useDeepAgentMessageEditSubmit() {
|
|
7
7
|
const aui = store.useAui();
|
|
8
|
+
const alive = react.useRef(true);
|
|
9
|
+
const submitting = react.useRef(false);
|
|
10
|
+
react.useEffect(() => {
|
|
11
|
+
alive.current = true;
|
|
12
|
+
return () => {
|
|
13
|
+
alive.current = false;
|
|
14
|
+
};
|
|
15
|
+
}, []);
|
|
8
16
|
return react.useCallback(async () => {
|
|
9
|
-
var _a, _b;
|
|
17
|
+
var _a, _b, _c;
|
|
18
|
+
if (submitting.current) return false;
|
|
10
19
|
const composer = aui.message().composer();
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
const draft = composer.getState();
|
|
21
|
+
const text = draft.text.trim();
|
|
22
|
+
if (!text && draft.attachments.length === 0) return false;
|
|
23
|
+
const files = [];
|
|
24
|
+
const texts = text ? [text] : [];
|
|
25
|
+
for (const attachment of draft.attachments) {
|
|
26
|
+
if (attachment.status.type !== "complete")
|
|
27
|
+
throw new Error("Wait for attachments to finish before sending your edit.");
|
|
28
|
+
for (const part of attachment.content ?? []) {
|
|
29
|
+
if (part.type === "text") texts.push(part.text);
|
|
30
|
+
else if (part.type === "image")
|
|
31
|
+
files.push({ type: "file", mediaType: "image/*", url: part.image });
|
|
32
|
+
else if (part.type === "file")
|
|
33
|
+
files.push({
|
|
34
|
+
type: "file",
|
|
35
|
+
mediaType: part.mimeType,
|
|
36
|
+
url: part.data,
|
|
37
|
+
...part.filename && { filename: part.filename }
|
|
38
|
+
});
|
|
39
|
+
else throw new Error("This attachment cannot be edited here.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
submitting.current = true;
|
|
43
|
+
try {
|
|
44
|
+
const sourceId = aui.message().getState().id;
|
|
45
|
+
const readExtras = () => alive.current ? editCommand.readDeepAgentStatewireExtras(aui.thread().getState().extras) : null;
|
|
46
|
+
const extras = readExtras();
|
|
47
|
+
if (extras) {
|
|
48
|
+
const preparedMetadata = (_a = extras.prepareMessageMetadata) == null ? void 0 : _a.call(extras);
|
|
49
|
+
try {
|
|
50
|
+
const command = await editCommand.resolveDeepAgentEditCommand({
|
|
51
|
+
readExtras,
|
|
52
|
+
sourceId,
|
|
53
|
+
text
|
|
54
|
+
});
|
|
55
|
+
if (!alive.current || !composer.getState().isEditing) {
|
|
56
|
+
if (preparedMetadata) (_b = extras.discardPreparedMessageMetadata) == null ? void 0 : _b.call(extras, preparedMetadata);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
if (command) {
|
|
60
|
+
void Promise.resolve(
|
|
61
|
+
extras.sendCommand(
|
|
62
|
+
{
|
|
63
|
+
...command,
|
|
64
|
+
message: {
|
|
65
|
+
...command.message,
|
|
66
|
+
parts: [
|
|
67
|
+
...files,
|
|
68
|
+
...texts.length ? [{ type: "text", text: texts.join("\n\n") }] : []
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
preparedMetadata === void 0 ? void 0 : { preparedMetadata }
|
|
73
|
+
)
|
|
74
|
+
).catch(() => void 0);
|
|
75
|
+
composer.cancel();
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (preparedMetadata && extras.sendPreparedEdit) {
|
|
79
|
+
extras.sendPreparedEdit(sourceId, preparedMetadata);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (preparedMetadata) (_c = extras.discardPreparedMessageMetadata) == null ? void 0 : _c.call(extras, preparedMetadata);
|
|
84
|
+
throw error;
|
|
33
85
|
}
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (preparedMetadata) (_b = extras.discardPreparedMessageMetadata) == null ? void 0 : _b.call(extras, preparedMetadata);
|
|
36
|
-
throw error;
|
|
37
86
|
}
|
|
87
|
+
composer.send();
|
|
88
|
+
return true;
|
|
89
|
+
} finally {
|
|
90
|
+
submitting.current = false;
|
|
38
91
|
}
|
|
39
|
-
composer.send();
|
|
40
|
-
return true;
|
|
41
92
|
}, [aui]);
|
|
42
93
|
}
|
|
43
94
|
exports.useDeepAgentMessageEditSubmit = useDeepAgentMessageEditSubmit;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-deep-agent-edit-submit.cjs","sources":["../../../../../agent-runtime/src/use-deep-agent-edit-submit.ts"],"sourcesContent":["import { useAui } from '@assistant-ui/store';\nimport { useCallback } from 'react';\nimport { readDeepAgentStatewireExtras, resolveDeepAgentEditCommand } from './edit-command';\n\n/**\n * The submit handler of a message's edit composer on a deep-agent thread: the\n * edited text leaves as a `run/edit` anchored on the host's own parent of the\n * message (see `edit-command.ts`) — on a windowed wire the lane's\n * `anchorBefore` at the window edge, and older pages loaded first for a\n * message below the loaded range — instead of the library's converted-message\n * anchor, which the host rejects `wrong-anchor` after a chunked assistant turn\n * and at the window edge.\n *\n * Read inside the message scope (`aui.message()`): the library's optimistic\n * fold shows the edited bubble and rewinds the visible history the moment the\n * command is dispatched, so the edit composer closes right away. Falls back\n * to the composer's own `send()` when the thread is not a statewire thread or\n * the host has not settled the message yet. Resolves `false`
|
|
1
|
+
{"version":3,"file":"use-deep-agent-edit-submit.cjs","sources":["../../../../../agent-runtime/src/use-deep-agent-edit-submit.ts"],"sourcesContent":["import { useAui } from '@assistant-ui/store';\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { readDeepAgentStatewireExtras, resolveDeepAgentEditCommand } from './edit-command';\n\n/**\n * The submit handler of a message's edit composer on a deep-agent thread: the\n * edited text leaves as a `run/edit` anchored on the host's own parent of the\n * message (see `edit-command.ts`) — on a windowed wire the lane's\n * `anchorBefore` at the window edge, and older pages loaded first for a\n * message below the loaded range — instead of the library's converted-message\n * anchor, which the host rejects `wrong-anchor` after a chunked assistant turn\n * and at the window edge.\n *\n * Read inside the message scope (`aui.message()`): the library's optimistic\n * fold shows the edited bubble and rewinds the visible history the moment the\n * command is dispatched, so the edit composer closes right away. Falls back\n * to the composer's own `send()` when the thread is not a statewire thread or\n * the host has not settled the message yet. Resolves `false` for an empty\n * draft, a duplicate submission, or an edit cancelled during preparation.\n */\nexport function useDeepAgentMessageEditSubmit(): () => Promise<boolean> {\n const aui = useAui();\n const alive = useRef(true);\n const submitting = useRef(false);\n useEffect(() => {\n alive.current = true;\n return () => {\n alive.current = false;\n };\n }, []);\n return useCallback(async () => {\n if (submitting.current) return false;\n const composer = aui.message().composer();\n const draft = composer.getState();\n const text = draft.text.trim();\n if (!text && draft.attachments.length === 0) return false;\n const files: StatewireThread.FilePart[] = [];\n const texts = text ? [text] : [];\n // beginEdit lifts original inline images/files into completed composer\n // attachments. The anchored command must carry those too: a text-only\n // reconstruction silently deleted them when editing a cross-device turn.\n for (const attachment of draft.attachments) {\n if (attachment.status.type !== 'complete')\n throw new Error('Wait for attachments to finish before sending your edit.');\n for (const part of attachment.content ?? []) {\n if (part.type === 'text') texts.push(part.text);\n else if (part.type === 'image')\n files.push({ type: 'file', mediaType: 'image/*', url: part.image });\n else if (part.type === 'file')\n files.push({\n type: 'file',\n mediaType: part.mimeType,\n url: part.data,\n ...(part.filename && { filename: part.filename }),\n });\n else throw new Error('This attachment cannot be edited here.');\n }\n }\n submitting.current = true;\n try {\n const sourceId = aui.message().getState().id;\n const readExtras = () =>\n alive.current ? readDeepAgentStatewireExtras(aui.thread().getState().extras) : null;\n const extras = readExtras();\n if (extras) {\n const preparedMetadata = extras.prepareMessageMetadata?.();\n try {\n const command = await resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n });\n if (!alive.current || !composer.getState().isEditing) {\n if (preparedMetadata) extras.discardPreparedMessageMetadata?.(preparedMetadata);\n return false;\n }\n if (command) {\n void Promise.resolve(\n extras.sendCommand(\n {\n ...command,\n message: {\n ...command.message,\n parts: [\n ...files,\n ...(texts.length\n ? [{ type: 'text' as const, text: texts.join('\\n\\n') }]\n : []),\n ],\n },\n },\n preparedMetadata === undefined ? undefined : { preparedMetadata },\n ),\n ).catch(() => undefined);\n composer.cancel();\n return true;\n }\n if (preparedMetadata && extras.sendPreparedEdit) {\n extras.sendPreparedEdit(sourceId, preparedMetadata);\n return true;\n }\n } catch (error) {\n if (preparedMetadata) extras.discardPreparedMessageMetadata?.(preparedMetadata);\n throw error;\n }\n }\n composer.send();\n return true;\n } finally {\n submitting.current = false;\n }\n }, [aui]);\n}\n"],"names":["useAui","useRef","useEffect","useCallback","readDeepAgentStatewireExtras","resolveDeepAgentEditCommand"],"mappings":";;;;;AAqBO,SAAS,gCAAwD;AACtE,QAAM,MAAMA,MAAAA,OAAA;AACZ,QAAM,QAAQC,MAAAA,OAAO,IAAI;AACzB,QAAM,aAAaA,MAAAA,OAAO,KAAK;AAC/BC,QAAAA,UAAU,MAAM;AACd,UAAM,UAAU;AAChB,WAAO,MAAM;AACX,YAAM,UAAU;AAAA,IAClB;AAAA,EACF,GAAG,CAAA,CAAE;AACL,SAAOC,MAAAA,YAAY,YAAY;;AAC7B,QAAI,WAAW,QAAS,QAAO;AAC/B,UAAM,WAAW,IAAI,QAAA,EAAU,SAAA;AAC/B,UAAM,QAAQ,SAAS,SAAA;AACvB,UAAM,OAAO,MAAM,KAAK,KAAA;AACxB,QAAI,CAAC,QAAQ,MAAM,YAAY,WAAW,EAAG,QAAO;AACpD,UAAM,QAAoC,CAAA;AAC1C,UAAM,QAAQ,OAAO,CAAC,IAAI,IAAI,CAAA;AAI9B,eAAW,cAAc,MAAM,aAAa;AAC1C,UAAI,WAAW,OAAO,SAAS;AAC7B,cAAM,IAAI,MAAM,0DAA0D;AAC5E,iBAAW,QAAQ,WAAW,WAAW,CAAA,GAAI;AAC3C,YAAI,KAAK,SAAS,OAAQ,OAAM,KAAK,KAAK,IAAI;AAAA,iBACrC,KAAK,SAAS;AACrB,gBAAM,KAAK,EAAE,MAAM,QAAQ,WAAW,WAAW,KAAK,KAAK,OAAO;AAAA,iBAC3D,KAAK,SAAS;AACrB,gBAAM,KAAK;AAAA,YACT,MAAM;AAAA,YACN,WAAW,KAAK;AAAA,YAChB,KAAK,KAAK;AAAA,YACV,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAA;AAAA,UAAS,CAChD;AAAA,YACE,OAAM,IAAI,MAAM,wCAAwC;AAAA,MAC/D;AAAA,IACF;AACA,eAAW,UAAU;AACrB,QAAI;AACF,YAAM,WAAW,IAAI,QAAA,EAAU,WAAW;AAC1C,YAAM,aAAa,MACjB,MAAM,UAAUC,YAAAA,6BAA6B,IAAI,SAAS,WAAW,MAAM,IAAI;AACjF,YAAM,SAAS,WAAA;AACf,UAAI,QAAQ;AACV,cAAM,oBAAmB,YAAO,2BAAP;AACzB,YAAI;AACF,gBAAM,UAAU,MAAMC,wCAA4B;AAAA,YAChD;AAAA,YACA;AAAA,YACA;AAAA,UAAA,CACD;AACD,cAAI,CAAC,MAAM,WAAW,CAAC,SAAS,SAAA,EAAW,WAAW;AACpD,gBAAI,iBAAkB,cAAO,mCAAP,gCAAwC;AAC9D,mBAAO;AAAA,UACT;AACA,cAAI,SAAS;AACX,iBAAK,QAAQ;AAAA,cACX,OAAO;AAAA,gBACL;AAAA,kBACE,GAAG;AAAA,kBACH,SAAS;AAAA,oBACP,GAAG,QAAQ;AAAA,oBACX,OAAO;AAAA,sBACL,GAAG;AAAA,sBACH,GAAI,MAAM,SACN,CAAC,EAAE,MAAM,QAAiB,MAAM,MAAM,KAAK,MAAM,EAAA,CAAG,IACpD,CAAA;AAAA,oBAAC;AAAA,kBACP;AAAA,gBACF;AAAA,gBAEF,qBAAqB,SAAY,SAAY,EAAE,iBAAA;AAAA,cAAiB;AAAA,YAClE,EACA,MAAM,MAAM,MAAS;AACvB,qBAAS,OAAA;AACT,mBAAO;AAAA,UACT;AACA,cAAI,oBAAoB,OAAO,kBAAkB;AAC/C,mBAAO,iBAAiB,UAAU,gBAAgB;AAClD,mBAAO;AAAA,UACT;AAAA,QACF,SAAS,OAAO;AACd,cAAI,iBAAkB,cAAO,mCAAP,gCAAwC;AAC9D,gBAAM;AAAA,QACR;AAAA,MACF;AACA,eAAS,KAAA;AACT,aAAO;AAAA,IACT,UAAA;AACE,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AACV;;"}
|
|
@@ -1,41 +1,92 @@
|
|
|
1
1
|
import { useAui } from "@assistant-ui/store";
|
|
2
|
-
import { useCallback } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { useRef, useEffect, useCallback } from "react";
|
|
3
|
+
import { readDeepAgentStatewireExtras, resolveDeepAgentEditCommand } from "./edit-command.js";
|
|
4
4
|
function useDeepAgentMessageEditSubmit() {
|
|
5
5
|
const aui = useAui();
|
|
6
|
+
const alive = useRef(true);
|
|
7
|
+
const submitting = useRef(false);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
alive.current = true;
|
|
10
|
+
return () => {
|
|
11
|
+
alive.current = false;
|
|
12
|
+
};
|
|
13
|
+
}, []);
|
|
6
14
|
return useCallback(async () => {
|
|
7
|
-
var _a, _b;
|
|
15
|
+
var _a, _b, _c;
|
|
16
|
+
if (submitting.current) return false;
|
|
8
17
|
const composer = aui.message().composer();
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
const draft = composer.getState();
|
|
19
|
+
const text = draft.text.trim();
|
|
20
|
+
if (!text && draft.attachments.length === 0) return false;
|
|
21
|
+
const files = [];
|
|
22
|
+
const texts = text ? [text] : [];
|
|
23
|
+
for (const attachment of draft.attachments) {
|
|
24
|
+
if (attachment.status.type !== "complete")
|
|
25
|
+
throw new Error("Wait for attachments to finish before sending your edit.");
|
|
26
|
+
for (const part of attachment.content ?? []) {
|
|
27
|
+
if (part.type === "text") texts.push(part.text);
|
|
28
|
+
else if (part.type === "image")
|
|
29
|
+
files.push({ type: "file", mediaType: "image/*", url: part.image });
|
|
30
|
+
else if (part.type === "file")
|
|
31
|
+
files.push({
|
|
32
|
+
type: "file",
|
|
33
|
+
mediaType: part.mimeType,
|
|
34
|
+
url: part.data,
|
|
35
|
+
...part.filename && { filename: part.filename }
|
|
36
|
+
});
|
|
37
|
+
else throw new Error("This attachment cannot be edited here.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
submitting.current = true;
|
|
41
|
+
try {
|
|
42
|
+
const sourceId = aui.message().getState().id;
|
|
43
|
+
const readExtras = () => alive.current ? readDeepAgentStatewireExtras(aui.thread().getState().extras) : null;
|
|
44
|
+
const extras = readExtras();
|
|
45
|
+
if (extras) {
|
|
46
|
+
const preparedMetadata = (_a = extras.prepareMessageMetadata) == null ? void 0 : _a.call(extras);
|
|
47
|
+
try {
|
|
48
|
+
const command = await resolveDeepAgentEditCommand({
|
|
49
|
+
readExtras,
|
|
50
|
+
sourceId,
|
|
51
|
+
text
|
|
52
|
+
});
|
|
53
|
+
if (!alive.current || !composer.getState().isEditing) {
|
|
54
|
+
if (preparedMetadata) (_b = extras.discardPreparedMessageMetadata) == null ? void 0 : _b.call(extras, preparedMetadata);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (command) {
|
|
58
|
+
void Promise.resolve(
|
|
59
|
+
extras.sendCommand(
|
|
60
|
+
{
|
|
61
|
+
...command,
|
|
62
|
+
message: {
|
|
63
|
+
...command.message,
|
|
64
|
+
parts: [
|
|
65
|
+
...files,
|
|
66
|
+
...texts.length ? [{ type: "text", text: texts.join("\n\n") }] : []
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
preparedMetadata === void 0 ? void 0 : { preparedMetadata }
|
|
71
|
+
)
|
|
72
|
+
).catch(() => void 0);
|
|
73
|
+
composer.cancel();
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (preparedMetadata && extras.sendPreparedEdit) {
|
|
77
|
+
extras.sendPreparedEdit(sourceId, preparedMetadata);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (preparedMetadata) (_c = extras.discardPreparedMessageMetadata) == null ? void 0 : _c.call(extras, preparedMetadata);
|
|
82
|
+
throw error;
|
|
31
83
|
}
|
|
32
|
-
} catch (error) {
|
|
33
|
-
if (preparedMetadata) (_b = extras.discardPreparedMessageMetadata) == null ? void 0 : _b.call(extras, preparedMetadata);
|
|
34
|
-
throw error;
|
|
35
84
|
}
|
|
85
|
+
composer.send();
|
|
86
|
+
return true;
|
|
87
|
+
} finally {
|
|
88
|
+
submitting.current = false;
|
|
36
89
|
}
|
|
37
|
-
composer.send();
|
|
38
|
-
return true;
|
|
39
90
|
}, [aui]);
|
|
40
91
|
}
|
|
41
92
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-deep-agent-edit-submit.js","sources":["../../../../../agent-runtime/src/use-deep-agent-edit-submit.ts"],"sourcesContent":["import { useAui } from '@assistant-ui/store';\nimport { useCallback } from 'react';\nimport { readDeepAgentStatewireExtras, resolveDeepAgentEditCommand } from './edit-command';\n\n/**\n * The submit handler of a message's edit composer on a deep-agent thread: the\n * edited text leaves as a `run/edit` anchored on the host's own parent of the\n * message (see `edit-command.ts`) — on a windowed wire the lane's\n * `anchorBefore` at the window edge, and older pages loaded first for a\n * message below the loaded range — instead of the library's converted-message\n * anchor, which the host rejects `wrong-anchor` after a chunked assistant turn\n * and at the window edge.\n *\n * Read inside the message scope (`aui.message()`): the library's optimistic\n * fold shows the edited bubble and rewinds the visible history the moment the\n * command is dispatched, so the edit composer closes right away. Falls back\n * to the composer's own `send()` when the thread is not a statewire thread or\n * the host has not settled the message yet. Resolves `false`
|
|
1
|
+
{"version":3,"file":"use-deep-agent-edit-submit.js","sources":["../../../../../agent-runtime/src/use-deep-agent-edit-submit.ts"],"sourcesContent":["import { useAui } from '@assistant-ui/store';\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef } from 'react';\nimport { readDeepAgentStatewireExtras, resolveDeepAgentEditCommand } from './edit-command';\n\n/**\n * The submit handler of a message's edit composer on a deep-agent thread: the\n * edited text leaves as a `run/edit` anchored on the host's own parent of the\n * message (see `edit-command.ts`) — on a windowed wire the lane's\n * `anchorBefore` at the window edge, and older pages loaded first for a\n * message below the loaded range — instead of the library's converted-message\n * anchor, which the host rejects `wrong-anchor` after a chunked assistant turn\n * and at the window edge.\n *\n * Read inside the message scope (`aui.message()`): the library's optimistic\n * fold shows the edited bubble and rewinds the visible history the moment the\n * command is dispatched, so the edit composer closes right away. Falls back\n * to the composer's own `send()` when the thread is not a statewire thread or\n * the host has not settled the message yet. Resolves `false` for an empty\n * draft, a duplicate submission, or an edit cancelled during preparation.\n */\nexport function useDeepAgentMessageEditSubmit(): () => Promise<boolean> {\n const aui = useAui();\n const alive = useRef(true);\n const submitting = useRef(false);\n useEffect(() => {\n alive.current = true;\n return () => {\n alive.current = false;\n };\n }, []);\n return useCallback(async () => {\n if (submitting.current) return false;\n const composer = aui.message().composer();\n const draft = composer.getState();\n const text = draft.text.trim();\n if (!text && draft.attachments.length === 0) return false;\n const files: StatewireThread.FilePart[] = [];\n const texts = text ? [text] : [];\n // beginEdit lifts original inline images/files into completed composer\n // attachments. The anchored command must carry those too: a text-only\n // reconstruction silently deleted them when editing a cross-device turn.\n for (const attachment of draft.attachments) {\n if (attachment.status.type !== 'complete')\n throw new Error('Wait for attachments to finish before sending your edit.');\n for (const part of attachment.content ?? []) {\n if (part.type === 'text') texts.push(part.text);\n else if (part.type === 'image')\n files.push({ type: 'file', mediaType: 'image/*', url: part.image });\n else if (part.type === 'file')\n files.push({\n type: 'file',\n mediaType: part.mimeType,\n url: part.data,\n ...(part.filename && { filename: part.filename }),\n });\n else throw new Error('This attachment cannot be edited here.');\n }\n }\n submitting.current = true;\n try {\n const sourceId = aui.message().getState().id;\n const readExtras = () =>\n alive.current ? readDeepAgentStatewireExtras(aui.thread().getState().extras) : null;\n const extras = readExtras();\n if (extras) {\n const preparedMetadata = extras.prepareMessageMetadata?.();\n try {\n const command = await resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n });\n if (!alive.current || !composer.getState().isEditing) {\n if (preparedMetadata) extras.discardPreparedMessageMetadata?.(preparedMetadata);\n return false;\n }\n if (command) {\n void Promise.resolve(\n extras.sendCommand(\n {\n ...command,\n message: {\n ...command.message,\n parts: [\n ...files,\n ...(texts.length\n ? [{ type: 'text' as const, text: texts.join('\\n\\n') }]\n : []),\n ],\n },\n },\n preparedMetadata === undefined ? undefined : { preparedMetadata },\n ),\n ).catch(() => undefined);\n composer.cancel();\n return true;\n }\n if (preparedMetadata && extras.sendPreparedEdit) {\n extras.sendPreparedEdit(sourceId, preparedMetadata);\n return true;\n }\n } catch (error) {\n if (preparedMetadata) extras.discardPreparedMessageMetadata?.(preparedMetadata);\n throw error;\n }\n }\n composer.send();\n return true;\n } finally {\n submitting.current = false;\n }\n }, [aui]);\n}\n"],"names":[],"mappings":";;;AAqBO,SAAS,gCAAwD;AACtE,QAAM,MAAM,OAAA;AACZ,QAAM,QAAQ,OAAO,IAAI;AACzB,QAAM,aAAa,OAAO,KAAK;AAC/B,YAAU,MAAM;AACd,UAAM,UAAU;AAChB,WAAO,MAAM;AACX,YAAM,UAAU;AAAA,IAClB;AAAA,EACF,GAAG,CAAA,CAAE;AACL,SAAO,YAAY,YAAY;;AAC7B,QAAI,WAAW,QAAS,QAAO;AAC/B,UAAM,WAAW,IAAI,QAAA,EAAU,SAAA;AAC/B,UAAM,QAAQ,SAAS,SAAA;AACvB,UAAM,OAAO,MAAM,KAAK,KAAA;AACxB,QAAI,CAAC,QAAQ,MAAM,YAAY,WAAW,EAAG,QAAO;AACpD,UAAM,QAAoC,CAAA;AAC1C,UAAM,QAAQ,OAAO,CAAC,IAAI,IAAI,CAAA;AAI9B,eAAW,cAAc,MAAM,aAAa;AAC1C,UAAI,WAAW,OAAO,SAAS;AAC7B,cAAM,IAAI,MAAM,0DAA0D;AAC5E,iBAAW,QAAQ,WAAW,WAAW,CAAA,GAAI;AAC3C,YAAI,KAAK,SAAS,OAAQ,OAAM,KAAK,KAAK,IAAI;AAAA,iBACrC,KAAK,SAAS;AACrB,gBAAM,KAAK,EAAE,MAAM,QAAQ,WAAW,WAAW,KAAK,KAAK,OAAO;AAAA,iBAC3D,KAAK,SAAS;AACrB,gBAAM,KAAK;AAAA,YACT,MAAM;AAAA,YACN,WAAW,KAAK;AAAA,YAChB,KAAK,KAAK;AAAA,YACV,GAAI,KAAK,YAAY,EAAE,UAAU,KAAK,SAAA;AAAA,UAAS,CAChD;AAAA,YACE,OAAM,IAAI,MAAM,wCAAwC;AAAA,MAC/D;AAAA,IACF;AACA,eAAW,UAAU;AACrB,QAAI;AACF,YAAM,WAAW,IAAI,QAAA,EAAU,WAAW;AAC1C,YAAM,aAAa,MACjB,MAAM,UAAU,6BAA6B,IAAI,SAAS,WAAW,MAAM,IAAI;AACjF,YAAM,SAAS,WAAA;AACf,UAAI,QAAQ;AACV,cAAM,oBAAmB,YAAO,2BAAP;AACzB,YAAI;AACF,gBAAM,UAAU,MAAM,4BAA4B;AAAA,YAChD;AAAA,YACA;AAAA,YACA;AAAA,UAAA,CACD;AACD,cAAI,CAAC,MAAM,WAAW,CAAC,SAAS,SAAA,EAAW,WAAW;AACpD,gBAAI,iBAAkB,cAAO,mCAAP,gCAAwC;AAC9D,mBAAO;AAAA,UACT;AACA,cAAI,SAAS;AACX,iBAAK,QAAQ;AAAA,cACX,OAAO;AAAA,gBACL;AAAA,kBACE,GAAG;AAAA,kBACH,SAAS;AAAA,oBACP,GAAG,QAAQ;AAAA,oBACX,OAAO;AAAA,sBACL,GAAG;AAAA,sBACH,GAAI,MAAM,SACN,CAAC,EAAE,MAAM,QAAiB,MAAM,MAAM,KAAK,MAAM,EAAA,CAAG,IACpD,CAAA;AAAA,oBAAC;AAAA,kBACP;AAAA,gBACF;AAAA,gBAEF,qBAAqB,SAAY,SAAY,EAAE,iBAAA;AAAA,cAAiB;AAAA,YAClE,EACA,MAAM,MAAM,MAAS;AACvB,qBAAS,OAAA;AACT,mBAAO;AAAA,UACT;AACA,cAAI,oBAAoB,OAAO,kBAAkB;AAC/C,mBAAO,iBAAiB,UAAU,gBAAgB;AAClD,mBAAO;AAAA,UACT;AAAA,QACF,SAAS,OAAO;AACd,cAAI,iBAAkB,cAAO,mCAAP,gCAAwC;AAC9D,gBAAM;AAAA,QACR;AAAA,MACF;AACA,eAAS,KAAA;AACT,aAAO;AAAA,IACT,UAAA;AACE,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AACV;"}
|
|
@@ -33,7 +33,7 @@ const createAthenaVoiceSession = async ({
|
|
|
33
33
|
if (!response.ok) {
|
|
34
34
|
const detail = await response.text().catch(() => "");
|
|
35
35
|
throw new Error(
|
|
36
|
-
`
|
|
36
|
+
`Athena voice session creation failed (${response.status})${detail ? `: ${detail}` : ""}`
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
39
|
return await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-athena-voice-session.cjs","sources":["../../src/voice/use-athena-voice-session.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteTrack,\n type TranscriptionSegment,\n} from 'livekit-client';\nimport { useParentBridge, type ParentBridgeState } from '../runtime/auth';\n\n/**\n * LiveKit connection details for one Athena voice session. This is the shape\n * Orpheus returns from `POST /api/orpheus/create-session`; the hook only reads\n * these fields, so any server that proxies that call can hand it straight back.\n */\nexport interface AthenaVoiceSessionCredentials {\n /** LiveKit signalling URL (`wss://…`). */\n server_url: string;\n /** Short-lived LiveKit participant JWT scoped to `room_name`. */\n token: string;\n room_name?: string;\n /**\n * Athena session asset the voice run is persisted into. Use it with\n * `GET /api/v0/sessions/{asset_id}/download` to retrieve the transcript\n * after the call.\n */\n asset_id?: string;\n}\n\nexport type AthenaVoiceSessionStatus =\n | 'idle'\n | 'connecting'\n | 'connected'\n | 'reconnecting'\n | 'ended'\n | 'error';\n\nexport type AthenaVoiceSpeaker = 'user' | 'agent';\n\n/** One committed (final) transcription segment from either side of the call. */\nexport interface AthenaVoiceTranscriptTurn {\n id: string;\n speaker: AthenaVoiceSpeaker;\n text: string;\n /** Segment start, in ms since the LiveKit room's epoch. */\n startTime: number;\n endTime: number;\n language: string;\n}\n\n/**\n * A voice tool the Orpheus worker can run for the agent. These are voice tool\n * names, not the chat `Toolkits` ids: `'web_search_browse_toolkit'` is not a\n * voice tool, `'web_search'` is. Only these names are accepted.\n *\n * The worker also has tools that call back into the client over LiveKit RPC\n * (`write_to_clipboard`, `replace_current_selection`, `open_in_olympus`,\n * `open_asset`, `get_screen_contents`). This hook registers no RPC handlers,\n * so they are not part of its contract.\n */\nexport type AthenaVoiceToolId =\n /** Web search (Exa). */\n | 'web_search'\n /** Legacy alias of `web_search`; prefer `web_search`. */\n | 'search_web'\n /** Fetch and read one URL. */\n | 'browse'\n /** The workspace knowledge base (Ragie), where configured. */\n | 'search_knowledge_base'\n /** Same knowledge base, thorough long-running retrieval. */\n | 'search_knowledge_base_lr'\n /** Read Athena assets by id, as the viewer. */\n | 'read_full_asset'\n /** Search the viewer's Athena assets. */\n | 'search_assets'\n /** Hand a question to the general agent and speak its answer. */\n | 'ask_agent'\n /** Save a note for the user (a stub on the worker today). */\n | 'save_note'\n /** Current date and time, Eastern. */\n | 'get_current_time_est';\n\n/**\n * Orpheus agent configuration for a session the hook creates itself (see\n * `UseAthenaVoiceSessionOptions.session`). Mirrors the body of\n * `POST /api/orpheus/create-session`; every field is optional and falls back\n * to the server default.\n */\nexport interface AthenaVoiceSessionConfig {\n /** Persona / instructions for the voice agent. */\n system_prompt?: string;\n /** Realtime provider, e.g. `'openai'` or `'google'`. */\n provider?: string;\n /** Provider model id, e.g. `'gpt-realtime-2'`. */\n model?: string;\n /** Provider voice id, e.g. `'marin'` (OpenAI) or `'Aoede'` (Google). */\n voice?: string;\n temperature?: number;\n /** BCP-47 language tag; default `'en'`. */\n language?: string;\n /**\n * Voice tools the agent may call. Omit it for the server's default set\n * (`web_search`, `search_knowledge_base`, `ask_agent`, …); pass `[]` for a\n * pure conversation with no tool calls.\n */\n enabled_tools?: AthenaVoiceToolId[];\n /** IANA timezone of the speaker, e.g. `'America/New_York'`. */\n timezone?: string;\n}\n\nexport interface UseAthenaVoiceSessionOptions {\n /**\n * Creates the LiveKit session. Omit it when the app is served from a\n * Marathon computer-asset URL: the hook then calls\n * `POST {backendBaseUrl}/api/orpheus/create-session` itself with the\n * viewer's bearer token from `/_athena/auth`, so the session is created as\n * — and shows up in the sessions of — whoever is using the app.\n *\n * Works for workspace members and external SSO viewers alike: the scoped\n * `athview_` viewer credential is authorized for the Orpheus route and\n * `agents/general/invoke` the same way it is for `/api/v0/sessions`.\n *\n * Supply it when the app runs anywhere else (standalone, custom auth): it\n * should run on the caller's own server so the Athena API key never reaches\n * the browser. The hook only reads the returned credentials.\n *\n * Decide once: switching between supplying and omitting it across renders\n * of the same component is not supported.\n */\n createSession?: (\n signal: AbortSignal,\n ) => Promise<AthenaVoiceSessionCredentials>;\n /**\n * Agent configuration for sessions the hook creates itself. Ignored when\n * `createSession` is supplied.\n */\n session?: AthenaVoiceSessionConfig;\n /** Called once per committed transcript turn, in arrival order. */\n onTranscript?: (turn: AthenaVoiceTranscriptTurn) => void;\n /** Called with interim (non-final) transcript text as it streams in. */\n onInterimTranscript?: (speaker: AthenaVoiceSpeaker, text: string) => void;\n onError?: (error: Error) => void;\n /**\n * Where to play the agent's audio. Defaults to a detached `<audio>` element\n * the hook creates and removes itself.\n */\n audioElement?: HTMLMediaElement | null;\n /** Enable the microphone as soon as the room connects. Default `true`. */\n autoEnableMicrophone?: boolean;\n /** @internal Overrides `new Room()`; used by tests. */\n createRoom?: () => Room;\n /** @internal Overrides `useParentBridge()`; used by tests. */\n useBridge?: () => ParentBridgeState;\n}\n\nexport interface UseAthenaVoiceSessionReturn {\n status: AthenaVoiceSessionStatus;\n /** True between `start()` resolving the session and the room connecting. */\n isConnecting: boolean;\n isConnected: boolean;\n /** Who is speaking right now, or `null` when the room is quiet. */\n activeSpeaker: AthenaVoiceSpeaker | null;\n isMuted: boolean;\n /** Committed transcript turns so far, in arrival order. */\n transcript: AthenaVoiceTranscriptTurn[];\n /** Athena session asset id, once the session has been created. */\n sessionId: string | null;\n roomName: string | null;\n error: Error | null;\n /** Creates a session and joins the room. No-op while a session is live. */\n start: () => Promise<void>;\n /** Leaves the room and stops local tracks. Safe to call at any time. */\n stop: () => Promise<void>;\n mute: () => Promise<void>;\n unmute: () => Promise<void>;\n /** Clears `transcript`; does not affect the live session. */\n clearTranscript: () => void;\n}\n\nconst toError = (value: unknown): Error =>\n value instanceof Error ? value : new Error(String(value));\n\nconst NO_BRIDGE: ParentBridgeState = {\n token: null,\n statewireSyncUrl: null,\n backendBaseUrl: null,\n appUrl: null,\n hostAnswered: false,\n ready: true,\n};\n// Stands in for `useParentBridge` when the caller creates sessions itself, so\n// the hook never fetches `/_athena/auth` it would not use.\nconst useNoBridge = (): ParentBridgeState => NO_BRIDGE;\n\nconst NO_VIEWER_TOKEN_MESSAGE =\n 'useAthenaVoiceSession: no Athena viewer token. Either serve the app from ' +\n 'a Marathon computer-asset URL (which mints one via /_athena/auth) or pass ' +\n '`createSession` to create the session through your own server.';\n\n/**\n * `POST /api/orpheus/create-session` as the current viewer. Exported so apps\n * with their own bridge state (or a server proxy holding a user bearer) can\n * reuse the exact request the hook makes.\n */\nexport const createAthenaVoiceSession = async ({\n backendBaseUrl,\n token,\n session = {},\n signal,\n}: {\n /** Bare Agora API base, e.g. `https://api.athenaintel.com`. */\n backendBaseUrl: string;\n /** Bearer token for the user the session belongs to. */\n token: string;\n session?: AthenaVoiceSessionConfig;\n signal?: AbortSignal;\n}): Promise<AthenaVoiceSessionCredentials> => {\n const url: string = `${backendBaseUrl.replace(/\\/+$/, '')}/api/orpheus/create-session`;\n const response: Response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(session),\n signal,\n });\n if (!response.ok) {\n const detail: string = await response.text().catch(() => '');\n throw new Error(\n `Orpheus create-session failed (${response.status})${detail ? `: ${detail}` : ''}`,\n );\n }\n return (await response.json()) as AthenaVoiceSessionCredentials;\n};\n\nconst speakerOf = (\n participant: Participant | undefined,\n room: Room,\n): AthenaVoiceSpeaker =>\n participant !== undefined &&\n participant.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n\n/**\n * Runs one Athena voice session (Orpheus agent over LiveKit) from React.\n *\n * Owns the whole browser-side lifecycle the Orpheus API leaves to the caller:\n * creating the session through the app's own server, joining the LiveKit room,\n * publishing the microphone, playing the agent's audio, tracking who is\n * speaking, committing transcript turns from LiveKit transcription events, and\n * tearing everything down on `stop()` or unmount. A second `start()` while a\n * session is live (or still connecting) is ignored, so React StrictMode's\n * double-invoked effects cannot dispatch two agents into one room.\n */\nexport const useAthenaVoiceSession = (\n options: UseAthenaVoiceSessionOptions = {},\n): UseAthenaVoiceSessionReturn => {\n const {\n createSession,\n session,\n onTranscript,\n onInterimTranscript,\n onError,\n audioElement,\n autoEnableMicrophone = true,\n createRoom,\n useBridge = createSession ? useNoBridge : useParentBridge,\n } = options;\n\n // Viewer identity minted by Marathon (`/_athena/auth`) or the Olympus iframe\n // bridge. Only consulted when the caller did not supply `createSession`.\n const bridge: ParentBridgeState = useBridge();\n const bridgeRef = useRef<ParentBridgeState>(bridge);\n bridgeRef.current = bridge;\n const bridgeReadyWaitersRef = useRef<Array<() => void>>([]);\n useEffect(() => {\n if (!bridge.ready) return;\n const waiters: Array<() => void> = bridgeReadyWaitersRef.current;\n bridgeReadyWaitersRef.current = [];\n for (const resolve of waiters) resolve();\n }, [bridge.ready]);\n\n const sessionRef = useRef<AthenaVoiceSessionConfig | undefined>(session);\n sessionRef.current = session;\n\n const createSessionAsViewer = useCallback(\n async (signal: AbortSignal): Promise<AthenaVoiceSessionCredentials> => {\n if (!bridgeRef.current.ready) {\n await new Promise<void>((resolve) => {\n bridgeReadyWaitersRef.current.push(resolve);\n signal.addEventListener('abort', () => resolve(), { once: true });\n });\n if (signal.aborted) throw new Error('aborted');\n }\n const { token, backendBaseUrl } = bridgeRef.current;\n if (!token || !backendBaseUrl) throw new Error(NO_VIEWER_TOKEN_MESSAGE);\n return createAthenaVoiceSession({\n backendBaseUrl,\n token,\n session: sessionRef.current,\n signal,\n });\n },\n [],\n );\n\n const [status, setStatus] = useState<AthenaVoiceSessionStatus>('idle');\n const [activeSpeaker, setActiveSpeaker] = useState<AthenaVoiceSpeaker | null>(\n null,\n );\n const [isMuted, setIsMuted] = useState<boolean>(false);\n const [transcript, setTranscript] = useState<AthenaVoiceTranscriptTurn[]>([]);\n const [sessionId, setSessionId] = useState<string | null>(null);\n const [roomName, setRoomName] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Latest callbacks without re-subscribing room listeners on every render.\n const callbacksRef = useRef({\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n });\n callbacksRef.current = {\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n };\n\n const roomRef = useRef<Room | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const attachedElementsRef = useRef<HTMLMediaElement[]>([]);\n const ownedAudioRef = useRef<HTMLMediaElement | null>(null);\n const committedSegmentIdsRef = useRef<Set<string>>(new Set());\n // Guards `start()` against re-entry from the moment it is called, before any\n // state update lands — a StrictMode double effect fires both calls\n // synchronously in the same tick.\n const startingRef = useRef<boolean>(false);\n\n const fail = useCallback((cause: unknown) => {\n const err: Error = toError(cause);\n setError(err);\n setStatus('error');\n callbacksRef.current.onError?.(err);\n }, []);\n\n const detachAudio = useCallback(() => {\n for (const element of attachedElementsRef.current) {\n element.srcObject = null;\n if (element === ownedAudioRef.current) element.remove();\n }\n attachedElementsRef.current = [];\n ownedAudioRef.current = null;\n }, []);\n\n const teardown = useCallback(async () => {\n abortRef.current?.abort();\n abortRef.current = null;\n const room: Room | null = roomRef.current;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n if (room) {\n room.removeAllListeners();\n await room.disconnect(true);\n }\n }, [detachAudio]);\n\n const stop = useCallback(async () => {\n const hadRoom: boolean =\n roomRef.current !== null || abortRef.current !== null;\n await teardown();\n setActiveSpeaker(null);\n if (hadRoom) setStatus('ended');\n }, [teardown]);\n\n const attachTrack = useCallback(\n (track: RemoteTrack) => {\n if (track.kind !== Track.Kind.Audio) return;\n if (audioElement) {\n track.attach(audioElement);\n attachedElementsRef.current.push(audioElement);\n return;\n }\n const element: HTMLMediaElement = track.attach();\n element.setAttribute('data-athena-voice-audio', '');\n document.body.appendChild(element);\n ownedAudioRef.current = element;\n attachedElementsRef.current.push(element);\n },\n [audioElement],\n );\n\n const handleTranscription = useCallback(\n (\n segments: TranscriptionSegment[],\n participant: Participant | undefined,\n room: Room,\n ) => {\n const speaker: AthenaVoiceSpeaker = speakerOf(participant, room);\n const committed: AthenaVoiceTranscriptTurn[] = [];\n for (const segment of segments) {\n if (!segment.final) {\n callbacksRef.current.onInterimTranscript?.(speaker, segment.text);\n continue;\n }\n if (committedSegmentIdsRef.current.has(segment.id)) continue;\n committedSegmentIdsRef.current.add(segment.id);\n committed.push({\n id: segment.id,\n speaker,\n text: segment.text,\n startTime: segment.startTime,\n endTime: segment.endTime,\n language: segment.language,\n });\n }\n if (committed.length === 0) return;\n setTranscript((previous) => [...previous, ...committed]);\n for (const turn of committed) callbacksRef.current.onTranscript?.(turn);\n },\n [],\n );\n\n const start = useCallback(async () => {\n if (startingRef.current || roomRef.current) return;\n startingRef.current = true;\n setError(null);\n setSessionId(null);\n setRoomName(null);\n setStatus('connecting');\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n let credentials: AthenaVoiceSessionCredentials;\n try {\n credentials = await callbacksRef.current.createSession(controller.signal);\n } catch (cause) {\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n fail(cause);\n return;\n }\n if (controller.signal.aborted) return;\n\n setSessionId(credentials.asset_id ?? null);\n setRoomName(credentials.room_name ?? null);\n\n const room: Room = createRoom ? createRoom() : new Room();\n roomRef.current = room;\n\n room\n .on(RoomEvent.TrackSubscribed, attachTrack)\n .on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n const first: Participant | undefined = speakers[0];\n setActiveSpeaker(first ? speakerOf(first, room) : null);\n })\n .on(RoomEvent.TranscriptionReceived, (segments, participant) =>\n handleTranscription(segments, participant, room),\n )\n .on(RoomEvent.Reconnecting, () => setStatus('reconnecting'))\n .on(RoomEvent.Reconnected, () => setStatus('connected'))\n .on(RoomEvent.Disconnected, () => {\n if (roomRef.current !== room) return;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n setActiveSpeaker(null);\n setStatus('ended');\n })\n .on(RoomEvent.MediaDevicesError, fail);\n\n try {\n await room.connect(credentials.server_url, credentials.token);\n if (controller.signal.aborted) return;\n if (autoEnableMicrophone) {\n await room.localParticipant.setMicrophoneEnabled(true);\n setIsMuted(false);\n } else {\n setIsMuted(true);\n }\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n setStatus('connected');\n } catch (cause) {\n if (controller.signal.aborted) return;\n await teardown();\n fail(cause);\n }\n }, [\n attachTrack,\n autoEnableMicrophone,\n createRoom,\n detachAudio,\n fail,\n handleTranscription,\n teardown,\n ]);\n\n const setMicrophone = useCallback(async (enabled: boolean) => {\n const room: Room | null = roomRef.current;\n if (!room) return;\n await room.localParticipant.setMicrophoneEnabled(enabled);\n setIsMuted(!enabled);\n }, []);\n const mute = useCallback(() => setMicrophone(false), [setMicrophone]);\n const unmute = useCallback(() => setMicrophone(true), [setMicrophone]);\n\n const clearTranscript = useCallback(() => {\n committedSegmentIdsRef.current.clear();\n setTranscript([]);\n }, []);\n\n // Unmount teardown is deferred one tick so StrictMode's synchronous\n // unmount/remount of the same instance does not abort an in-flight\n // `createSession` (which would dispatch a second Orpheus agent on remount).\n const unmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n if (unmountTimerRef.current !== null) {\n clearTimeout(unmountTimerRef.current);\n unmountTimerRef.current = null;\n }\n return () => {\n unmountTimerRef.current = setTimeout(() => {\n unmountTimerRef.current = null;\n void teardown();\n }, 0);\n };\n }, [teardown]);\n\n return useMemo<UseAthenaVoiceSessionReturn>(\n () => ({\n status,\n isConnecting: status === 'connecting',\n isConnected: status === 'connected' || status === 'reconnecting',\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n }),\n [\n status,\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n ],\n );\n};\n"],"names":["useParentBridge","useRef","useEffect","useCallback","useState","Track","Room","RoomEvent","useMemo"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-athena-voice-session.cjs","sources":["../../src/voice/use-athena-voice-session.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteTrack,\n type TranscriptionSegment,\n} from 'livekit-client';\nimport { useParentBridge, type ParentBridgeState } from '../runtime/auth';\n\n/**\n * Realtime connection details for one Athena voice session. This is the shape\n * the Athena voice session endpoint returns; the hook only reads these\n * fields, so any server that proxies that call can hand it straight back.\n */\nexport interface AthenaVoiceSessionCredentials {\n /** Realtime signalling URL (`wss://…`). */\n server_url: string;\n /** Short-lived participant JWT scoped to `room_name`. */\n token: string;\n room_name?: string;\n /**\n * Athena session asset the voice run is persisted into. Use it with\n * `GET /api/v0/sessions/{asset_id}/download` to retrieve the transcript\n * after the call.\n */\n asset_id?: string;\n}\n\nexport type AthenaVoiceSessionStatus =\n | 'idle'\n | 'connecting'\n | 'connected'\n | 'reconnecting'\n | 'ended'\n | 'error';\n\nexport type AthenaVoiceSpeaker = 'user' | 'agent';\n\n/** One committed (final) transcription segment from either side of the call. */\nexport interface AthenaVoiceTranscriptTurn {\n id: string;\n speaker: AthenaVoiceSpeaker;\n text: string;\n /** Segment start, in ms since the room's epoch. */\n startTime: number;\n endTime: number;\n language: string;\n}\n\n/**\n * A voice tool the Athena voice agent can run. These are voice tool\n * names, not the chat `Toolkits` ids: `'web_search_browse_toolkit'` is not a\n * voice tool, `'web_search'` is. Only these names are accepted.\n *\n * The agent also has tools that call back into the client over room RPC\n * (clipboard, selection, screen contents, opening assets). This hook registers\n * no RPC handlers, so they are not part of its contract.\n */\nexport type AthenaVoiceToolId =\n /** Web search. */\n | 'web_search'\n /** Legacy alias of `web_search`; prefer `web_search`. */\n | 'search_web'\n /** Fetch and read one URL. */\n | 'browse'\n /** The workspace knowledge base, where configured. */\n | 'search_knowledge_base'\n /** Same knowledge base, thorough long-running retrieval. */\n | 'search_knowledge_base_lr'\n /** Read Athena assets by id, as the viewer. */\n | 'read_full_asset'\n /** Search the viewer's Athena assets. */\n | 'search_assets'\n /** Hand a question to the general agent and speak its answer. */\n | 'ask_agent'\n /** Save a note for the user (a stub on the worker today). */\n | 'save_note'\n /** Current date and time, Eastern. */\n | 'get_current_time_est';\n\n/**\n * Voice agent configuration for a session the hook creates itself (see\n * `UseAthenaVoiceSessionOptions.session`). Mirrors the body of the Athena\n * voice session endpoint; every field is optional and falls back to the\n * server default.\n */\nexport interface AthenaVoiceSessionConfig {\n /** Persona / instructions for the voice agent. */\n system_prompt?: string;\n /** Realtime provider, e.g. `'openai'` or `'google'`. */\n provider?: string;\n /** Provider model id, e.g. `'gpt-realtime-2'`. */\n model?: string;\n /** Provider voice id, e.g. `'marin'` (OpenAI) or `'Aoede'` (Google). */\n voice?: string;\n temperature?: number;\n /** BCP-47 language tag; default `'en'`. */\n language?: string;\n /**\n * Voice tools the agent may call. Omit it for the server's default set\n * (`web_search`, `search_knowledge_base`, `ask_agent`, …); pass `[]` for a\n * pure conversation with no tool calls.\n */\n enabled_tools?: AthenaVoiceToolId[];\n /** IANA timezone of the speaker, e.g. `'America/New_York'`. */\n timezone?: string;\n}\n\nexport interface UseAthenaVoiceSessionOptions {\n /**\n * Creates the voice session. Omit it when the app is served from a\n * Marathon computer-asset URL: the hook then calls the Athena voice session\n * endpoint itself with the viewer's bearer token from `/_athena/auth`, so\n * the session is created as — and shows up in the sessions of — whoever is\n * using the app.\n *\n * Works for workspace members and external SSO viewers alike: the scoped\n * `athview_` viewer credential is authorized for the voice session route and\n * `agents/general/invoke` the same way it is for `/api/v0/sessions`.\n *\n * Supply it when the app runs anywhere else (standalone, custom auth): it\n * should run on the caller's own server so the Athena API key never reaches\n * the browser. The hook only reads the returned credentials.\n *\n * Decide once: switching between supplying and omitting it across renders\n * of the same component is not supported.\n */\n createSession?: (\n signal: AbortSignal,\n ) => Promise<AthenaVoiceSessionCredentials>;\n /**\n * Agent configuration for sessions the hook creates itself. Ignored when\n * `createSession` is supplied.\n */\n session?: AthenaVoiceSessionConfig;\n /** Called once per committed transcript turn, in arrival order. */\n onTranscript?: (turn: AthenaVoiceTranscriptTurn) => void;\n /** Called with interim (non-final) transcript text as it streams in. */\n onInterimTranscript?: (speaker: AthenaVoiceSpeaker, text: string) => void;\n onError?: (error: Error) => void;\n /**\n * Where to play the agent's audio. Defaults to a detached `<audio>` element\n * the hook creates and removes itself.\n */\n audioElement?: HTMLMediaElement | null;\n /** Enable the microphone as soon as the room connects. Default `true`. */\n autoEnableMicrophone?: boolean;\n /** @internal Overrides `new Room()`; used by tests. */\n createRoom?: () => Room;\n /** @internal Overrides `useParentBridge()`; used by tests. */\n useBridge?: () => ParentBridgeState;\n}\n\nexport interface UseAthenaVoiceSessionReturn {\n status: AthenaVoiceSessionStatus;\n /** True between `start()` resolving the session and the room connecting. */\n isConnecting: boolean;\n isConnected: boolean;\n /** Who is speaking right now, or `null` when the room is quiet. */\n activeSpeaker: AthenaVoiceSpeaker | null;\n isMuted: boolean;\n /** Committed transcript turns so far, in arrival order. */\n transcript: AthenaVoiceTranscriptTurn[];\n /** Athena session asset id, once the session has been created. */\n sessionId: string | null;\n roomName: string | null;\n error: Error | null;\n /** Creates a session and joins the room. No-op while a session is live. */\n start: () => Promise<void>;\n /** Leaves the room and stops local tracks. Safe to call at any time. */\n stop: () => Promise<void>;\n mute: () => Promise<void>;\n unmute: () => Promise<void>;\n /** Clears `transcript`; does not affect the live session. */\n clearTranscript: () => void;\n}\n\nconst toError = (value: unknown): Error =>\n value instanceof Error ? value : new Error(String(value));\n\nconst NO_BRIDGE: ParentBridgeState = {\n token: null,\n statewireSyncUrl: null,\n backendBaseUrl: null,\n appUrl: null,\n hostAnswered: false,\n ready: true,\n};\n// Stands in for `useParentBridge` when the caller creates sessions itself, so\n// the hook never fetches `/_athena/auth` it would not use.\nconst useNoBridge = (): ParentBridgeState => NO_BRIDGE;\n\nconst NO_VIEWER_TOKEN_MESSAGE =\n 'useAthenaVoiceSession: no Athena viewer token. Either serve the app from ' +\n 'a Marathon computer-asset URL (which mints one via /_athena/auth) or pass ' +\n '`createSession` to create the session through your own server.';\n\n/**\n * Creates an Athena voice session as the current viewer. Exported so apps with\n * their own bridge state (or a server proxy holding a user bearer) can reuse\n * the exact request the hook makes.\n */\nexport const createAthenaVoiceSession = async ({\n backendBaseUrl,\n token,\n session = {},\n signal,\n}: {\n /** Bare Athena API base, e.g. `https://api.athenaintel.com`. */\n backendBaseUrl: string;\n /** Bearer token for the user the session belongs to. */\n token: string;\n session?: AthenaVoiceSessionConfig;\n signal?: AbortSignal;\n}): Promise<AthenaVoiceSessionCredentials> => {\n const url: string = `${backendBaseUrl.replace(/\\/+$/, '')}/api/orpheus/create-session`;\n const response: Response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(session),\n signal,\n });\n if (!response.ok) {\n const detail: string = await response.text().catch(() => '');\n throw new Error(\n `Athena voice session creation failed (${response.status})${detail ? `: ${detail}` : ''}`,\n );\n }\n return (await response.json()) as AthenaVoiceSessionCredentials;\n};\n\nconst speakerOf = (\n participant: Participant | undefined,\n room: Room,\n): AthenaVoiceSpeaker =>\n participant !== undefined &&\n participant.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n\n/**\n * Runs one Athena voice session from React.\n *\n * Owns the whole browser-side lifecycle the voice API leaves to the caller:\n * creating the session through the app's own server, joining the room,\n * publishing the microphone, playing the agent's audio, tracking who is\n * speaking, committing transcript turns from room transcription events, and\n * tearing everything down on `stop()` or unmount. A second `start()` while a\n * session is live (or still connecting) is ignored, so React StrictMode's\n * double-invoked effects cannot dispatch two agents into one room.\n */\nexport const useAthenaVoiceSession = (\n options: UseAthenaVoiceSessionOptions = {},\n): UseAthenaVoiceSessionReturn => {\n const {\n createSession,\n session,\n onTranscript,\n onInterimTranscript,\n onError,\n audioElement,\n autoEnableMicrophone = true,\n createRoom,\n useBridge = createSession ? useNoBridge : useParentBridge,\n } = options;\n\n // Viewer identity minted by Marathon (`/_athena/auth`) or the Athena app\n // iframe bridge. Only consulted when the caller did not supply `createSession`.\n const bridge: ParentBridgeState = useBridge();\n const bridgeRef = useRef<ParentBridgeState>(bridge);\n bridgeRef.current = bridge;\n const bridgeReadyWaitersRef = useRef<Array<() => void>>([]);\n useEffect(() => {\n if (!bridge.ready) return;\n const waiters: Array<() => void> = bridgeReadyWaitersRef.current;\n bridgeReadyWaitersRef.current = [];\n for (const resolve of waiters) resolve();\n }, [bridge.ready]);\n\n const sessionRef = useRef<AthenaVoiceSessionConfig | undefined>(session);\n sessionRef.current = session;\n\n const createSessionAsViewer = useCallback(\n async (signal: AbortSignal): Promise<AthenaVoiceSessionCredentials> => {\n if (!bridgeRef.current.ready) {\n await new Promise<void>((resolve) => {\n bridgeReadyWaitersRef.current.push(resolve);\n signal.addEventListener('abort', () => resolve(), { once: true });\n });\n if (signal.aborted) throw new Error('aborted');\n }\n const { token, backendBaseUrl } = bridgeRef.current;\n if (!token || !backendBaseUrl) throw new Error(NO_VIEWER_TOKEN_MESSAGE);\n return createAthenaVoiceSession({\n backendBaseUrl,\n token,\n session: sessionRef.current,\n signal,\n });\n },\n [],\n );\n\n const [status, setStatus] = useState<AthenaVoiceSessionStatus>('idle');\n const [activeSpeaker, setActiveSpeaker] = useState<AthenaVoiceSpeaker | null>(\n null,\n );\n const [isMuted, setIsMuted] = useState<boolean>(false);\n const [transcript, setTranscript] = useState<AthenaVoiceTranscriptTurn[]>([]);\n const [sessionId, setSessionId] = useState<string | null>(null);\n const [roomName, setRoomName] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Latest callbacks without re-subscribing room listeners on every render.\n const callbacksRef = useRef({\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n });\n callbacksRef.current = {\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n };\n\n const roomRef = useRef<Room | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const attachedElementsRef = useRef<HTMLMediaElement[]>([]);\n const ownedAudioRef = useRef<HTMLMediaElement | null>(null);\n const committedSegmentIdsRef = useRef<Set<string>>(new Set());\n // Guards `start()` against re-entry from the moment it is called, before any\n // state update lands — a StrictMode double effect fires both calls\n // synchronously in the same tick.\n const startingRef = useRef<boolean>(false);\n\n const fail = useCallback((cause: unknown) => {\n const err: Error = toError(cause);\n setError(err);\n setStatus('error');\n callbacksRef.current.onError?.(err);\n }, []);\n\n const detachAudio = useCallback(() => {\n for (const element of attachedElementsRef.current) {\n element.srcObject = null;\n if (element === ownedAudioRef.current) element.remove();\n }\n attachedElementsRef.current = [];\n ownedAudioRef.current = null;\n }, []);\n\n const teardown = useCallback(async () => {\n abortRef.current?.abort();\n abortRef.current = null;\n const room: Room | null = roomRef.current;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n if (room) {\n room.removeAllListeners();\n await room.disconnect(true);\n }\n }, [detachAudio]);\n\n const stop = useCallback(async () => {\n const hadRoom: boolean =\n roomRef.current !== null || abortRef.current !== null;\n await teardown();\n setActiveSpeaker(null);\n if (hadRoom) setStatus('ended');\n }, [teardown]);\n\n const attachTrack = useCallback(\n (track: RemoteTrack) => {\n if (track.kind !== Track.Kind.Audio) return;\n if (audioElement) {\n track.attach(audioElement);\n attachedElementsRef.current.push(audioElement);\n return;\n }\n const element: HTMLMediaElement = track.attach();\n element.setAttribute('data-athena-voice-audio', '');\n document.body.appendChild(element);\n ownedAudioRef.current = element;\n attachedElementsRef.current.push(element);\n },\n [audioElement],\n );\n\n const handleTranscription = useCallback(\n (\n segments: TranscriptionSegment[],\n participant: Participant | undefined,\n room: Room,\n ) => {\n const speaker: AthenaVoiceSpeaker = speakerOf(participant, room);\n const committed: AthenaVoiceTranscriptTurn[] = [];\n for (const segment of segments) {\n if (!segment.final) {\n callbacksRef.current.onInterimTranscript?.(speaker, segment.text);\n continue;\n }\n if (committedSegmentIdsRef.current.has(segment.id)) continue;\n committedSegmentIdsRef.current.add(segment.id);\n committed.push({\n id: segment.id,\n speaker,\n text: segment.text,\n startTime: segment.startTime,\n endTime: segment.endTime,\n language: segment.language,\n });\n }\n if (committed.length === 0) return;\n setTranscript((previous) => [...previous, ...committed]);\n for (const turn of committed) callbacksRef.current.onTranscript?.(turn);\n },\n [],\n );\n\n const start = useCallback(async () => {\n if (startingRef.current || roomRef.current) return;\n startingRef.current = true;\n setError(null);\n setSessionId(null);\n setRoomName(null);\n setStatus('connecting');\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n let credentials: AthenaVoiceSessionCredentials;\n try {\n credentials = await callbacksRef.current.createSession(controller.signal);\n } catch (cause) {\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n fail(cause);\n return;\n }\n if (controller.signal.aborted) return;\n\n setSessionId(credentials.asset_id ?? null);\n setRoomName(credentials.room_name ?? null);\n\n const room: Room = createRoom ? createRoom() : new Room();\n roomRef.current = room;\n\n room\n .on(RoomEvent.TrackSubscribed, attachTrack)\n .on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n const first: Participant | undefined = speakers[0];\n setActiveSpeaker(first ? speakerOf(first, room) : null);\n })\n .on(RoomEvent.TranscriptionReceived, (segments, participant) =>\n handleTranscription(segments, participant, room),\n )\n .on(RoomEvent.Reconnecting, () => setStatus('reconnecting'))\n .on(RoomEvent.Reconnected, () => setStatus('connected'))\n .on(RoomEvent.Disconnected, () => {\n if (roomRef.current !== room) return;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n setActiveSpeaker(null);\n setStatus('ended');\n })\n .on(RoomEvent.MediaDevicesError, fail);\n\n try {\n await room.connect(credentials.server_url, credentials.token);\n if (controller.signal.aborted) return;\n if (autoEnableMicrophone) {\n await room.localParticipant.setMicrophoneEnabled(true);\n setIsMuted(false);\n } else {\n setIsMuted(true);\n }\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n setStatus('connected');\n } catch (cause) {\n if (controller.signal.aborted) return;\n await teardown();\n fail(cause);\n }\n }, [\n attachTrack,\n autoEnableMicrophone,\n createRoom,\n detachAudio,\n fail,\n handleTranscription,\n teardown,\n ]);\n\n const setMicrophone = useCallback(async (enabled: boolean) => {\n const room: Room | null = roomRef.current;\n if (!room) return;\n await room.localParticipant.setMicrophoneEnabled(enabled);\n setIsMuted(!enabled);\n }, []);\n const mute = useCallback(() => setMicrophone(false), [setMicrophone]);\n const unmute = useCallback(() => setMicrophone(true), [setMicrophone]);\n\n const clearTranscript = useCallback(() => {\n committedSegmentIdsRef.current.clear();\n setTranscript([]);\n }, []);\n\n // Unmount teardown is deferred one tick so StrictMode's synchronous\n // unmount/remount of the same instance does not abort an in-flight\n // `createSession` (which would dispatch a second voice agent on remount).\n const unmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n if (unmountTimerRef.current !== null) {\n clearTimeout(unmountTimerRef.current);\n unmountTimerRef.current = null;\n }\n return () => {\n unmountTimerRef.current = setTimeout(() => {\n unmountTimerRef.current = null;\n void teardown();\n }, 0);\n };\n }, [teardown]);\n\n return useMemo<UseAthenaVoiceSessionReturn>(\n () => ({\n status,\n isConnecting: status === 'connecting',\n isConnected: status === 'connected' || status === 'reconnecting',\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n }),\n [\n status,\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n ],\n );\n};\n"],"names":["useParentBridge","useRef","useEffect","useCallback","useState","Track","Room","RoomEvent","useMemo"],"mappings":";;;;;AAmLA,MAAM,UAAU,CAAC,UACf,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAE1D,MAAM,YAA+B;AAAA,EACnC,OAAO;AAAA,EACP,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,OAAO;AACT;AAGA,MAAM,cAAc,MAAyB;AAE7C,MAAM,0BACJ;AASK,MAAM,2BAA2B,OAAO;AAAA,EAC7C;AAAA,EACA;AAAA,EACA,UAAU,CAAA;AAAA,EACV;AACF,MAO8C;AAC5C,QAAM,MAAc,GAAG,eAAe,QAAQ,QAAQ,EAAE,CAAC;AACzD,QAAM,WAAqB,MAAM,MAAM,KAAK;AAAA,IAC1C,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,KAAK;AAAA,IAAA;AAAA,IAEhC,MAAM,KAAK,UAAU,OAAO;AAAA,IAC5B;AAAA,EAAA,CACD;AACD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,SAAiB,MAAM,SAAS,OAAO,MAAM,MAAM,EAAE;AAC3D,UAAM,IAAI;AAAA,MACR,yCAAyC,SAAS,MAAM,IAAI,SAAS,KAAK,MAAM,KAAK,EAAE;AAAA,IAAA;AAAA,EAE3F;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;AAEA,MAAM,YAAY,CAChB,aACA,SAEA,gBAAgB,UAChB,YAAY,aAAa,KAAK,iBAAiB,WAC3C,SACA;AAaC,MAAM,wBAAwB,CACnC,UAAwC,OACR;AAChC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,uBAAuB;AAAA,IACvB;AAAA,IACA,YAAY,gBAAgB,cAAcA,KAAAA;AAAAA,EAAA,IACxC;AAIJ,QAAM,SAA4B,UAAA;AAClC,QAAM,YAAYC,MAAAA,OAA0B,MAAM;AAClD,YAAU,UAAU;AACpB,QAAM,wBAAwBA,MAAAA,OAA0B,EAAE;AAC1DC,QAAAA,UAAU,MAAM;AACd,QAAI,CAAC,OAAO,MAAO;AACnB,UAAM,UAA6B,sBAAsB;AACzD,0BAAsB,UAAU,CAAA;AAChC,eAAW,WAAW,QAAS,SAAA;AAAA,EACjC,GAAG,CAAC,OAAO,KAAK,CAAC;AAEjB,QAAM,aAAaD,MAAAA,OAA6C,OAAO;AACvE,aAAW,UAAU;AAErB,QAAM,wBAAwBE,MAAAA;AAAAA,IAC5B,OAAO,WAAgE;AACrE,UAAI,CAAC,UAAU,QAAQ,OAAO;AAC5B,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,gCAAsB,QAAQ,KAAK,OAAO;AAC1C,iBAAO,iBAAiB,SAAS,MAAM,QAAA,GAAW,EAAE,MAAM,MAAM;AAAA,QAClE,CAAC;AACD,YAAI,OAAO,QAAS,OAAM,IAAI,MAAM,SAAS;AAAA,MAC/C;AACA,YAAM,EAAE,OAAO,eAAA,IAAmB,UAAU;AAC5C,UAAI,CAAC,SAAS,CAAC,eAAgB,OAAM,IAAI,MAAM,uBAAuB;AACtE,aAAO,yBAAyB;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,SAAS,WAAW;AAAA,QACpB;AAAA,MAAA,CACD;AAAA,IACH;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,CAAC,QAAQ,SAAS,IAAIC,MAAAA,SAAmC,MAAM;AACrE,QAAM,CAAC,eAAe,gBAAgB,IAAIA,MAAAA;AAAAA,IACxC;AAAA,EAAA;AAEF,QAAM,CAAC,SAAS,UAAU,IAAIA,MAAAA,SAAkB,KAAK;AACrD,QAAM,CAAC,YAAY,aAAa,IAAIA,MAAAA,SAAsC,CAAA,CAAE;AAC5E,QAAM,CAAC,WAAW,YAAY,IAAIA,MAAAA,SAAwB,IAAI;AAC9D,QAAM,CAAC,UAAU,WAAW,IAAIA,MAAAA,SAAwB,IAAI;AAC5D,QAAM,CAAC,OAAO,QAAQ,IAAIA,MAAAA,SAAuB,IAAI;AAGrD,QAAM,eAAeH,MAAAA,OAAO;AAAA,IAC1B,eAAe,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AACD,eAAa,UAAU;AAAA,IACrB,eAAe,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,QAAM,UAAUA,MAAAA,OAAoB,IAAI;AACxC,QAAM,WAAWA,MAAAA,OAA+B,IAAI;AACpD,QAAM,sBAAsBA,MAAAA,OAA2B,EAAE;AACzD,QAAM,gBAAgBA,MAAAA,OAAgC,IAAI;AAC1D,QAAM,yBAAyBA,MAAAA,OAAoB,oBAAI,KAAK;AAI5D,QAAM,cAAcA,MAAAA,OAAgB,KAAK;AAEzC,QAAM,OAAOE,kBAAY,CAAC,UAAmB;;AAC3C,UAAM,MAAa,QAAQ,KAAK;AAChC,aAAS,GAAG;AACZ,cAAU,OAAO;AACjB,6BAAa,SAAQ,YAArB,4BAA+B;AAAA,EACjC,GAAG,CAAA,CAAE;AAEL,QAAM,cAAcA,MAAAA,YAAY,MAAM;AACpC,eAAW,WAAW,oBAAoB,SAAS;AACjD,cAAQ,YAAY;AACpB,UAAI,YAAY,cAAc,QAAS,SAAQ,OAAA;AAAA,IACjD;AACA,wBAAoB,UAAU,CAAA;AAC9B,kBAAc,UAAU;AAAA,EAC1B,GAAG,CAAA,CAAE;AAEL,QAAM,WAAWA,MAAAA,YAAY,YAAY;;AACvC,mBAAS,YAAT,mBAAkB;AAClB,aAAS,UAAU;AACnB,UAAM,OAAoB,QAAQ;AAClC,YAAQ,UAAU;AAClB,gBAAY,UAAU;AACtB,gBAAA;AACA,QAAI,MAAM;AACR,WAAK,mBAAA;AACL,YAAM,KAAK,WAAW,IAAI;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,OAAOA,MAAAA,YAAY,YAAY;AACnC,UAAM,UACJ,QAAQ,YAAY,QAAQ,SAAS,YAAY;AACnD,UAAM,SAAA;AACN,qBAAiB,IAAI;AACrB,QAAI,mBAAmB,OAAO;AAAA,EAChC,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,cAAcA,MAAAA;AAAAA,IAClB,CAAC,UAAuB;AACtB,UAAI,MAAM,SAASE,oBAAM,KAAK,MAAO;AACrC,UAAI,cAAc;AAChB,cAAM,OAAO,YAAY;AACzB,4BAAoB,QAAQ,KAAK,YAAY;AAC7C;AAAA,MACF;AACA,YAAM,UAA4B,MAAM,OAAA;AACxC,cAAQ,aAAa,2BAA2B,EAAE;AAClD,eAAS,KAAK,YAAY,OAAO;AACjC,oBAAc,UAAU;AACxB,0BAAoB,QAAQ,KAAK,OAAO;AAAA,IAC1C;AAAA,IACA,CAAC,YAAY;AAAA,EAAA;AAGf,QAAM,sBAAsBF,MAAAA;AAAAA,IAC1B,CACE,UACA,aACA,SACG;;AACH,YAAM,UAA8B,UAAU,aAAa,IAAI;AAC/D,YAAM,YAAyC,CAAA;AAC/C,iBAAW,WAAW,UAAU;AAC9B,YAAI,CAAC,QAAQ,OAAO;AAClB,mCAAa,SAAQ,wBAArB,4BAA2C,SAAS,QAAQ;AAC5D;AAAA,QACF;AACA,YAAI,uBAAuB,QAAQ,IAAI,QAAQ,EAAE,EAAG;AACpD,+BAAuB,QAAQ,IAAI,QAAQ,EAAE;AAC7C,kBAAU,KAAK;AAAA,UACb,IAAI,QAAQ;AAAA,UACZ;AAAA,UACA,MAAM,QAAQ;AAAA,UACd,WAAW,QAAQ;AAAA,UACnB,SAAS,QAAQ;AAAA,UACjB,UAAU,QAAQ;AAAA,QAAA,CACnB;AAAA,MACH;AACA,UAAI,UAAU,WAAW,EAAG;AAC5B,oBAAc,CAAC,aAAa,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;AACvD,iBAAW,QAAQ,UAAW,0BAAa,SAAQ,iBAArB,4BAAoC;AAAA,IACpE;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,QAAQA,MAAAA,YAAY,YAAY;AACpC,QAAI,YAAY,WAAW,QAAQ,QAAS;AAC5C,gBAAY,UAAU;AACtB,aAAS,IAAI;AACb,iBAAa,IAAI;AACjB,gBAAY,IAAI;AAChB,cAAU,YAAY;AAEtB,UAAM,aAAa,IAAI,gBAAA;AACvB,aAAS,UAAU;AAEnB,QAAI;AACJ,QAAI;AACF,oBAAc,MAAM,aAAa,QAAQ,cAAc,WAAW,MAAM;AAAA,IAC1E,SAAS,OAAO;AACd,UAAI,WAAW,OAAO,QAAS;AAC/B,eAAS,UAAU;AACnB,kBAAY,UAAU;AACtB,WAAK,KAAK;AACV;AAAA,IACF;AACA,QAAI,WAAW,OAAO,QAAS;AAE/B,iBAAa,YAAY,YAAY,IAAI;AACzC,gBAAY,YAAY,aAAa,IAAI;AAEzC,UAAM,OAAa,aAAa,WAAA,IAAe,IAAIG,cAAAA,KAAA;AACnD,YAAQ,UAAU;AAElB,SACG,GAAGC,cAAAA,UAAU,iBAAiB,WAAW,EACzC,GAAGA,cAAAA,UAAU,uBAAuB,CAAC,aAA4B;AAChE,YAAM,QAAiC,SAAS,CAAC;AACjD,uBAAiB,QAAQ,UAAU,OAAO,IAAI,IAAI,IAAI;AAAA,IACxD,CAAC,EACA;AAAA,MAAGA,cAAAA,UAAU;AAAA,MAAuB,CAAC,UAAU,gBAC9C,oBAAoB,UAAU,aAAa,IAAI;AAAA,IAAA,EAEhD,GAAGA,cAAAA,UAAU,cAAc,MAAM,UAAU,cAAc,CAAC,EAC1D,GAAGA,cAAAA,UAAU,aAAa,MAAM,UAAU,WAAW,CAAC,EACtD,GAAGA,cAAAA,UAAU,cAAc,MAAM;AAChC,UAAI,QAAQ,YAAY,KAAM;AAC9B,cAAQ,UAAU;AAClB,kBAAY,UAAU;AACtB,kBAAA;AACA,uBAAiB,IAAI;AACrB,gBAAU,OAAO;AAAA,IACnB,CAAC,EACA,GAAGA,wBAAU,mBAAmB,IAAI;AAEvC,QAAI;AACF,YAAM,KAAK,QAAQ,YAAY,YAAY,YAAY,KAAK;AAC5D,UAAI,WAAW,OAAO,QAAS;AAC/B,UAAI,sBAAsB;AACxB,cAAM,KAAK,iBAAiB,qBAAqB,IAAI;AACrD,mBAAW,KAAK;AAAA,MAClB,OAAO;AACL,mBAAW,IAAI;AAAA,MACjB;AACA,UAAI,WAAW,OAAO,QAAS;AAC/B,eAAS,UAAU;AACnB,kBAAY,UAAU;AACtB,gBAAU,WAAW;AAAA,IACvB,SAAS,OAAO;AACd,UAAI,WAAW,OAAO,QAAS;AAC/B,YAAM,SAAA;AACN,WAAK,KAAK;AAAA,IACZ;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AAED,QAAM,gBAAgBJ,kBAAY,OAAO,YAAqB;AAC5D,UAAM,OAAoB,QAAQ;AAClC,QAAI,CAAC,KAAM;AACX,UAAM,KAAK,iBAAiB,qBAAqB,OAAO;AACxD,eAAW,CAAC,OAAO;AAAA,EACrB,GAAG,CAAA,CAAE;AACL,QAAM,OAAOA,MAAAA,YAAY,MAAM,cAAc,KAAK,GAAG,CAAC,aAAa,CAAC;AACpE,QAAM,SAASA,MAAAA,YAAY,MAAM,cAAc,IAAI,GAAG,CAAC,aAAa,CAAC;AAErE,QAAM,kBAAkBA,MAAAA,YAAY,MAAM;AACxC,2BAAuB,QAAQ,MAAA;AAC/B,kBAAc,CAAA,CAAE;AAAA,EAClB,GAAG,CAAA,CAAE;AAKL,QAAM,kBAAkBF,MAAAA,OAA6C,IAAI;AACzEC,QAAAA,UAAU,MAAM;AACd,QAAI,gBAAgB,YAAY,MAAM;AACpC,mBAAa,gBAAgB,OAAO;AACpC,sBAAgB,UAAU;AAAA,IAC5B;AACA,WAAO,MAAM;AACX,sBAAgB,UAAU,WAAW,MAAM;AACzC,wBAAgB,UAAU;AAC1B,aAAK,SAAA;AAAA,MACP,GAAG,CAAC;AAAA,IACN;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAOM,MAAAA;AAAAA,IACL,OAAO;AAAA,MACL;AAAA,MACA,cAAc,WAAW;AAAA,MACzB,aAAa,WAAW,eAAe,WAAW;AAAA,MAClD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAEF;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EACF;AAEJ;;;"}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { Room } from 'livekit-client';
|
|
2
2
|
import { type ParentBridgeState } from '../runtime/auth';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Realtime connection details for one Athena voice session. This is the shape
|
|
5
|
+
* the Athena voice session endpoint returns; the hook only reads these
|
|
6
|
+
* fields, so any server that proxies that call can hand it straight back.
|
|
7
7
|
*/
|
|
8
8
|
export interface AthenaVoiceSessionCredentials {
|
|
9
|
-
/**
|
|
9
|
+
/** Realtime signalling URL (`wss://…`). */
|
|
10
10
|
server_url: string;
|
|
11
|
-
/** Short-lived
|
|
11
|
+
/** Short-lived participant JWT scoped to `room_name`. */
|
|
12
12
|
token: string;
|
|
13
13
|
room_name?: string;
|
|
14
14
|
/**
|
|
@@ -25,29 +25,28 @@ export interface AthenaVoiceTranscriptTurn {
|
|
|
25
25
|
id: string;
|
|
26
26
|
speaker: AthenaVoiceSpeaker;
|
|
27
27
|
text: string;
|
|
28
|
-
/** Segment start, in ms since the
|
|
28
|
+
/** Segment start, in ms since the room's epoch. */
|
|
29
29
|
startTime: number;
|
|
30
30
|
endTime: number;
|
|
31
31
|
language: string;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
* A voice tool the
|
|
34
|
+
* A voice tool the Athena voice agent can run. These are voice tool
|
|
35
35
|
* names, not the chat `Toolkits` ids: `'web_search_browse_toolkit'` is not a
|
|
36
36
|
* voice tool, `'web_search'` is. Only these names are accepted.
|
|
37
37
|
*
|
|
38
|
-
* The
|
|
39
|
-
* (
|
|
40
|
-
*
|
|
41
|
-
* so they are not part of its contract.
|
|
38
|
+
* The agent also has tools that call back into the client over room RPC
|
|
39
|
+
* (clipboard, selection, screen contents, opening assets). This hook registers
|
|
40
|
+
* no RPC handlers, so they are not part of its contract.
|
|
42
41
|
*/
|
|
43
42
|
export type AthenaVoiceToolId =
|
|
44
|
-
/** Web search
|
|
43
|
+
/** Web search. */
|
|
45
44
|
'web_search'
|
|
46
45
|
/** Legacy alias of `web_search`; prefer `web_search`. */
|
|
47
46
|
| 'search_web'
|
|
48
47
|
/** Fetch and read one URL. */
|
|
49
48
|
| 'browse'
|
|
50
|
-
/** The workspace knowledge base
|
|
49
|
+
/** The workspace knowledge base, where configured. */
|
|
51
50
|
| 'search_knowledge_base'
|
|
52
51
|
/** Same knowledge base, thorough long-running retrieval. */
|
|
53
52
|
| 'search_knowledge_base_lr'
|
|
@@ -62,10 +61,10 @@ export type AthenaVoiceToolId =
|
|
|
62
61
|
/** Current date and time, Eastern. */
|
|
63
62
|
| 'get_current_time_est';
|
|
64
63
|
/**
|
|
65
|
-
*
|
|
66
|
-
* `UseAthenaVoiceSessionOptions.session`). Mirrors the body of
|
|
67
|
-
*
|
|
68
|
-
*
|
|
64
|
+
* Voice agent configuration for a session the hook creates itself (see
|
|
65
|
+
* `UseAthenaVoiceSessionOptions.session`). Mirrors the body of the Athena
|
|
66
|
+
* voice session endpoint; every field is optional and falls back to the
|
|
67
|
+
* server default.
|
|
69
68
|
*/
|
|
70
69
|
export interface AthenaVoiceSessionConfig {
|
|
71
70
|
/** Persona / instructions for the voice agent. */
|
|
@@ -90,14 +89,14 @@ export interface AthenaVoiceSessionConfig {
|
|
|
90
89
|
}
|
|
91
90
|
export interface UseAthenaVoiceSessionOptions {
|
|
92
91
|
/**
|
|
93
|
-
* Creates the
|
|
94
|
-
* Marathon computer-asset URL: the hook then calls
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
92
|
+
* Creates the voice session. Omit it when the app is served from a
|
|
93
|
+
* Marathon computer-asset URL: the hook then calls the Athena voice session
|
|
94
|
+
* endpoint itself with the viewer's bearer token from `/_athena/auth`, so
|
|
95
|
+
* the session is created as — and shows up in the sessions of — whoever is
|
|
96
|
+
* using the app.
|
|
98
97
|
*
|
|
99
98
|
* Works for workspace members and external SSO viewers alike: the scoped
|
|
100
|
-
* `athview_` viewer credential is authorized for the
|
|
99
|
+
* `athview_` viewer credential is authorized for the voice session route and
|
|
101
100
|
* `agents/general/invoke` the same way it is for `/api/v0/sessions`.
|
|
102
101
|
*
|
|
103
102
|
* Supply it when the app runs anywhere else (standalone, custom auth): it
|
|
@@ -154,12 +153,12 @@ export interface UseAthenaVoiceSessionReturn {
|
|
|
154
153
|
clearTranscript: () => void;
|
|
155
154
|
}
|
|
156
155
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
156
|
+
* Creates an Athena voice session as the current viewer. Exported so apps with
|
|
157
|
+
* their own bridge state (or a server proxy holding a user bearer) can reuse
|
|
158
|
+
* the exact request the hook makes.
|
|
160
159
|
*/
|
|
161
160
|
export declare const createAthenaVoiceSession: ({ backendBaseUrl, token, session, signal, }: {
|
|
162
|
-
/** Bare
|
|
161
|
+
/** Bare Athena API base, e.g. `https://api.athenaintel.com`. */
|
|
163
162
|
backendBaseUrl: string;
|
|
164
163
|
/** Bearer token for the user the session belongs to. */
|
|
165
164
|
token: string;
|
|
@@ -167,12 +166,12 @@ export declare const createAthenaVoiceSession: ({ backendBaseUrl, token, session
|
|
|
167
166
|
signal?: AbortSignal;
|
|
168
167
|
}) => Promise<AthenaVoiceSessionCredentials>;
|
|
169
168
|
/**
|
|
170
|
-
* Runs one Athena voice session
|
|
169
|
+
* Runs one Athena voice session from React.
|
|
171
170
|
*
|
|
172
|
-
* Owns the whole browser-side lifecycle the
|
|
173
|
-
* creating the session through the app's own server, joining the
|
|
171
|
+
* Owns the whole browser-side lifecycle the voice API leaves to the caller:
|
|
172
|
+
* creating the session through the app's own server, joining the room,
|
|
174
173
|
* publishing the microphone, playing the agent's audio, tracking who is
|
|
175
|
-
* speaking, committing transcript turns from
|
|
174
|
+
* speaking, committing transcript turns from room transcription events, and
|
|
176
175
|
* tearing everything down on `stop()` or unmount. A second `start()` while a
|
|
177
176
|
* session is live (or still connecting) is ignored, so React StrictMode's
|
|
178
177
|
* double-invoked effects cannot dispatch two agents into one room.
|
|
@@ -31,7 +31,7 @@ const createAthenaVoiceSession = async ({
|
|
|
31
31
|
if (!response.ok) {
|
|
32
32
|
const detail = await response.text().catch(() => "");
|
|
33
33
|
throw new Error(
|
|
34
|
-
`
|
|
34
|
+
`Athena voice session creation failed (${response.status})${detail ? `: ${detail}` : ""}`
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
return await response.json();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-athena-voice-session.js","sources":["../../src/voice/use-athena-voice-session.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteTrack,\n type TranscriptionSegment,\n} from 'livekit-client';\nimport { useParentBridge, type ParentBridgeState } from '../runtime/auth';\n\n/**\n * LiveKit connection details for one Athena voice session. This is the shape\n * Orpheus returns from `POST /api/orpheus/create-session`; the hook only reads\n * these fields, so any server that proxies that call can hand it straight back.\n */\nexport interface AthenaVoiceSessionCredentials {\n /** LiveKit signalling URL (`wss://…`). */\n server_url: string;\n /** Short-lived LiveKit participant JWT scoped to `room_name`. */\n token: string;\n room_name?: string;\n /**\n * Athena session asset the voice run is persisted into. Use it with\n * `GET /api/v0/sessions/{asset_id}/download` to retrieve the transcript\n * after the call.\n */\n asset_id?: string;\n}\n\nexport type AthenaVoiceSessionStatus =\n | 'idle'\n | 'connecting'\n | 'connected'\n | 'reconnecting'\n | 'ended'\n | 'error';\n\nexport type AthenaVoiceSpeaker = 'user' | 'agent';\n\n/** One committed (final) transcription segment from either side of the call. */\nexport interface AthenaVoiceTranscriptTurn {\n id: string;\n speaker: AthenaVoiceSpeaker;\n text: string;\n /** Segment start, in ms since the LiveKit room's epoch. */\n startTime: number;\n endTime: number;\n language: string;\n}\n\n/**\n * A voice tool the Orpheus worker can run for the agent. These are voice tool\n * names, not the chat `Toolkits` ids: `'web_search_browse_toolkit'` is not a\n * voice tool, `'web_search'` is. Only these names are accepted.\n *\n * The worker also has tools that call back into the client over LiveKit RPC\n * (`write_to_clipboard`, `replace_current_selection`, `open_in_olympus`,\n * `open_asset`, `get_screen_contents`). This hook registers no RPC handlers,\n * so they are not part of its contract.\n */\nexport type AthenaVoiceToolId =\n /** Web search (Exa). */\n | 'web_search'\n /** Legacy alias of `web_search`; prefer `web_search`. */\n | 'search_web'\n /** Fetch and read one URL. */\n | 'browse'\n /** The workspace knowledge base (Ragie), where configured. */\n | 'search_knowledge_base'\n /** Same knowledge base, thorough long-running retrieval. */\n | 'search_knowledge_base_lr'\n /** Read Athena assets by id, as the viewer. */\n | 'read_full_asset'\n /** Search the viewer's Athena assets. */\n | 'search_assets'\n /** Hand a question to the general agent and speak its answer. */\n | 'ask_agent'\n /** Save a note for the user (a stub on the worker today). */\n | 'save_note'\n /** Current date and time, Eastern. */\n | 'get_current_time_est';\n\n/**\n * Orpheus agent configuration for a session the hook creates itself (see\n * `UseAthenaVoiceSessionOptions.session`). Mirrors the body of\n * `POST /api/orpheus/create-session`; every field is optional and falls back\n * to the server default.\n */\nexport interface AthenaVoiceSessionConfig {\n /** Persona / instructions for the voice agent. */\n system_prompt?: string;\n /** Realtime provider, e.g. `'openai'` or `'google'`. */\n provider?: string;\n /** Provider model id, e.g. `'gpt-realtime-2'`. */\n model?: string;\n /** Provider voice id, e.g. `'marin'` (OpenAI) or `'Aoede'` (Google). */\n voice?: string;\n temperature?: number;\n /** BCP-47 language tag; default `'en'`. */\n language?: string;\n /**\n * Voice tools the agent may call. Omit it for the server's default set\n * (`web_search`, `search_knowledge_base`, `ask_agent`, …); pass `[]` for a\n * pure conversation with no tool calls.\n */\n enabled_tools?: AthenaVoiceToolId[];\n /** IANA timezone of the speaker, e.g. `'America/New_York'`. */\n timezone?: string;\n}\n\nexport interface UseAthenaVoiceSessionOptions {\n /**\n * Creates the LiveKit session. Omit it when the app is served from a\n * Marathon computer-asset URL: the hook then calls\n * `POST {backendBaseUrl}/api/orpheus/create-session` itself with the\n * viewer's bearer token from `/_athena/auth`, so the session is created as\n * — and shows up in the sessions of — whoever is using the app.\n *\n * Works for workspace members and external SSO viewers alike: the scoped\n * `athview_` viewer credential is authorized for the Orpheus route and\n * `agents/general/invoke` the same way it is for `/api/v0/sessions`.\n *\n * Supply it when the app runs anywhere else (standalone, custom auth): it\n * should run on the caller's own server so the Athena API key never reaches\n * the browser. The hook only reads the returned credentials.\n *\n * Decide once: switching between supplying and omitting it across renders\n * of the same component is not supported.\n */\n createSession?: (\n signal: AbortSignal,\n ) => Promise<AthenaVoiceSessionCredentials>;\n /**\n * Agent configuration for sessions the hook creates itself. Ignored when\n * `createSession` is supplied.\n */\n session?: AthenaVoiceSessionConfig;\n /** Called once per committed transcript turn, in arrival order. */\n onTranscript?: (turn: AthenaVoiceTranscriptTurn) => void;\n /** Called with interim (non-final) transcript text as it streams in. */\n onInterimTranscript?: (speaker: AthenaVoiceSpeaker, text: string) => void;\n onError?: (error: Error) => void;\n /**\n * Where to play the agent's audio. Defaults to a detached `<audio>` element\n * the hook creates and removes itself.\n */\n audioElement?: HTMLMediaElement | null;\n /** Enable the microphone as soon as the room connects. Default `true`. */\n autoEnableMicrophone?: boolean;\n /** @internal Overrides `new Room()`; used by tests. */\n createRoom?: () => Room;\n /** @internal Overrides `useParentBridge()`; used by tests. */\n useBridge?: () => ParentBridgeState;\n}\n\nexport interface UseAthenaVoiceSessionReturn {\n status: AthenaVoiceSessionStatus;\n /** True between `start()` resolving the session and the room connecting. */\n isConnecting: boolean;\n isConnected: boolean;\n /** Who is speaking right now, or `null` when the room is quiet. */\n activeSpeaker: AthenaVoiceSpeaker | null;\n isMuted: boolean;\n /** Committed transcript turns so far, in arrival order. */\n transcript: AthenaVoiceTranscriptTurn[];\n /** Athena session asset id, once the session has been created. */\n sessionId: string | null;\n roomName: string | null;\n error: Error | null;\n /** Creates a session and joins the room. No-op while a session is live. */\n start: () => Promise<void>;\n /** Leaves the room and stops local tracks. Safe to call at any time. */\n stop: () => Promise<void>;\n mute: () => Promise<void>;\n unmute: () => Promise<void>;\n /** Clears `transcript`; does not affect the live session. */\n clearTranscript: () => void;\n}\n\nconst toError = (value: unknown): Error =>\n value instanceof Error ? value : new Error(String(value));\n\nconst NO_BRIDGE: ParentBridgeState = {\n token: null,\n statewireSyncUrl: null,\n backendBaseUrl: null,\n appUrl: null,\n hostAnswered: false,\n ready: true,\n};\n// Stands in for `useParentBridge` when the caller creates sessions itself, so\n// the hook never fetches `/_athena/auth` it would not use.\nconst useNoBridge = (): ParentBridgeState => NO_BRIDGE;\n\nconst NO_VIEWER_TOKEN_MESSAGE =\n 'useAthenaVoiceSession: no Athena viewer token. Either serve the app from ' +\n 'a Marathon computer-asset URL (which mints one via /_athena/auth) or pass ' +\n '`createSession` to create the session through your own server.';\n\n/**\n * `POST /api/orpheus/create-session` as the current viewer. Exported so apps\n * with their own bridge state (or a server proxy holding a user bearer) can\n * reuse the exact request the hook makes.\n */\nexport const createAthenaVoiceSession = async ({\n backendBaseUrl,\n token,\n session = {},\n signal,\n}: {\n /** Bare Agora API base, e.g. `https://api.athenaintel.com`. */\n backendBaseUrl: string;\n /** Bearer token for the user the session belongs to. */\n token: string;\n session?: AthenaVoiceSessionConfig;\n signal?: AbortSignal;\n}): Promise<AthenaVoiceSessionCredentials> => {\n const url: string = `${backendBaseUrl.replace(/\\/+$/, '')}/api/orpheus/create-session`;\n const response: Response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(session),\n signal,\n });\n if (!response.ok) {\n const detail: string = await response.text().catch(() => '');\n throw new Error(\n `Orpheus create-session failed (${response.status})${detail ? `: ${detail}` : ''}`,\n );\n }\n return (await response.json()) as AthenaVoiceSessionCredentials;\n};\n\nconst speakerOf = (\n participant: Participant | undefined,\n room: Room,\n): AthenaVoiceSpeaker =>\n participant !== undefined &&\n participant.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n\n/**\n * Runs one Athena voice session (Orpheus agent over LiveKit) from React.\n *\n * Owns the whole browser-side lifecycle the Orpheus API leaves to the caller:\n * creating the session through the app's own server, joining the LiveKit room,\n * publishing the microphone, playing the agent's audio, tracking who is\n * speaking, committing transcript turns from LiveKit transcription events, and\n * tearing everything down on `stop()` or unmount. A second `start()` while a\n * session is live (or still connecting) is ignored, so React StrictMode's\n * double-invoked effects cannot dispatch two agents into one room.\n */\nexport const useAthenaVoiceSession = (\n options: UseAthenaVoiceSessionOptions = {},\n): UseAthenaVoiceSessionReturn => {\n const {\n createSession,\n session,\n onTranscript,\n onInterimTranscript,\n onError,\n audioElement,\n autoEnableMicrophone = true,\n createRoom,\n useBridge = createSession ? useNoBridge : useParentBridge,\n } = options;\n\n // Viewer identity minted by Marathon (`/_athena/auth`) or the Olympus iframe\n // bridge. Only consulted when the caller did not supply `createSession`.\n const bridge: ParentBridgeState = useBridge();\n const bridgeRef = useRef<ParentBridgeState>(bridge);\n bridgeRef.current = bridge;\n const bridgeReadyWaitersRef = useRef<Array<() => void>>([]);\n useEffect(() => {\n if (!bridge.ready) return;\n const waiters: Array<() => void> = bridgeReadyWaitersRef.current;\n bridgeReadyWaitersRef.current = [];\n for (const resolve of waiters) resolve();\n }, [bridge.ready]);\n\n const sessionRef = useRef<AthenaVoiceSessionConfig | undefined>(session);\n sessionRef.current = session;\n\n const createSessionAsViewer = useCallback(\n async (signal: AbortSignal): Promise<AthenaVoiceSessionCredentials> => {\n if (!bridgeRef.current.ready) {\n await new Promise<void>((resolve) => {\n bridgeReadyWaitersRef.current.push(resolve);\n signal.addEventListener('abort', () => resolve(), { once: true });\n });\n if (signal.aborted) throw new Error('aborted');\n }\n const { token, backendBaseUrl } = bridgeRef.current;\n if (!token || !backendBaseUrl) throw new Error(NO_VIEWER_TOKEN_MESSAGE);\n return createAthenaVoiceSession({\n backendBaseUrl,\n token,\n session: sessionRef.current,\n signal,\n });\n },\n [],\n );\n\n const [status, setStatus] = useState<AthenaVoiceSessionStatus>('idle');\n const [activeSpeaker, setActiveSpeaker] = useState<AthenaVoiceSpeaker | null>(\n null,\n );\n const [isMuted, setIsMuted] = useState<boolean>(false);\n const [transcript, setTranscript] = useState<AthenaVoiceTranscriptTurn[]>([]);\n const [sessionId, setSessionId] = useState<string | null>(null);\n const [roomName, setRoomName] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Latest callbacks without re-subscribing room listeners on every render.\n const callbacksRef = useRef({\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n });\n callbacksRef.current = {\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n };\n\n const roomRef = useRef<Room | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const attachedElementsRef = useRef<HTMLMediaElement[]>([]);\n const ownedAudioRef = useRef<HTMLMediaElement | null>(null);\n const committedSegmentIdsRef = useRef<Set<string>>(new Set());\n // Guards `start()` against re-entry from the moment it is called, before any\n // state update lands — a StrictMode double effect fires both calls\n // synchronously in the same tick.\n const startingRef = useRef<boolean>(false);\n\n const fail = useCallback((cause: unknown) => {\n const err: Error = toError(cause);\n setError(err);\n setStatus('error');\n callbacksRef.current.onError?.(err);\n }, []);\n\n const detachAudio = useCallback(() => {\n for (const element of attachedElementsRef.current) {\n element.srcObject = null;\n if (element === ownedAudioRef.current) element.remove();\n }\n attachedElementsRef.current = [];\n ownedAudioRef.current = null;\n }, []);\n\n const teardown = useCallback(async () => {\n abortRef.current?.abort();\n abortRef.current = null;\n const room: Room | null = roomRef.current;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n if (room) {\n room.removeAllListeners();\n await room.disconnect(true);\n }\n }, [detachAudio]);\n\n const stop = useCallback(async () => {\n const hadRoom: boolean =\n roomRef.current !== null || abortRef.current !== null;\n await teardown();\n setActiveSpeaker(null);\n if (hadRoom) setStatus('ended');\n }, [teardown]);\n\n const attachTrack = useCallback(\n (track: RemoteTrack) => {\n if (track.kind !== Track.Kind.Audio) return;\n if (audioElement) {\n track.attach(audioElement);\n attachedElementsRef.current.push(audioElement);\n return;\n }\n const element: HTMLMediaElement = track.attach();\n element.setAttribute('data-athena-voice-audio', '');\n document.body.appendChild(element);\n ownedAudioRef.current = element;\n attachedElementsRef.current.push(element);\n },\n [audioElement],\n );\n\n const handleTranscription = useCallback(\n (\n segments: TranscriptionSegment[],\n participant: Participant | undefined,\n room: Room,\n ) => {\n const speaker: AthenaVoiceSpeaker = speakerOf(participant, room);\n const committed: AthenaVoiceTranscriptTurn[] = [];\n for (const segment of segments) {\n if (!segment.final) {\n callbacksRef.current.onInterimTranscript?.(speaker, segment.text);\n continue;\n }\n if (committedSegmentIdsRef.current.has(segment.id)) continue;\n committedSegmentIdsRef.current.add(segment.id);\n committed.push({\n id: segment.id,\n speaker,\n text: segment.text,\n startTime: segment.startTime,\n endTime: segment.endTime,\n language: segment.language,\n });\n }\n if (committed.length === 0) return;\n setTranscript((previous) => [...previous, ...committed]);\n for (const turn of committed) callbacksRef.current.onTranscript?.(turn);\n },\n [],\n );\n\n const start = useCallback(async () => {\n if (startingRef.current || roomRef.current) return;\n startingRef.current = true;\n setError(null);\n setSessionId(null);\n setRoomName(null);\n setStatus('connecting');\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n let credentials: AthenaVoiceSessionCredentials;\n try {\n credentials = await callbacksRef.current.createSession(controller.signal);\n } catch (cause) {\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n fail(cause);\n return;\n }\n if (controller.signal.aborted) return;\n\n setSessionId(credentials.asset_id ?? null);\n setRoomName(credentials.room_name ?? null);\n\n const room: Room = createRoom ? createRoom() : new Room();\n roomRef.current = room;\n\n room\n .on(RoomEvent.TrackSubscribed, attachTrack)\n .on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n const first: Participant | undefined = speakers[0];\n setActiveSpeaker(first ? speakerOf(first, room) : null);\n })\n .on(RoomEvent.TranscriptionReceived, (segments, participant) =>\n handleTranscription(segments, participant, room),\n )\n .on(RoomEvent.Reconnecting, () => setStatus('reconnecting'))\n .on(RoomEvent.Reconnected, () => setStatus('connected'))\n .on(RoomEvent.Disconnected, () => {\n if (roomRef.current !== room) return;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n setActiveSpeaker(null);\n setStatus('ended');\n })\n .on(RoomEvent.MediaDevicesError, fail);\n\n try {\n await room.connect(credentials.server_url, credentials.token);\n if (controller.signal.aborted) return;\n if (autoEnableMicrophone) {\n await room.localParticipant.setMicrophoneEnabled(true);\n setIsMuted(false);\n } else {\n setIsMuted(true);\n }\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n setStatus('connected');\n } catch (cause) {\n if (controller.signal.aborted) return;\n await teardown();\n fail(cause);\n }\n }, [\n attachTrack,\n autoEnableMicrophone,\n createRoom,\n detachAudio,\n fail,\n handleTranscription,\n teardown,\n ]);\n\n const setMicrophone = useCallback(async (enabled: boolean) => {\n const room: Room | null = roomRef.current;\n if (!room) return;\n await room.localParticipant.setMicrophoneEnabled(enabled);\n setIsMuted(!enabled);\n }, []);\n const mute = useCallback(() => setMicrophone(false), [setMicrophone]);\n const unmute = useCallback(() => setMicrophone(true), [setMicrophone]);\n\n const clearTranscript = useCallback(() => {\n committedSegmentIdsRef.current.clear();\n setTranscript([]);\n }, []);\n\n // Unmount teardown is deferred one tick so StrictMode's synchronous\n // unmount/remount of the same instance does not abort an in-flight\n // `createSession` (which would dispatch a second Orpheus agent on remount).\n const unmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n if (unmountTimerRef.current !== null) {\n clearTimeout(unmountTimerRef.current);\n unmountTimerRef.current = null;\n }\n return () => {\n unmountTimerRef.current = setTimeout(() => {\n unmountTimerRef.current = null;\n void teardown();\n }, 0);\n };\n }, [teardown]);\n\n return useMemo<UseAthenaVoiceSessionReturn>(\n () => ({\n status,\n isConnecting: status === 'connecting',\n isConnected: status === 'connected' || status === 'reconnecting',\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n }),\n [\n status,\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n ],\n );\n};\n"],"names":[],"mappings":";;;AAoLA,MAAM,UAAU,CAAC,UACf,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAE1D,MAAM,YAA+B;AAAA,EACnC,OAAO;AAAA,EACP,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,QAAQ;AAAA,EACR,cAAc;AAAA,EACd,OAAO;AACT;AAGA,MAAM,cAAc,MAAyB;AAE7C,MAAM,0BACJ;AASK,MAAM,2BAA2B,OAAO;AAAA,EAC7C;AAAA,EACA;AAAA,EACA,UAAU,CAAA;AAAA,EACV;AACF,MAO8C;AAC5C,QAAM,MAAc,GAAG,eAAe,QAAQ,QAAQ,EAAE,CAAC;AACzD,QAAM,WAAqB,MAAM,MAAM,KAAK;AAAA,IAC1C,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,eAAe,UAAU,KAAK;AAAA,IAAA;AAAA,IAEhC,MAAM,KAAK,UAAU,OAAO;AAAA,IAC5B;AAAA,EAAA,CACD;AACD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,SAAiB,MAAM,SAAS,OAAO,MAAM,MAAM,EAAE;AAC3D,UAAM,IAAI;AAAA,MACR,kCAAkC,SAAS,MAAM,IAAI,SAAS,KAAK,MAAM,KAAK,EAAE;AAAA,IAAA;AAAA,EAEpF;AACA,SAAQ,MAAM,SAAS,KAAA;AACzB;AAEA,MAAM,YAAY,CAChB,aACA,SAEA,gBAAgB,UAChB,YAAY,aAAa,KAAK,iBAAiB,WAC3C,SACA;AAaC,MAAM,wBAAwB,CACnC,UAAwC,OACR;AAChC,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,uBAAuB;AAAA,IACvB;AAAA,IACA,YAAY,gBAAgB,cAAc;AAAA,EAAA,IACxC;AAIJ,QAAM,SAA4B,UAAA;AAClC,QAAM,YAAY,OAA0B,MAAM;AAClD,YAAU,UAAU;AACpB,QAAM,wBAAwB,OAA0B,EAAE;AAC1D,YAAU,MAAM;AACd,QAAI,CAAC,OAAO,MAAO;AACnB,UAAM,UAA6B,sBAAsB;AACzD,0BAAsB,UAAU,CAAA;AAChC,eAAW,WAAW,QAAS,SAAA;AAAA,EACjC,GAAG,CAAC,OAAO,KAAK,CAAC;AAEjB,QAAM,aAAa,OAA6C,OAAO;AACvE,aAAW,UAAU;AAErB,QAAM,wBAAwB;AAAA,IAC5B,OAAO,WAAgE;AACrE,UAAI,CAAC,UAAU,QAAQ,OAAO;AAC5B,cAAM,IAAI,QAAc,CAAC,YAAY;AACnC,gCAAsB,QAAQ,KAAK,OAAO;AAC1C,iBAAO,iBAAiB,SAAS,MAAM,QAAA,GAAW,EAAE,MAAM,MAAM;AAAA,QAClE,CAAC;AACD,YAAI,OAAO,QAAS,OAAM,IAAI,MAAM,SAAS;AAAA,MAC/C;AACA,YAAM,EAAE,OAAO,eAAA,IAAmB,UAAU;AAC5C,UAAI,CAAC,SAAS,CAAC,eAAgB,OAAM,IAAI,MAAM,uBAAuB;AACtE,aAAO,yBAAyB;AAAA,QAC9B;AAAA,QACA;AAAA,QACA,SAAS,WAAW;AAAA,QACpB;AAAA,MAAA,CACD;AAAA,IACH;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAmC,MAAM;AACrE,QAAM,CAAC,eAAe,gBAAgB,IAAI;AAAA,IACxC;AAAA,EAAA;AAEF,QAAM,CAAC,SAAS,UAAU,IAAI,SAAkB,KAAK;AACrD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAsC,CAAA,CAAE;AAC5E,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAC5D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AAGrD,QAAM,eAAe,OAAO;AAAA,IAC1B,eAAe,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AACD,eAAa,UAAU;AAAA,IACrB,eAAe,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAGF,QAAM,UAAU,OAAoB,IAAI;AACxC,QAAM,WAAW,OAA+B,IAAI;AACpD,QAAM,sBAAsB,OAA2B,EAAE;AACzD,QAAM,gBAAgB,OAAgC,IAAI;AAC1D,QAAM,yBAAyB,OAAoB,oBAAI,KAAK;AAI5D,QAAM,cAAc,OAAgB,KAAK;AAEzC,QAAM,OAAO,YAAY,CAAC,UAAmB;;AAC3C,UAAM,MAAa,QAAQ,KAAK;AAChC,aAAS,GAAG;AACZ,cAAU,OAAO;AACjB,6BAAa,SAAQ,YAArB,4BAA+B;AAAA,EACjC,GAAG,CAAA,CAAE;AAEL,QAAM,cAAc,YAAY,MAAM;AACpC,eAAW,WAAW,oBAAoB,SAAS;AACjD,cAAQ,YAAY;AACpB,UAAI,YAAY,cAAc,QAAS,SAAQ,OAAA;AAAA,IACjD;AACA,wBAAoB,UAAU,CAAA;AAC9B,kBAAc,UAAU;AAAA,EAC1B,GAAG,CAAA,CAAE;AAEL,QAAM,WAAW,YAAY,YAAY;;AACvC,mBAAS,YAAT,mBAAkB;AAClB,aAAS,UAAU;AACnB,UAAM,OAAoB,QAAQ;AAClC,YAAQ,UAAU;AAClB,gBAAY,UAAU;AACtB,gBAAA;AACA,QAAI,MAAM;AACR,WAAK,mBAAA;AACL,YAAM,KAAK,WAAW,IAAI;AAAA,IAC5B;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,OAAO,YAAY,YAAY;AACnC,UAAM,UACJ,QAAQ,YAAY,QAAQ,SAAS,YAAY;AACnD,UAAM,SAAA;AACN,qBAAiB,IAAI;AACrB,QAAI,mBAAmB,OAAO;AAAA,EAChC,GAAG,CAAC,QAAQ,CAAC;AAEb,QAAM,cAAc;AAAA,IAClB,CAAC,UAAuB;AACtB,UAAI,MAAM,SAAS,MAAM,KAAK,MAAO;AACrC,UAAI,cAAc;AAChB,cAAM,OAAO,YAAY;AACzB,4BAAoB,QAAQ,KAAK,YAAY;AAC7C;AAAA,MACF;AACA,YAAM,UAA4B,MAAM,OAAA;AACxC,cAAQ,aAAa,2BAA2B,EAAE;AAClD,eAAS,KAAK,YAAY,OAAO;AACjC,oBAAc,UAAU;AACxB,0BAAoB,QAAQ,KAAK,OAAO;AAAA,IAC1C;AAAA,IACA,CAAC,YAAY;AAAA,EAAA;AAGf,QAAM,sBAAsB;AAAA,IAC1B,CACE,UACA,aACA,SACG;;AACH,YAAM,UAA8B,UAAU,aAAa,IAAI;AAC/D,YAAM,YAAyC,CAAA;AAC/C,iBAAW,WAAW,UAAU;AAC9B,YAAI,CAAC,QAAQ,OAAO;AAClB,mCAAa,SAAQ,wBAArB,4BAA2C,SAAS,QAAQ;AAC5D;AAAA,QACF;AACA,YAAI,uBAAuB,QAAQ,IAAI,QAAQ,EAAE,EAAG;AACpD,+BAAuB,QAAQ,IAAI,QAAQ,EAAE;AAC7C,kBAAU,KAAK;AAAA,UACb,IAAI,QAAQ;AAAA,UACZ;AAAA,UACA,MAAM,QAAQ;AAAA,UACd,WAAW,QAAQ;AAAA,UACnB,SAAS,QAAQ;AAAA,UACjB,UAAU,QAAQ;AAAA,QAAA,CACnB;AAAA,MACH;AACA,UAAI,UAAU,WAAW,EAAG;AAC5B,oBAAc,CAAC,aAAa,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;AACvD,iBAAW,QAAQ,UAAW,0BAAa,SAAQ,iBAArB,4BAAoC;AAAA,IACpE;AAAA,IACA,CAAA;AAAA,EAAC;AAGH,QAAM,QAAQ,YAAY,YAAY;AACpC,QAAI,YAAY,WAAW,QAAQ,QAAS;AAC5C,gBAAY,UAAU;AACtB,aAAS,IAAI;AACb,iBAAa,IAAI;AACjB,gBAAY,IAAI;AAChB,cAAU,YAAY;AAEtB,UAAM,aAAa,IAAI,gBAAA;AACvB,aAAS,UAAU;AAEnB,QAAI;AACJ,QAAI;AACF,oBAAc,MAAM,aAAa,QAAQ,cAAc,WAAW,MAAM;AAAA,IAC1E,SAAS,OAAO;AACd,UAAI,WAAW,OAAO,QAAS;AAC/B,eAAS,UAAU;AACnB,kBAAY,UAAU;AACtB,WAAK,KAAK;AACV;AAAA,IACF;AACA,QAAI,WAAW,OAAO,QAAS;AAE/B,iBAAa,YAAY,YAAY,IAAI;AACzC,gBAAY,YAAY,aAAa,IAAI;AAEzC,UAAM,OAAa,aAAa,WAAA,IAAe,IAAI,KAAA;AACnD,YAAQ,UAAU;AAElB,SACG,GAAG,UAAU,iBAAiB,WAAW,EACzC,GAAG,UAAU,uBAAuB,CAAC,aAA4B;AAChE,YAAM,QAAiC,SAAS,CAAC;AACjD,uBAAiB,QAAQ,UAAU,OAAO,IAAI,IAAI,IAAI;AAAA,IACxD,CAAC,EACA;AAAA,MAAG,UAAU;AAAA,MAAuB,CAAC,UAAU,gBAC9C,oBAAoB,UAAU,aAAa,IAAI;AAAA,IAAA,EAEhD,GAAG,UAAU,cAAc,MAAM,UAAU,cAAc,CAAC,EAC1D,GAAG,UAAU,aAAa,MAAM,UAAU,WAAW,CAAC,EACtD,GAAG,UAAU,cAAc,MAAM;AAChC,UAAI,QAAQ,YAAY,KAAM;AAC9B,cAAQ,UAAU;AAClB,kBAAY,UAAU;AACtB,kBAAA;AACA,uBAAiB,IAAI;AACrB,gBAAU,OAAO;AAAA,IACnB,CAAC,EACA,GAAG,UAAU,mBAAmB,IAAI;AAEvC,QAAI;AACF,YAAM,KAAK,QAAQ,YAAY,YAAY,YAAY,KAAK;AAC5D,UAAI,WAAW,OAAO,QAAS;AAC/B,UAAI,sBAAsB;AACxB,cAAM,KAAK,iBAAiB,qBAAqB,IAAI;AACrD,mBAAW,KAAK;AAAA,MAClB,OAAO;AACL,mBAAW,IAAI;AAAA,MACjB;AACA,UAAI,WAAW,OAAO,QAAS;AAC/B,eAAS,UAAU;AACnB,kBAAY,UAAU;AACtB,gBAAU,WAAW;AAAA,IACvB,SAAS,OAAO;AACd,UAAI,WAAW,OAAO,QAAS;AAC/B,YAAM,SAAA;AACN,WAAK,KAAK;AAAA,IACZ;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AAED,QAAM,gBAAgB,YAAY,OAAO,YAAqB;AAC5D,UAAM,OAAoB,QAAQ;AAClC,QAAI,CAAC,KAAM;AACX,UAAM,KAAK,iBAAiB,qBAAqB,OAAO;AACxD,eAAW,CAAC,OAAO;AAAA,EACrB,GAAG,CAAA,CAAE;AACL,QAAM,OAAO,YAAY,MAAM,cAAc,KAAK,GAAG,CAAC,aAAa,CAAC;AACpE,QAAM,SAAS,YAAY,MAAM,cAAc,IAAI,GAAG,CAAC,aAAa,CAAC;AAErE,QAAM,kBAAkB,YAAY,MAAM;AACxC,2BAAuB,QAAQ,MAAA;AAC/B,kBAAc,CAAA,CAAE;AAAA,EAClB,GAAG,CAAA,CAAE;AAKL,QAAM,kBAAkB,OAA6C,IAAI;AACzE,YAAU,MAAM;AACd,QAAI,gBAAgB,YAAY,MAAM;AACpC,mBAAa,gBAAgB,OAAO;AACpC,sBAAgB,UAAU;AAAA,IAC5B;AACA,WAAO,MAAM;AACX,sBAAgB,UAAU,WAAW,MAAM;AACzC,wBAAgB,UAAU;AAC1B,aAAK,SAAA;AAAA,MACP,GAAG,CAAC;AAAA,IACN;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO;AAAA,IACL,OAAO;AAAA,MACL;AAAA,MACA,cAAc,WAAW;AAAA,MACzB,aAAa,WAAW,eAAe,WAAW;AAAA,MAClD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAEF;AAAA,MACE;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EACF;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"use-athena-voice-session.js","sources":["../../src/voice/use-athena-voice-session.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport {\n Room,\n RoomEvent,\n Track,\n type Participant,\n type RemoteTrack,\n type TranscriptionSegment,\n} from 'livekit-client';\nimport { useParentBridge, type ParentBridgeState } from '../runtime/auth';\n\n/**\n * Realtime connection details for one Athena voice session. This is the shape\n * the Athena voice session endpoint returns; the hook only reads these\n * fields, so any server that proxies that call can hand it straight back.\n */\nexport interface AthenaVoiceSessionCredentials {\n /** Realtime signalling URL (`wss://…`). */\n server_url: string;\n /** Short-lived participant JWT scoped to `room_name`. */\n token: string;\n room_name?: string;\n /**\n * Athena session asset the voice run is persisted into. Use it with\n * `GET /api/v0/sessions/{asset_id}/download` to retrieve the transcript\n * after the call.\n */\n asset_id?: string;\n}\n\nexport type AthenaVoiceSessionStatus =\n | 'idle'\n | 'connecting'\n | 'connected'\n | 'reconnecting'\n | 'ended'\n | 'error';\n\nexport type AthenaVoiceSpeaker = 'user' | 'agent';\n\n/** One committed (final) transcription segment from either side of the call. */\nexport interface AthenaVoiceTranscriptTurn {\n id: string;\n speaker: AthenaVoiceSpeaker;\n text: string;\n /** Segment start, in ms since the room's epoch. */\n startTime: number;\n endTime: number;\n language: string;\n}\n\n/**\n * A voice tool the Athena voice agent can run. These are voice tool\n * names, not the chat `Toolkits` ids: `'web_search_browse_toolkit'` is not a\n * voice tool, `'web_search'` is. Only these names are accepted.\n *\n * The agent also has tools that call back into the client over room RPC\n * (clipboard, selection, screen contents, opening assets). This hook registers\n * no RPC handlers, so they are not part of its contract.\n */\nexport type AthenaVoiceToolId =\n /** Web search. */\n | 'web_search'\n /** Legacy alias of `web_search`; prefer `web_search`. */\n | 'search_web'\n /** Fetch and read one URL. */\n | 'browse'\n /** The workspace knowledge base, where configured. */\n | 'search_knowledge_base'\n /** Same knowledge base, thorough long-running retrieval. */\n | 'search_knowledge_base_lr'\n /** Read Athena assets by id, as the viewer. */\n | 'read_full_asset'\n /** Search the viewer's Athena assets. */\n | 'search_assets'\n /** Hand a question to the general agent and speak its answer. */\n | 'ask_agent'\n /** Save a note for the user (a stub on the worker today). */\n | 'save_note'\n /** Current date and time, Eastern. */\n | 'get_current_time_est';\n\n/**\n * Voice agent configuration for a session the hook creates itself (see\n * `UseAthenaVoiceSessionOptions.session`). Mirrors the body of the Athena\n * voice session endpoint; every field is optional and falls back to the\n * server default.\n */\nexport interface AthenaVoiceSessionConfig {\n /** Persona / instructions for the voice agent. */\n system_prompt?: string;\n /** Realtime provider, e.g. `'openai'` or `'google'`. */\n provider?: string;\n /** Provider model id, e.g. `'gpt-realtime-2'`. */\n model?: string;\n /** Provider voice id, e.g. `'marin'` (OpenAI) or `'Aoede'` (Google). */\n voice?: string;\n temperature?: number;\n /** BCP-47 language tag; default `'en'`. */\n language?: string;\n /**\n * Voice tools the agent may call. Omit it for the server's default set\n * (`web_search`, `search_knowledge_base`, `ask_agent`, …); pass `[]` for a\n * pure conversation with no tool calls.\n */\n enabled_tools?: AthenaVoiceToolId[];\n /** IANA timezone of the speaker, e.g. `'America/New_York'`. */\n timezone?: string;\n}\n\nexport interface UseAthenaVoiceSessionOptions {\n /**\n * Creates the voice session. Omit it when the app is served from a\n * Marathon computer-asset URL: the hook then calls the Athena voice session\n * endpoint itself with the viewer's bearer token from `/_athena/auth`, so\n * the session is created as — and shows up in the sessions of — whoever is\n * using the app.\n *\n * Works for workspace members and external SSO viewers alike: the scoped\n * `athview_` viewer credential is authorized for the voice session route and\n * `agents/general/invoke` the same way it is for `/api/v0/sessions`.\n *\n * Supply it when the app runs anywhere else (standalone, custom auth): it\n * should run on the caller's own server so the Athena API key never reaches\n * the browser. The hook only reads the returned credentials.\n *\n * Decide once: switching between supplying and omitting it across renders\n * of the same component is not supported.\n */\n createSession?: (\n signal: AbortSignal,\n ) => Promise<AthenaVoiceSessionCredentials>;\n /**\n * Agent configuration for sessions the hook creates itself. Ignored when\n * `createSession` is supplied.\n */\n session?: AthenaVoiceSessionConfig;\n /** Called once per committed transcript turn, in arrival order. */\n onTranscript?: (turn: AthenaVoiceTranscriptTurn) => void;\n /** Called with interim (non-final) transcript text as it streams in. */\n onInterimTranscript?: (speaker: AthenaVoiceSpeaker, text: string) => void;\n onError?: (error: Error) => void;\n /**\n * Where to play the agent's audio. Defaults to a detached `<audio>` element\n * the hook creates and removes itself.\n */\n audioElement?: HTMLMediaElement | null;\n /** Enable the microphone as soon as the room connects. Default `true`. */\n autoEnableMicrophone?: boolean;\n /** @internal Overrides `new Room()`; used by tests. */\n createRoom?: () => Room;\n /** @internal Overrides `useParentBridge()`; used by tests. */\n useBridge?: () => ParentBridgeState;\n}\n\nexport interface UseAthenaVoiceSessionReturn {\n status: AthenaVoiceSessionStatus;\n /** True between `start()` resolving the session and the room connecting. */\n isConnecting: boolean;\n isConnected: boolean;\n /** Who is speaking right now, or `null` when the room is quiet. */\n activeSpeaker: AthenaVoiceSpeaker | null;\n isMuted: boolean;\n /** Committed transcript turns so far, in arrival order. */\n transcript: AthenaVoiceTranscriptTurn[];\n /** Athena session asset id, once the session has been created. */\n sessionId: string | null;\n roomName: string | null;\n error: Error | null;\n /** Creates a session and joins the room. No-op while a session is live. */\n start: () => Promise<void>;\n /** Leaves the room and stops local tracks. Safe to call at any time. */\n stop: () => Promise<void>;\n mute: () => Promise<void>;\n unmute: () => Promise<void>;\n /** Clears `transcript`; does not affect the live session. */\n clearTranscript: () => void;\n}\n\nconst toError = (value: unknown): Error =>\n value instanceof Error ? value : new Error(String(value));\n\nconst NO_BRIDGE: ParentBridgeState = {\n token: null,\n statewireSyncUrl: null,\n backendBaseUrl: null,\n appUrl: null,\n hostAnswered: false,\n ready: true,\n};\n// Stands in for `useParentBridge` when the caller creates sessions itself, so\n// the hook never fetches `/_athena/auth` it would not use.\nconst useNoBridge = (): ParentBridgeState => NO_BRIDGE;\n\nconst NO_VIEWER_TOKEN_MESSAGE =\n 'useAthenaVoiceSession: no Athena viewer token. Either serve the app from ' +\n 'a Marathon computer-asset URL (which mints one via /_athena/auth) or pass ' +\n '`createSession` to create the session through your own server.';\n\n/**\n * Creates an Athena voice session as the current viewer. Exported so apps with\n * their own bridge state (or a server proxy holding a user bearer) can reuse\n * the exact request the hook makes.\n */\nexport const createAthenaVoiceSession = async ({\n backendBaseUrl,\n token,\n session = {},\n signal,\n}: {\n /** Bare Athena API base, e.g. `https://api.athenaintel.com`. */\n backendBaseUrl: string;\n /** Bearer token for the user the session belongs to. */\n token: string;\n session?: AthenaVoiceSessionConfig;\n signal?: AbortSignal;\n}): Promise<AthenaVoiceSessionCredentials> => {\n const url: string = `${backendBaseUrl.replace(/\\/+$/, '')}/api/orpheus/create-session`;\n const response: Response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify(session),\n signal,\n });\n if (!response.ok) {\n const detail: string = await response.text().catch(() => '');\n throw new Error(\n `Athena voice session creation failed (${response.status})${detail ? `: ${detail}` : ''}`,\n );\n }\n return (await response.json()) as AthenaVoiceSessionCredentials;\n};\n\nconst speakerOf = (\n participant: Participant | undefined,\n room: Room,\n): AthenaVoiceSpeaker =>\n participant !== undefined &&\n participant.identity === room.localParticipant.identity\n ? 'user'\n : 'agent';\n\n/**\n * Runs one Athena voice session from React.\n *\n * Owns the whole browser-side lifecycle the voice API leaves to the caller:\n * creating the session through the app's own server, joining the room,\n * publishing the microphone, playing the agent's audio, tracking who is\n * speaking, committing transcript turns from room transcription events, and\n * tearing everything down on `stop()` or unmount. A second `start()` while a\n * session is live (or still connecting) is ignored, so React StrictMode's\n * double-invoked effects cannot dispatch two agents into one room.\n */\nexport const useAthenaVoiceSession = (\n options: UseAthenaVoiceSessionOptions = {},\n): UseAthenaVoiceSessionReturn => {\n const {\n createSession,\n session,\n onTranscript,\n onInterimTranscript,\n onError,\n audioElement,\n autoEnableMicrophone = true,\n createRoom,\n useBridge = createSession ? useNoBridge : useParentBridge,\n } = options;\n\n // Viewer identity minted by Marathon (`/_athena/auth`) or the Athena app\n // iframe bridge. Only consulted when the caller did not supply `createSession`.\n const bridge: ParentBridgeState = useBridge();\n const bridgeRef = useRef<ParentBridgeState>(bridge);\n bridgeRef.current = bridge;\n const bridgeReadyWaitersRef = useRef<Array<() => void>>([]);\n useEffect(() => {\n if (!bridge.ready) return;\n const waiters: Array<() => void> = bridgeReadyWaitersRef.current;\n bridgeReadyWaitersRef.current = [];\n for (const resolve of waiters) resolve();\n }, [bridge.ready]);\n\n const sessionRef = useRef<AthenaVoiceSessionConfig | undefined>(session);\n sessionRef.current = session;\n\n const createSessionAsViewer = useCallback(\n async (signal: AbortSignal): Promise<AthenaVoiceSessionCredentials> => {\n if (!bridgeRef.current.ready) {\n await new Promise<void>((resolve) => {\n bridgeReadyWaitersRef.current.push(resolve);\n signal.addEventListener('abort', () => resolve(), { once: true });\n });\n if (signal.aborted) throw new Error('aborted');\n }\n const { token, backendBaseUrl } = bridgeRef.current;\n if (!token || !backendBaseUrl) throw new Error(NO_VIEWER_TOKEN_MESSAGE);\n return createAthenaVoiceSession({\n backendBaseUrl,\n token,\n session: sessionRef.current,\n signal,\n });\n },\n [],\n );\n\n const [status, setStatus] = useState<AthenaVoiceSessionStatus>('idle');\n const [activeSpeaker, setActiveSpeaker] = useState<AthenaVoiceSpeaker | null>(\n null,\n );\n const [isMuted, setIsMuted] = useState<boolean>(false);\n const [transcript, setTranscript] = useState<AthenaVoiceTranscriptTurn[]>([]);\n const [sessionId, setSessionId] = useState<string | null>(null);\n const [roomName, setRoomName] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n // Latest callbacks without re-subscribing room listeners on every render.\n const callbacksRef = useRef({\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n });\n callbacksRef.current = {\n createSession: createSession ?? createSessionAsViewer,\n onTranscript,\n onInterimTranscript,\n onError,\n };\n\n const roomRef = useRef<Room | null>(null);\n const abortRef = useRef<AbortController | null>(null);\n const attachedElementsRef = useRef<HTMLMediaElement[]>([]);\n const ownedAudioRef = useRef<HTMLMediaElement | null>(null);\n const committedSegmentIdsRef = useRef<Set<string>>(new Set());\n // Guards `start()` against re-entry from the moment it is called, before any\n // state update lands — a StrictMode double effect fires both calls\n // synchronously in the same tick.\n const startingRef = useRef<boolean>(false);\n\n const fail = useCallback((cause: unknown) => {\n const err: Error = toError(cause);\n setError(err);\n setStatus('error');\n callbacksRef.current.onError?.(err);\n }, []);\n\n const detachAudio = useCallback(() => {\n for (const element of attachedElementsRef.current) {\n element.srcObject = null;\n if (element === ownedAudioRef.current) element.remove();\n }\n attachedElementsRef.current = [];\n ownedAudioRef.current = null;\n }, []);\n\n const teardown = useCallback(async () => {\n abortRef.current?.abort();\n abortRef.current = null;\n const room: Room | null = roomRef.current;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n if (room) {\n room.removeAllListeners();\n await room.disconnect(true);\n }\n }, [detachAudio]);\n\n const stop = useCallback(async () => {\n const hadRoom: boolean =\n roomRef.current !== null || abortRef.current !== null;\n await teardown();\n setActiveSpeaker(null);\n if (hadRoom) setStatus('ended');\n }, [teardown]);\n\n const attachTrack = useCallback(\n (track: RemoteTrack) => {\n if (track.kind !== Track.Kind.Audio) return;\n if (audioElement) {\n track.attach(audioElement);\n attachedElementsRef.current.push(audioElement);\n return;\n }\n const element: HTMLMediaElement = track.attach();\n element.setAttribute('data-athena-voice-audio', '');\n document.body.appendChild(element);\n ownedAudioRef.current = element;\n attachedElementsRef.current.push(element);\n },\n [audioElement],\n );\n\n const handleTranscription = useCallback(\n (\n segments: TranscriptionSegment[],\n participant: Participant | undefined,\n room: Room,\n ) => {\n const speaker: AthenaVoiceSpeaker = speakerOf(participant, room);\n const committed: AthenaVoiceTranscriptTurn[] = [];\n for (const segment of segments) {\n if (!segment.final) {\n callbacksRef.current.onInterimTranscript?.(speaker, segment.text);\n continue;\n }\n if (committedSegmentIdsRef.current.has(segment.id)) continue;\n committedSegmentIdsRef.current.add(segment.id);\n committed.push({\n id: segment.id,\n speaker,\n text: segment.text,\n startTime: segment.startTime,\n endTime: segment.endTime,\n language: segment.language,\n });\n }\n if (committed.length === 0) return;\n setTranscript((previous) => [...previous, ...committed]);\n for (const turn of committed) callbacksRef.current.onTranscript?.(turn);\n },\n [],\n );\n\n const start = useCallback(async () => {\n if (startingRef.current || roomRef.current) return;\n startingRef.current = true;\n setError(null);\n setSessionId(null);\n setRoomName(null);\n setStatus('connecting');\n\n const controller = new AbortController();\n abortRef.current = controller;\n\n let credentials: AthenaVoiceSessionCredentials;\n try {\n credentials = await callbacksRef.current.createSession(controller.signal);\n } catch (cause) {\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n fail(cause);\n return;\n }\n if (controller.signal.aborted) return;\n\n setSessionId(credentials.asset_id ?? null);\n setRoomName(credentials.room_name ?? null);\n\n const room: Room = createRoom ? createRoom() : new Room();\n roomRef.current = room;\n\n room\n .on(RoomEvent.TrackSubscribed, attachTrack)\n .on(RoomEvent.ActiveSpeakersChanged, (speakers: Participant[]) => {\n const first: Participant | undefined = speakers[0];\n setActiveSpeaker(first ? speakerOf(first, room) : null);\n })\n .on(RoomEvent.TranscriptionReceived, (segments, participant) =>\n handleTranscription(segments, participant, room),\n )\n .on(RoomEvent.Reconnecting, () => setStatus('reconnecting'))\n .on(RoomEvent.Reconnected, () => setStatus('connected'))\n .on(RoomEvent.Disconnected, () => {\n if (roomRef.current !== room) return;\n roomRef.current = null;\n startingRef.current = false;\n detachAudio();\n setActiveSpeaker(null);\n setStatus('ended');\n })\n .on(RoomEvent.MediaDevicesError, fail);\n\n try {\n await room.connect(credentials.server_url, credentials.token);\n if (controller.signal.aborted) return;\n if (autoEnableMicrophone) {\n await room.localParticipant.setMicrophoneEnabled(true);\n setIsMuted(false);\n } else {\n setIsMuted(true);\n }\n if (controller.signal.aborted) return;\n abortRef.current = null;\n startingRef.current = false;\n setStatus('connected');\n } catch (cause) {\n if (controller.signal.aborted) return;\n await teardown();\n fail(cause);\n }\n }, [\n attachTrack,\n autoEnableMicrophone,\n createRoom,\n detachAudio,\n fail,\n handleTranscription,\n teardown,\n ]);\n\n const setMicrophone = useCallback(async (enabled: boolean) => {\n const room: Room | null = roomRef.current;\n if (!room) return;\n await room.localParticipant.setMicrophoneEnabled(enabled);\n setIsMuted(!enabled);\n }, []);\n const mute = useCallback(() => setMicrophone(false), [setMicrophone]);\n const unmute = useCallback(() => setMicrophone(true), [setMicrophone]);\n\n const clearTranscript = useCallback(() => {\n committedSegmentIdsRef.current.clear();\n setTranscript([]);\n }, []);\n\n // Unmount teardown is deferred one tick so StrictMode's synchronous\n // unmount/remount of the same instance does not abort an in-flight\n // `createSession` (which would dispatch a second voice agent on remount).\n const unmountTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n useEffect(() => {\n if (unmountTimerRef.current !== null) {\n clearTimeout(unmountTimerRef.current);\n unmountTimerRef.current = null;\n }\n return () => {\n unmountTimerRef.current = setTimeout(() => {\n unmountTimerRef.current = null;\n void teardown();\n }, 0);\n };\n }, [teardown]);\n\n return useMemo<UseAthenaVoiceSessionReturn>(\n () => ({\n status,\n isConnecting: status === 'connecting',\n isConnected: status === 'connected' || status === 'reconnecting',\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n }),\n [\n status,\n activeSpeaker,\n isMuted,\n transcript,\n sessionId,\n roomName,\n error,\n start,\n stop,\n mute,\n unmute,\n clearTranscript,\n ],\n );\n};\n"],"names":[],"mappings":"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"}
|