@agent-native/core 0.94.1 → 0.94.3
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/cloneable-saas.mdx +15 -15
- package/corpus/core/docs/content/creating-templates.mdx +1 -1
- package/corpus/core/docs/content/extensions.mdx +1 -1
- package/corpus/core/docs/content/local-file-mode.mdx +6 -6
- package/corpus/core/docs/content/locales/ar-SA/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/de-DE/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/es-ES/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/fr-FR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/hi-IN/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/ja-JP/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/ko-KR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/pt-BR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-CN/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-TW/voice-input.mdx +2 -2
- package/corpus/core/docs/content/sharing.mdx +4 -4
- package/corpus/core/docs/content/template-analytics.mdx +2 -2
- package/corpus/core/docs/content/template-calendar.mdx +4 -4
- package/corpus/core/docs/content/template-clips.mdx +2 -2
- package/corpus/core/docs/content/template-content.mdx +12 -11
- package/corpus/core/docs/content/template-mail.mdx +1 -1
- package/corpus/core/docs/content/template-slides.mdx +2 -2
- package/corpus/core/docs/content/voice-input.mdx +2 -2
- package/corpus/core/docs/content/workspace.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +154 -1
- package/corpus/core/src/client/AssistantChat.tsx +30 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +19 -1
- package/corpus/core/src/client/agent-chat.ts +6 -0
- package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +72 -2
- package/corpus/core/src/client/composer/realtime-voice-audio-level.ts +64 -0
- package/corpus/core/src/client/composer/realtime-voice-transcript.ts +134 -0
- package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +220 -6
- package/corpus/core/src/client/use-chat-threads.ts +33 -9
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
- package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
- package/corpus/core/src/integrations/adapters/slack.ts +77 -7
- package/corpus/core/src/integrations/types.ts +30 -0
- package/corpus/core/src/integrations/webhook-handler.ts +54 -18
- package/corpus/core/src/scripts/call-agent.ts +1 -0
- package/corpus/core/src/server/request-context.ts +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +98 -1
- package/corpus/templates/clips/changelog/2026-07-10-comment-urls-open-as-clickable-links.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-recording-uploads-now-show-a-brief-uploaded-state-wi.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-recordings-now-retry-saving-their-locally-captured-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-update-prompts-are-more-compact-and-easier-to-dismiss.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-quiet-recordings-now-finish-saving-instead-of-getting-stuck.md +6 -0
- package/corpus/templates/clips/desktop/src/components/UpdateBanner.tsx +7 -4
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +284 -85
- package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +148 -40
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +13 -13
- package/corpus/templates/clips/desktop/src/styles.css +83 -13
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -28
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +130 -65
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +10 -4
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +82 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +82 -1
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +127 -1
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +20 -0
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +9 -0
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts +3 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.js +32 -3
- package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/client/composer/realtime-voice-audio-level.d.ts +14 -0
- package/dist/client/composer/realtime-voice-audio-level.d.ts.map +1 -0
- package/dist/client/composer/realtime-voice-audio-level.js +47 -0
- package/dist/client/composer/realtime-voice-audio-level.js.map +1 -0
- package/dist/client/composer/realtime-voice-transcript.d.ts +26 -0
- package/dist/client/composer/realtime-voice-transcript.d.ts.map +1 -0
- package/dist/client/composer/realtime-voice-transcript.js +103 -0
- package/dist/client/composer/realtime-voice-transcript.js.map +1 -0
- package/dist/client/composer/useRealtimeVoiceMode.d.ts +14 -0
- package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/useRealtimeVoiceMode.js +177 -6
- package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +26 -6
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +76 -21
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +4 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +130 -6
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +56 -7
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.js +54 -16
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +1 -0
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
- package/docs/content/cloneable-saas.mdx +15 -15
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/extensions.mdx +1 -1
- package/docs/content/local-file-mode.mdx +6 -6
- package/docs/content/locales/ar-SA/voice-input.mdx +2 -2
- package/docs/content/locales/de-DE/voice-input.mdx +2 -2
- package/docs/content/locales/es-ES/voice-input.mdx +2 -2
- package/docs/content/locales/fr-FR/voice-input.mdx +2 -2
- package/docs/content/locales/hi-IN/voice-input.mdx +2 -2
- package/docs/content/locales/ja-JP/voice-input.mdx +2 -2
- package/docs/content/locales/ko-KR/voice-input.mdx +2 -2
- package/docs/content/locales/pt-BR/voice-input.mdx +2 -2
- package/docs/content/locales/zh-CN/voice-input.mdx +2 -2
- package/docs/content/locales/zh-TW/voice-input.mdx +2 -2
- package/docs/content/sharing.mdx +4 -4
- package/docs/content/template-analytics.mdx +2 -2
- package/docs/content/template-calendar.mdx +4 -4
- package/docs/content/template-clips.mdx +2 -2
- package/docs/content/template-content.mdx +12 -11
- package/docs/content/template-mail.mdx +1 -1
- package/docs/content/template-slides.mdx +2 -2
- package/docs/content/voice-input.mdx +2 -2
- package/docs/content/workspace.mdx +1 -1
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const SILENT_LEVELS = { input: 0, output: 0 };
|
|
2
|
+
function clampLevel(value) {
|
|
3
|
+
if (!Number.isFinite(value))
|
|
4
|
+
return 0;
|
|
5
|
+
return Math.max(0, Math.min(1, value));
|
|
6
|
+
}
|
|
7
|
+
export function normalizeRealtimeVoiceRms(samples) {
|
|
8
|
+
if (samples.length === 0)
|
|
9
|
+
return 0;
|
|
10
|
+
let sumSquares = 0;
|
|
11
|
+
for (const sample of samples) {
|
|
12
|
+
const normalized = (sample - 128) / 128;
|
|
13
|
+
sumSquares += normalized * normalized;
|
|
14
|
+
}
|
|
15
|
+
const rms = Math.sqrt(sumSquares / samples.length);
|
|
16
|
+
return clampLevel(Math.max(0, rms - 0.012) * 3.2);
|
|
17
|
+
}
|
|
18
|
+
export function smoothRealtimeVoiceLevel(previous, next) {
|
|
19
|
+
const target = clampLevel(next);
|
|
20
|
+
const factor = target > previous ? 0.55 : 0.2;
|
|
21
|
+
return clampLevel(previous + (target - previous) * factor);
|
|
22
|
+
}
|
|
23
|
+
export function createRealtimeVoiceAudioLevelStore() {
|
|
24
|
+
let snapshot = SILENT_LEVELS;
|
|
25
|
+
const listeners = new Set();
|
|
26
|
+
const set = (levels) => {
|
|
27
|
+
const next = {
|
|
28
|
+
input: clampLevel(levels.input),
|
|
29
|
+
output: clampLevel(levels.output),
|
|
30
|
+
};
|
|
31
|
+
if (next.input === snapshot.input && next.output === snapshot.output)
|
|
32
|
+
return;
|
|
33
|
+
snapshot = next;
|
|
34
|
+
for (const listener of listeners)
|
|
35
|
+
listener();
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
getSnapshot: () => snapshot,
|
|
39
|
+
subscribe(listener) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
return () => listeners.delete(listener);
|
|
42
|
+
},
|
|
43
|
+
set,
|
|
44
|
+
reset: () => set(SILENT_LEVELS),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=realtime-voice-audio-level.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-voice-audio-level.js","sourceRoot":"","sources":["../../../src/client/composer/realtime-voice-audio-level.ts"],"names":[],"mappings":"AAYA,MAAM,aAAa,GAA6B,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;AAExE,SAAS,UAAU,CAAC,KAAa;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACtC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,OAAmB;IAC3D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QACxC,UAAU,IAAI,UAAU,GAAG,UAAU,CAAC;IACxC,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,OAAO,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,QAAgB,EAChB,IAAY;IAEZ,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAChC,MAAM,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9C,OAAO,UAAU,CAAC,QAAQ,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,GAAG,MAAM,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,kCAAkC;IAChD,IAAI,QAAQ,GAAG,aAAa,CAAC;IAC7B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;IAExC,MAAM,GAAG,GAAG,CAAC,MAAgC,EAAE,EAAE;QAC/C,MAAM,IAAI,GAAG;YACX,KAAK,EAAE,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC;YAC/B,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC;SAClC,CAAC;QACF,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;YAClE,OAAO;QACT,QAAQ,GAAG,IAAI,CAAC;QAChB,KAAK,MAAM,QAAQ,IAAI,SAAS;YAAE,QAAQ,EAAE,CAAC;IAC/C,CAAC,CAAC;IAEF,OAAO;QACL,WAAW,EAAE,GAAG,EAAE,CAAC,QAAQ;QAC3B,SAAS,CAAC,QAAQ;YAChB,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC1C,CAAC;QACD,GAAG;QACH,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,CAAC;KAChC,CAAC;AACJ,CAAC","sourcesContent":["export interface RealtimeVoiceAudioLevels {\n input: number;\n output: number;\n}\n\nexport interface RealtimeVoiceAudioLevelStore {\n getSnapshot: () => RealtimeVoiceAudioLevels;\n subscribe: (listener: () => void) => () => void;\n set: (levels: RealtimeVoiceAudioLevels) => void;\n reset: () => void;\n}\n\nconst SILENT_LEVELS: RealtimeVoiceAudioLevels = { input: 0, output: 0 };\n\nfunction clampLevel(value: number): number {\n if (!Number.isFinite(value)) return 0;\n return Math.max(0, Math.min(1, value));\n}\n\nexport function normalizeRealtimeVoiceRms(samples: Uint8Array): number {\n if (samples.length === 0) return 0;\n let sumSquares = 0;\n for (const sample of samples) {\n const normalized = (sample - 128) / 128;\n sumSquares += normalized * normalized;\n }\n const rms = Math.sqrt(sumSquares / samples.length);\n return clampLevel(Math.max(0, rms - 0.012) * 3.2);\n}\n\nexport function smoothRealtimeVoiceLevel(\n previous: number,\n next: number,\n): number {\n const target = clampLevel(next);\n const factor = target > previous ? 0.55 : 0.2;\n return clampLevel(previous + (target - previous) * factor);\n}\n\nexport function createRealtimeVoiceAudioLevelStore(): RealtimeVoiceAudioLevelStore {\n let snapshot = SILENT_LEVELS;\n const listeners = new Set<() => void>();\n\n const set = (levels: RealtimeVoiceAudioLevels) => {\n const next = {\n input: clampLevel(levels.input),\n output: clampLevel(levels.output),\n };\n if (next.input === snapshot.input && next.output === snapshot.output)\n return;\n snapshot = next;\n for (const listener of listeners) listener();\n };\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n set,\n reset: () => set(SILENT_LEVELS),\n };\n}\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type RealtimeVoiceTranscriptRole = "user" | "assistant";
|
|
2
|
+
export interface RealtimeVoiceTranscriptMessage {
|
|
3
|
+
id: string;
|
|
4
|
+
threadId: string;
|
|
5
|
+
role: RealtimeVoiceTranscriptRole;
|
|
6
|
+
text: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface RealtimeVoiceTranscriptSink {
|
|
10
|
+
threadId: string;
|
|
11
|
+
active: boolean;
|
|
12
|
+
append: (message: RealtimeVoiceTranscriptMessage) => boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface RealtimeVoiceTranscriptRegistry {
|
|
15
|
+
activeThreadId: () => string | undefined;
|
|
16
|
+
publish: (message: RealtimeVoiceTranscriptMessage) => void;
|
|
17
|
+
register: (sink: RealtimeVoiceTranscriptSink) => () => void;
|
|
18
|
+
pendingCount: () => number;
|
|
19
|
+
}
|
|
20
|
+
export declare function appendRealtimeVoiceTranscriptToRepository(value: unknown, transcript: RealtimeVoiceTranscriptMessage): {
|
|
21
|
+
appended: boolean;
|
|
22
|
+
repository: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
export declare function createRealtimeVoiceTranscriptRegistry(): RealtimeVoiceTranscriptRegistry;
|
|
25
|
+
export declare const realtimeVoiceTranscriptRegistry: RealtimeVoiceTranscriptRegistry;
|
|
26
|
+
//# sourceMappingURL=realtime-voice-transcript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-voice-transcript.d.ts","sourceRoot":"","sources":["../../../src/client/composer/realtime-voice-transcript.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,WAAW,CAAC;AAE/D,MAAM,WAAW,8BAA8B;IAC7C,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,2BAA2B,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,CAAC,OAAO,EAAE,8BAA8B,KAAK,OAAO,CAAC;CAC9D;AAED,MAAM,WAAW,+BAA+B;IAC9C,cAAc,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IACzC,OAAO,EAAE,CAAC,OAAO,EAAE,8BAA8B,KAAK,IAAI,CAAC;IAC3D,QAAQ,EAAE,CAAC,IAAI,EAAE,2BAA2B,KAAK,MAAM,IAAI,CAAC;IAC5D,YAAY,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,wBAAgB,yCAAyC,CACvD,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,8BAA8B,GACzC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CA6C5D;AAED,wBAAgB,qCAAqC,IAAI,+BAA+B,CAyDvF;AAED,eAAO,MAAM,+BAA+B,iCACH,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export function appendRealtimeVoiceTranscriptToRepository(value, transcript) {
|
|
2
|
+
const repository = value && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: {};
|
|
5
|
+
const messages = Array.isArray(repository.messages)
|
|
6
|
+
? [...repository.messages]
|
|
7
|
+
: [];
|
|
8
|
+
const duplicate = messages.some((entry) => {
|
|
9
|
+
if (!entry || typeof entry !== "object")
|
|
10
|
+
return false;
|
|
11
|
+
const record = entry;
|
|
12
|
+
const message = record.message && typeof record.message === "object"
|
|
13
|
+
? record.message
|
|
14
|
+
: record;
|
|
15
|
+
return message.id === transcript.id;
|
|
16
|
+
});
|
|
17
|
+
if (duplicate)
|
|
18
|
+
return { appended: false, repository };
|
|
19
|
+
const parentId = typeof repository.headId === "string" ? repository.headId : null;
|
|
20
|
+
const createdAt = new Date(transcript.createdAt);
|
|
21
|
+
const message = {
|
|
22
|
+
id: transcript.id,
|
|
23
|
+
role: transcript.role,
|
|
24
|
+
content: [{ type: "text", text: transcript.text }],
|
|
25
|
+
createdAt: Number.isNaN(createdAt.getTime()) ? new Date() : createdAt,
|
|
26
|
+
metadata: {
|
|
27
|
+
custom: {
|
|
28
|
+
source: "realtime-voice",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
...(transcript.role === "assistant"
|
|
32
|
+
? { status: { type: "complete", reason: "stop" } }
|
|
33
|
+
: {}),
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
appended: true,
|
|
37
|
+
repository: {
|
|
38
|
+
...repository,
|
|
39
|
+
messages: [...messages, { message, parentId }],
|
|
40
|
+
headId: transcript.id,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function createRealtimeVoiceTranscriptRegistry() {
|
|
45
|
+
const sinks = new Map();
|
|
46
|
+
const pending = new Map();
|
|
47
|
+
const delivered = new Set();
|
|
48
|
+
let order = 0;
|
|
49
|
+
const flush = () => {
|
|
50
|
+
for (const [id, message] of pending) {
|
|
51
|
+
let consumed = false;
|
|
52
|
+
for (const sink of sinks.values()) {
|
|
53
|
+
if (sink.threadId !== message.threadId)
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
consumed = sink.append(message) || consumed;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Keep the completed transcript queued until the chat can import it.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!consumed)
|
|
63
|
+
continue;
|
|
64
|
+
pending.delete(id);
|
|
65
|
+
delivered.add(id);
|
|
66
|
+
if (delivered.size > 1_000) {
|
|
67
|
+
const oldest = delivered.values().next().value;
|
|
68
|
+
if (typeof oldest === "string")
|
|
69
|
+
delivered.delete(oldest);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
activeThreadId() {
|
|
75
|
+
let active;
|
|
76
|
+
for (const sink of sinks.values()) {
|
|
77
|
+
if (!sink.active || (active && sink.order <= active.order))
|
|
78
|
+
continue;
|
|
79
|
+
active = sink;
|
|
80
|
+
}
|
|
81
|
+
return active?.threadId;
|
|
82
|
+
},
|
|
83
|
+
publish(message) {
|
|
84
|
+
const id = message.id.trim();
|
|
85
|
+
const threadId = message.threadId.trim();
|
|
86
|
+
const text = message.text.trim();
|
|
87
|
+
if (!id || !threadId || !text || delivered.has(id) || pending.has(id)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
pending.set(id, { ...message, id, threadId, text });
|
|
91
|
+
flush();
|
|
92
|
+
},
|
|
93
|
+
register(sink) {
|
|
94
|
+
const token = Symbol("realtime-voice-transcript-sink");
|
|
95
|
+
sinks.set(token, { ...sink, order: ++order });
|
|
96
|
+
flush();
|
|
97
|
+
return () => sinks.delete(token);
|
|
98
|
+
},
|
|
99
|
+
pendingCount: () => pending.size,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export const realtimeVoiceTranscriptRegistry = createRealtimeVoiceTranscriptRegistry();
|
|
103
|
+
//# sourceMappingURL=realtime-voice-transcript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"realtime-voice-transcript.js","sourceRoot":"","sources":["../../../src/client/composer/realtime-voice-transcript.ts"],"names":[],"mappings":"AAuBA,MAAM,UAAU,yCAAyC,CACvD,KAAc,EACd,UAA0C;IAE1C,MAAM,UAAU,GACd,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACzD,CAAC,CAAE,KAAiC;QACpC,CAAC,CAAC,EAAE,CAAC;IACT,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC;QACjD,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC;QAC1B,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;QACxC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QACtD,MAAM,MAAM,GAAG,KAAgC,CAAC;QAChD,MAAM,OAAO,GACX,MAAM,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ;YAClD,CAAC,CAAE,MAAM,CAAC,OAAmC;YAC7C,CAAC,CAAC,MAAM,CAAC;QACb,OAAO,OAAO,CAAC,EAAE,KAAK,UAAU,CAAC,EAAE,CAAC;IACtC,CAAC,CAAC,CAAC;IACH,IAAI,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAEtD,MAAM,QAAQ,GACZ,OAAO,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;IACnE,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG;QACd,EAAE,EAAE,UAAU,CAAC,EAAE;QACjB,IAAI,EAAE,UAAU,CAAC,IAAI;QACrB,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC;QAClD,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS;QACrE,QAAQ,EAAE;YACR,MAAM,EAAE;gBACN,MAAM,EAAE,gBAAgB;aACzB;SACF;QACD,GAAG,CAAC,UAAU,CAAC,IAAI,KAAK,WAAW;YACjC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YAClD,CAAC,CAAC,EAAE,CAAC;KACR,CAAC;IAEF,OAAO;QACL,QAAQ,EAAE,IAAI;QACd,UAAU,EAAE;YACV,GAAG,UAAU;YACb,QAAQ,EAAE,CAAC,GAAG,QAAQ,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;YAC9C,MAAM,EAAE,UAAU,CAAC,EAAE;SACtB;KACF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,qCAAqC;IACnD,MAAM,KAAK,GAAG,IAAI,GAAG,EAGlB,CAAC;IACJ,MAAM,OAAO,GAAG,IAAI,GAAG,EAA0C,CAAC;IAClE,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,OAAO,EAAE,CAAC;YACpC,IAAI,QAAQ,GAAG,KAAK,CAAC;YACrB,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAClC,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ;oBAAE,SAAS;gBACjD,IAAI,CAAC;oBACH,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC;gBAC9C,CAAC;gBAAC,MAAM,CAAC;oBACP,qEAAqE;gBACvE,CAAC;YACH,CAAC;YACD,IAAI,CAAC,QAAQ;gBAAE,SAAS;YACxB,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACnB,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAClB,IAAI,SAAS,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;gBAC3B,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;gBAC/C,IAAI,OAAO,MAAM,KAAK,QAAQ;oBAAE,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,cAAc;YACZ,IAAI,MAAqE,CAAC;YAC1E,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAClC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;oBAAE,SAAS;gBACrE,MAAM,GAAG,IAAI,CAAC;YAChB,CAAC;YACD,OAAO,MAAM,EAAE,QAAQ,CAAC;QAC1B,CAAC;QACD,OAAO,CAAC,OAAO;YACb,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;YAC7B,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACjC,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;gBACtE,OAAO;YACT,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;YACpD,KAAK,EAAE,CAAC;QACV,CAAC;QACD,QAAQ,CAAC,IAAI;YACX,MAAM,KAAK,GAAG,MAAM,CAAC,gCAAgC,CAAC,CAAC;YACvD,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9C,KAAK,EAAE,CAAC;YACR,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnC,CAAC;QACD,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI;KACjC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,+BAA+B,GAC1C,qCAAqC,EAAE,CAAC","sourcesContent":["export type RealtimeVoiceTranscriptRole = \"user\" | \"assistant\";\n\nexport interface RealtimeVoiceTranscriptMessage {\n id: string;\n threadId: string;\n role: RealtimeVoiceTranscriptRole;\n text: string;\n createdAt: string;\n}\n\nexport interface RealtimeVoiceTranscriptSink {\n threadId: string;\n active: boolean;\n append: (message: RealtimeVoiceTranscriptMessage) => boolean;\n}\n\nexport interface RealtimeVoiceTranscriptRegistry {\n activeThreadId: () => string | undefined;\n publish: (message: RealtimeVoiceTranscriptMessage) => void;\n register: (sink: RealtimeVoiceTranscriptSink) => () => void;\n pendingCount: () => number;\n}\n\nexport function appendRealtimeVoiceTranscriptToRepository(\n value: unknown,\n transcript: RealtimeVoiceTranscriptMessage,\n): { appended: boolean; repository: Record<string, unknown> } {\n const repository =\n value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : {};\n const messages = Array.isArray(repository.messages)\n ? [...repository.messages]\n : [];\n const duplicate = messages.some((entry) => {\n if (!entry || typeof entry !== \"object\") return false;\n const record = entry as Record<string, unknown>;\n const message =\n record.message && typeof record.message === \"object\"\n ? (record.message as Record<string, unknown>)\n : record;\n return message.id === transcript.id;\n });\n if (duplicate) return { appended: false, repository };\n\n const parentId =\n typeof repository.headId === \"string\" ? repository.headId : null;\n const createdAt = new Date(transcript.createdAt);\n const message = {\n id: transcript.id,\n role: transcript.role,\n content: [{ type: \"text\", text: transcript.text }],\n createdAt: Number.isNaN(createdAt.getTime()) ? new Date() : createdAt,\n metadata: {\n custom: {\n source: \"realtime-voice\",\n },\n },\n ...(transcript.role === \"assistant\"\n ? { status: { type: \"complete\", reason: \"stop\" } }\n : {}),\n };\n\n return {\n appended: true,\n repository: {\n ...repository,\n messages: [...messages, { message, parentId }],\n headId: transcript.id,\n },\n };\n}\n\nexport function createRealtimeVoiceTranscriptRegistry(): RealtimeVoiceTranscriptRegistry {\n const sinks = new Map<\n symbol,\n RealtimeVoiceTranscriptSink & { order: number }\n >();\n const pending = new Map<string, RealtimeVoiceTranscriptMessage>();\n const delivered = new Set<string>();\n let order = 0;\n\n const flush = () => {\n for (const [id, message] of pending) {\n let consumed = false;\n for (const sink of sinks.values()) {\n if (sink.threadId !== message.threadId) continue;\n try {\n consumed = sink.append(message) || consumed;\n } catch {\n // Keep the completed transcript queued until the chat can import it.\n }\n }\n if (!consumed) continue;\n pending.delete(id);\n delivered.add(id);\n if (delivered.size > 1_000) {\n const oldest = delivered.values().next().value;\n if (typeof oldest === \"string\") delivered.delete(oldest);\n }\n }\n };\n\n return {\n activeThreadId() {\n let active: (RealtimeVoiceTranscriptSink & { order: number }) | undefined;\n for (const sink of sinks.values()) {\n if (!sink.active || (active && sink.order <= active.order)) continue;\n active = sink;\n }\n return active?.threadId;\n },\n publish(message) {\n const id = message.id.trim();\n const threadId = message.threadId.trim();\n const text = message.text.trim();\n if (!id || !threadId || !text || delivered.has(id) || pending.has(id)) {\n return;\n }\n pending.set(id, { ...message, id, threadId, text });\n flush();\n },\n register(sink) {\n const token = Symbol(\"realtime-voice-transcript-sink\");\n sinks.set(token, { ...sink, order: ++order });\n flush();\n return () => sinks.delete(token);\n },\n pendingCount: () => pending.size,\n };\n}\n\nexport const realtimeVoiceTranscriptRegistry =\n createRealtimeVoiceTranscriptRegistry();\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import { type RealtimeVoiceAudioLevelStore } from "./realtime-voice-audio-level.js";
|
|
2
3
|
import { type RealtimeVoiceModeCopy, type RealtimeVoiceModeState } from "./RealtimeVoiceMode.js";
|
|
3
4
|
type RealtimeServerEvent = Record<string, unknown> & {
|
|
4
5
|
type?: string;
|
|
@@ -14,10 +15,23 @@ export interface RealtimeVoiceModeApi {
|
|
|
14
15
|
active: boolean;
|
|
15
16
|
errorMessage: string | null;
|
|
16
17
|
chatVisible: boolean;
|
|
18
|
+
audioLevels: RealtimeVoiceAudioLevelStore;
|
|
17
19
|
start: () => Promise<void>;
|
|
18
20
|
end: () => void;
|
|
19
21
|
toggleChat: () => void;
|
|
20
22
|
}
|
|
23
|
+
export interface CompletedRealtimeVoiceTranscript {
|
|
24
|
+
role: "user" | "assistant";
|
|
25
|
+
text: string;
|
|
26
|
+
providerId?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Voice mode owns the chat only temporarily. Restore the captured transcript
|
|
30
|
+
* when it is still the user's active thread (or the chat has no active thread)
|
|
31
|
+
* but never pull them back after they deliberately selected another thread.
|
|
32
|
+
*/
|
|
33
|
+
export declare function shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId: string | undefined, activeThreadId: string | undefined): transcriptThreadId is string;
|
|
34
|
+
export declare function extractCompletedRealtimeVoiceTranscript(event: RealtimeServerEvent): CompletedRealtimeVoiceTranscript | null;
|
|
21
35
|
export interface RealtimeVoiceModeProviderProps {
|
|
22
36
|
children: ReactNode;
|
|
23
37
|
browserTabId?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useRealtimeVoiceMode.d.ts","sourceRoot":"","sources":["../../../src/client/composer/useRealtimeVoiceMode.tsx"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useRealtimeVoiceMode.d.ts","sourceRoot":"","sources":["../../../src/client/composer/useRealtimeVoiceMode.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,16 +1,43 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
+
import { requestAgentChatThreadOpen } from "../agent-chat.js";
|
|
4
5
|
import { SIDEBAR_STATE_CHANGE_EVENT, } from "../agent-sidebar-state.js";
|
|
5
6
|
import { agentNativePath } from "../api-path.js";
|
|
6
7
|
import { readClientAppState, setClientAppState } from "../application-state.js";
|
|
7
8
|
import { getBrowserTabId } from "../browser-tab-id.js";
|
|
8
9
|
import { useT } from "../i18n.js";
|
|
10
|
+
import { createRealtimeVoiceAudioLevelStore, normalizeRealtimeVoiceRms, smoothRealtimeVoiceLevel, } from "./realtime-voice-audio-level.js";
|
|
11
|
+
import { realtimeVoiceTranscriptRegistry } from "./realtime-voice-transcript.js";
|
|
9
12
|
import { RealtimeVoiceModeDock, } from "./RealtimeVoiceMode.js";
|
|
10
13
|
const REALTIME_VOICE_STATE_KEY = "realtime-voice-session";
|
|
11
14
|
const REALTIME_VOICE_REQUEST_SOURCE = "realtime-voice";
|
|
12
15
|
const REALTIME_VOICE_SESSION_PATH = "/_agent-native/realtime-voice/session";
|
|
13
16
|
const REALTIME_VOICE_TOOL_PATH = "/_agent-native/realtime-voice/tool";
|
|
17
|
+
/**
|
|
18
|
+
* Voice mode owns the chat only temporarily. Restore the captured transcript
|
|
19
|
+
* when it is still the user's active thread (or the chat has no active thread)
|
|
20
|
+
* but never pull them back after they deliberately selected another thread.
|
|
21
|
+
*/
|
|
22
|
+
export function shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId, activeThreadId) {
|
|
23
|
+
return Boolean(transcriptThreadId &&
|
|
24
|
+
(!activeThreadId || activeThreadId === transcriptThreadId));
|
|
25
|
+
}
|
|
26
|
+
export function extractCompletedRealtimeVoiceTranscript(event) {
|
|
27
|
+
const userCompleted = event.type === "conversation.item.input_audio_transcription.completed";
|
|
28
|
+
const assistantCompleted = event.type === "response.output_audio_transcript.done";
|
|
29
|
+
if (!userCompleted && !assistantCompleted)
|
|
30
|
+
return null;
|
|
31
|
+
const text = typeof event.transcript === "string" ? event.transcript.trim() : "";
|
|
32
|
+
if (!text)
|
|
33
|
+
return null;
|
|
34
|
+
const providerId = [event.item_id, event.response_id, event.event_id].find((value) => typeof value === "string" && value.length > 0);
|
|
35
|
+
return {
|
|
36
|
+
role: userCompleted ? "user" : "assistant",
|
|
37
|
+
text,
|
|
38
|
+
...(providerId ? { providerId } : {}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
14
41
|
function errorMessage(error) {
|
|
15
42
|
return error instanceof Error ? error.message : String(error);
|
|
16
43
|
}
|
|
@@ -169,17 +196,29 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
169
196
|
const [state, setState] = useState("idle");
|
|
170
197
|
const [error, setError] = useState(null);
|
|
171
198
|
const [chatVisible, setChatVisible] = useState(false);
|
|
199
|
+
const [audioLevels] = useState(createRealtimeVoiceAudioLevelStore);
|
|
172
200
|
const stateRef = useRef(state);
|
|
173
201
|
const peerRef = useRef(null);
|
|
174
202
|
const channelRef = useRef(null);
|
|
175
203
|
const streamRef = useRef(null);
|
|
176
204
|
const audioRef = useRef(null);
|
|
205
|
+
const audioContextRef = useRef(null);
|
|
206
|
+
const inputAnalyserRef = useRef(null);
|
|
207
|
+
const outputAnalyserRef = useRef(null);
|
|
208
|
+
const inputMeterBufferRef = useRef(null);
|
|
209
|
+
const outputMeterBufferRef = useRef(null);
|
|
210
|
+
const inputSourceRef = useRef(null);
|
|
211
|
+
const outputSourceRef = useRef(null);
|
|
212
|
+
const meterFrameRef = useRef(null);
|
|
213
|
+
const lastMeterSampleRef = useRef(0);
|
|
177
214
|
const abortRef = useRef(null);
|
|
178
215
|
const handledCallsRef = useRef(new Set());
|
|
179
216
|
const sessionIdRef = useRef(undefined);
|
|
180
217
|
const startedAtRef = useRef(undefined);
|
|
181
218
|
const lastUserTextRef = useRef("");
|
|
182
219
|
const lastAssistantTextRef = useRef("");
|
|
220
|
+
const transcriptThreadIdRef = useRef(undefined);
|
|
221
|
+
const transcriptSequenceRef = useRef(0);
|
|
183
222
|
useEffect(() => {
|
|
184
223
|
stateRef.current = state;
|
|
185
224
|
}, [state]);
|
|
@@ -205,6 +244,65 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
205
244
|
setState(nextState);
|
|
206
245
|
syncAppState(nextState);
|
|
207
246
|
}, [syncAppState]);
|
|
247
|
+
const startMeterLoop = useCallback(() => {
|
|
248
|
+
if (meterFrameRef.current !== null)
|
|
249
|
+
return;
|
|
250
|
+
const sample = (timestamp) => {
|
|
251
|
+
meterFrameRef.current = requestAnimationFrame(sample);
|
|
252
|
+
if (timestamp - lastMeterSampleRef.current < 50)
|
|
253
|
+
return;
|
|
254
|
+
lastMeterSampleRef.current = timestamp;
|
|
255
|
+
const current = audioLevels.getSnapshot();
|
|
256
|
+
let input = current.input;
|
|
257
|
+
let output = current.output;
|
|
258
|
+
const inputAnalyser = inputAnalyserRef.current;
|
|
259
|
+
const inputBuffer = inputMeterBufferRef.current;
|
|
260
|
+
if (inputAnalyser && inputBuffer) {
|
|
261
|
+
inputAnalyser.getByteTimeDomainData(inputBuffer);
|
|
262
|
+
input = smoothRealtimeVoiceLevel(input, normalizeRealtimeVoiceRms(inputBuffer));
|
|
263
|
+
}
|
|
264
|
+
const outputAnalyser = outputAnalyserRef.current;
|
|
265
|
+
const outputBuffer = outputMeterBufferRef.current;
|
|
266
|
+
if (outputAnalyser && outputBuffer) {
|
|
267
|
+
outputAnalyser.getByteTimeDomainData(outputBuffer);
|
|
268
|
+
output = smoothRealtimeVoiceLevel(output, normalizeRealtimeVoiceRms(outputBuffer));
|
|
269
|
+
}
|
|
270
|
+
audioLevels.set({ input, output });
|
|
271
|
+
};
|
|
272
|
+
meterFrameRef.current = requestAnimationFrame(sample);
|
|
273
|
+
}, [audioLevels]);
|
|
274
|
+
const attachAudioMeter = useCallback((stream, channel) => {
|
|
275
|
+
try {
|
|
276
|
+
const AudioCtor = window.AudioContext ??
|
|
277
|
+
window.webkitAudioContext;
|
|
278
|
+
if (!AudioCtor)
|
|
279
|
+
return;
|
|
280
|
+
const context = audioContextRef.current ?? new AudioCtor();
|
|
281
|
+
audioContextRef.current = context;
|
|
282
|
+
void context.resume().catch(() => undefined);
|
|
283
|
+
const analyser = context.createAnalyser();
|
|
284
|
+
analyser.fftSize = 512;
|
|
285
|
+
const source = context.createMediaStreamSource(stream);
|
|
286
|
+
source.connect(analyser);
|
|
287
|
+
const buffer = new Uint8Array(analyser.frequencyBinCount);
|
|
288
|
+
if (channel === "input") {
|
|
289
|
+
inputSourceRef.current?.disconnect();
|
|
290
|
+
inputSourceRef.current = source;
|
|
291
|
+
inputAnalyserRef.current = analyser;
|
|
292
|
+
inputMeterBufferRef.current = buffer;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
outputSourceRef.current?.disconnect();
|
|
296
|
+
outputSourceRef.current = source;
|
|
297
|
+
outputAnalyserRef.current = analyser;
|
|
298
|
+
outputMeterBufferRef.current = buffer;
|
|
299
|
+
}
|
|
300
|
+
startMeterLoop();
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// Audio metering is visual-only; keep the realtime call healthy.
|
|
304
|
+
}
|
|
305
|
+
}, [startMeterLoop]);
|
|
208
306
|
const cleanupTransport = useCallback(() => {
|
|
209
307
|
abortRef.current?.abort();
|
|
210
308
|
abortRef.current = null;
|
|
@@ -220,8 +318,26 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
220
318
|
audioRef.current.srcObject = null;
|
|
221
319
|
}
|
|
222
320
|
audioRef.current = null;
|
|
321
|
+
if (meterFrameRef.current !== null) {
|
|
322
|
+
cancelAnimationFrame(meterFrameRef.current);
|
|
323
|
+
meterFrameRef.current = null;
|
|
324
|
+
}
|
|
325
|
+
inputSourceRef.current?.disconnect();
|
|
326
|
+
outputSourceRef.current?.disconnect();
|
|
327
|
+
inputSourceRef.current = null;
|
|
328
|
+
outputSourceRef.current = null;
|
|
329
|
+
inputAnalyserRef.current = null;
|
|
330
|
+
outputAnalyserRef.current = null;
|
|
331
|
+
inputMeterBufferRef.current = null;
|
|
332
|
+
outputMeterBufferRef.current = null;
|
|
333
|
+
lastMeterSampleRef.current = 0;
|
|
334
|
+
const audioContext = audioContextRef.current;
|
|
335
|
+
audioContextRef.current = null;
|
|
336
|
+
if (audioContext)
|
|
337
|
+
void audioContext.close().catch(() => undefined);
|
|
338
|
+
audioLevels.reset();
|
|
223
339
|
handledCallsRef.current.clear();
|
|
224
|
-
}, []);
|
|
340
|
+
}, [audioLevels]);
|
|
225
341
|
const fail = useCallback((message, options) => {
|
|
226
342
|
cleanupTransport();
|
|
227
343
|
setError(message);
|
|
@@ -263,6 +379,21 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
263
379
|
});
|
|
264
380
|
sendDataChannelEvent(channelRef.current, { type: "response.create" });
|
|
265
381
|
}, [browserTabId, transition]);
|
|
382
|
+
const persistCompletedTranscript = useCallback((event) => {
|
|
383
|
+
const transcript = extractCompletedRealtimeVoiceTranscript(event);
|
|
384
|
+
const threadId = transcriptThreadIdRef.current;
|
|
385
|
+
if (!transcript || !threadId)
|
|
386
|
+
return;
|
|
387
|
+
const sessionIdentity = sessionIdRef.current ?? startedAtRef.current ?? "pending";
|
|
388
|
+
const providerIdentity = transcript.providerId ?? `sequence-${++transcriptSequenceRef.current}`;
|
|
389
|
+
realtimeVoiceTranscriptRegistry.publish({
|
|
390
|
+
id: `realtime-voice:${sessionIdentity}:${transcript.role}:${providerIdentity}`,
|
|
391
|
+
threadId,
|
|
392
|
+
role: transcript.role,
|
|
393
|
+
text: transcript.text,
|
|
394
|
+
createdAt: new Date().toISOString(),
|
|
395
|
+
});
|
|
396
|
+
}, []);
|
|
266
397
|
const handleServerEvent = useCallback((event) => {
|
|
267
398
|
if (event.type === "session.created") {
|
|
268
399
|
const session = event.session;
|
|
@@ -293,13 +424,15 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
293
424
|
if (typeof event.transcript === "string") {
|
|
294
425
|
lastAssistantTextRef.current = event.transcript;
|
|
295
426
|
}
|
|
427
|
+
persistCompletedTranscript(event);
|
|
296
428
|
syncAppState("speaking");
|
|
297
429
|
}
|
|
298
430
|
else if (event.type === "conversation.item.input_audio_transcription.completed") {
|
|
299
431
|
if (typeof event.transcript === "string") {
|
|
300
432
|
lastUserTextRef.current = event.transcript;
|
|
301
|
-
syncAppState("working");
|
|
302
433
|
}
|
|
434
|
+
persistCompletedTranscript(event);
|
|
435
|
+
syncAppState("working");
|
|
303
436
|
}
|
|
304
437
|
else if (event.type === "response.done") {
|
|
305
438
|
const response = event.response;
|
|
@@ -330,7 +463,14 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
330
463
|
if (event.type === "response.done" && calls.length === 0) {
|
|
331
464
|
transition("listening");
|
|
332
465
|
}
|
|
333
|
-
}, [
|
|
466
|
+
}, [
|
|
467
|
+
copy,
|
|
468
|
+
fail,
|
|
469
|
+
handleFunctionCall,
|
|
470
|
+
persistCompletedTranscript,
|
|
471
|
+
syncAppState,
|
|
472
|
+
transition,
|
|
473
|
+
]);
|
|
334
474
|
const start = useCallback(async () => {
|
|
335
475
|
if (stateRef.current !== "idle")
|
|
336
476
|
return;
|
|
@@ -345,6 +485,9 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
345
485
|
lastUserTextRef.current = "";
|
|
346
486
|
lastAssistantTextRef.current = "";
|
|
347
487
|
sessionIdRef.current = undefined;
|
|
488
|
+
transcriptThreadIdRef.current =
|
|
489
|
+
realtimeVoiceTranscriptRegistry.activeThreadId();
|
|
490
|
+
transcriptSequenceRef.current = 0;
|
|
348
491
|
transition("connecting");
|
|
349
492
|
setChatVisible(false);
|
|
350
493
|
window.dispatchEvent(new Event("agent-panel:close"));
|
|
@@ -359,6 +502,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
359
502
|
},
|
|
360
503
|
});
|
|
361
504
|
streamRef.current = stream;
|
|
505
|
+
attachAudioMeter(stream, "input");
|
|
362
506
|
const peer = new RTCPeerConnection();
|
|
363
507
|
peerRef.current = peer;
|
|
364
508
|
for (const track of stream.getAudioTracks())
|
|
@@ -368,7 +512,10 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
368
512
|
audio.setAttribute("playsinline", "");
|
|
369
513
|
audioRef.current = audio;
|
|
370
514
|
peer.ontrack = (trackEvent) => {
|
|
371
|
-
|
|
515
|
+
const remoteStream = trackEvent.streams[0] ?? null;
|
|
516
|
+
audio.srcObject = remoteStream;
|
|
517
|
+
if (remoteStream)
|
|
518
|
+
attachAudioMeter(remoteStream, "output");
|
|
372
519
|
void audio.play().catch(() => undefined);
|
|
373
520
|
};
|
|
374
521
|
const channel = peer.createDataChannel("oai-events");
|
|
@@ -408,15 +555,38 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
408
555
|
const status = startError?.status;
|
|
409
556
|
fail(errorMessage(startError), { openKeySettings: status === 400 });
|
|
410
557
|
}
|
|
411
|
-
}, [
|
|
558
|
+
}, [
|
|
559
|
+
attachAudioMeter,
|
|
560
|
+
browserTabId,
|
|
561
|
+
copy,
|
|
562
|
+
fail,
|
|
563
|
+
handleServerEvent,
|
|
564
|
+
transition,
|
|
565
|
+
]);
|
|
412
566
|
const end = useCallback(() => {
|
|
413
567
|
if (stateRef.current === "idle" || stateRef.current === "ending")
|
|
414
568
|
return;
|
|
569
|
+
const transcriptThreadId = transcriptThreadIdRef.current;
|
|
570
|
+
const activeThreadId = realtimeVoiceTranscriptRegistry.activeThreadId();
|
|
415
571
|
transition("ending");
|
|
416
572
|
cleanupTransport();
|
|
417
573
|
setError(null);
|
|
418
574
|
sessionIdRef.current = undefined;
|
|
419
575
|
startedAtRef.current = undefined;
|
|
576
|
+
transcriptThreadIdRef.current = undefined;
|
|
577
|
+
setChatVisible(true);
|
|
578
|
+
if (shouldRestoreRealtimeVoiceTranscriptThread(transcriptThreadId, activeThreadId)) {
|
|
579
|
+
requestAgentChatThreadOpen({
|
|
580
|
+
threadId: transcriptThreadId,
|
|
581
|
+
// The request is delivered asynchronously. Re-checking this at the
|
|
582
|
+
// receiver prevents a navigation that happened during that gap from
|
|
583
|
+
// being overwritten.
|
|
584
|
+
onlyIfActiveThreadId: transcriptThreadId,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
window.dispatchEvent(new Event("agent-panel:open"));
|
|
589
|
+
}
|
|
420
590
|
transition("idle");
|
|
421
591
|
}, [cleanupTransport, transition]);
|
|
422
592
|
const toggleChat = useCallback(() => {
|
|
@@ -440,6 +610,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
440
610
|
active: state !== "idle",
|
|
441
611
|
errorMessage: error,
|
|
442
612
|
chatVisible,
|
|
613
|
+
audioLevels,
|
|
443
614
|
start,
|
|
444
615
|
end,
|
|
445
616
|
toggleChat,
|
|
@@ -451,7 +622,7 @@ export function RealtimeVoiceModeProvider({ children, browserTabId, }) {
|
|
|
451
622
|
const copy = useRealtimeVoiceModeCopy();
|
|
452
623
|
const voice = useRealtimeVoiceModeController(resolvedBrowserTabId, copy);
|
|
453
624
|
return (_jsxs(RealtimeVoiceModeContext.Provider, { value: voice, children: [children, voice.active && typeof document !== "undefined"
|
|
454
|
-
? createPortal(_jsx(RealtimeVoiceModeDock, { state: voice.state === "idle" ? "ending" : voice.state, copy: copy, chatVisible: voice.chatVisible, onToggleChat: voice.toggleChat, onEndVoiceMode: voice.end, errorMessage: voice.errorMessage
|
|
625
|
+
? createPortal(_jsx(RealtimeVoiceModeDock, { state: voice.state === "idle" ? "ending" : voice.state, copy: copy, chatVisible: voice.chatVisible, audioLevels: voice.audioLevels, onToggleChat: voice.toggleChat, onEndVoiceMode: voice.end, errorMessage: voice.errorMessage }), document.body)
|
|
455
626
|
: null] }));
|
|
456
627
|
}
|
|
457
628
|
/**
|