@agent-native/core 0.98.0 → 0.98.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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +32 -0
- package/corpus/core/docs/content/locales/ar-SA/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/ar-SA/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/de-DE/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/de-DE/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/es-ES/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/es-ES/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/fr-FR/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/fr-FR/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/hi-IN/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/hi-IN/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/ja-JP/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/ja-JP/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/ko-KR/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/ko-KR/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/pt-BR/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/pt-BR/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-CN/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/zh-CN/template-plan.mdx +1 -1
- package/corpus/core/docs/content/locales/zh-TW/pr-visual-recap.mdx +25 -1
- package/corpus/core/docs/content/locales/zh-TW/template-plan.mdx +1 -1
- package/corpus/core/docs/content/pr-visual-recap.mdx +50 -1
- package/corpus/core/docs/content/template-plan.mdx +1 -1
- package/corpus/core/package.json +2 -2
- package/corpus/core/src/action-change-marker.ts +8 -1
- package/corpus/core/src/changelog/parse.ts +8 -3
- package/corpus/core/src/cli/plan-local.ts +43 -11
- package/corpus/core/src/cli/pr-visual-recap-workflow.ts +2 -9
- package/corpus/core/src/cli/recap.ts +356 -4
- package/corpus/core/src/cli/skills-content/local-files.ts +17 -12
- package/corpus/core/src/client/AgentPanel.tsx +2 -3
- package/corpus/core/src/client/AssistantChat.tsx +83 -39
- package/corpus/core/src/client/analytics.ts +125 -14
- package/corpus/core/src/client/blocks/library/wireframe-kit.tsx +16 -2
- package/corpus/core/src/client/chat-model-groups.ts +28 -0
- package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +442 -85
- package/corpus/core/src/client/composer/TiptapComposer.tsx +46 -29
- package/corpus/core/src/client/composer/VoiceButton.tsx +2 -1
- package/corpus/core/src/client/composer/index.ts +5 -0
- package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +1206 -67
- package/corpus/core/src/client/index.ts +10 -0
- package/corpus/core/src/client/progress/RunsTray.tsx +13 -1
- package/corpus/core/src/client/session-replay.ts +10 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +4 -28
- package/corpus/core/src/client/settings/SecretsSection.tsx +387 -266
- package/corpus/core/src/client/settings/SettingsPanel.tsx +117 -38
- package/corpus/core/src/client/url-scrub.ts +13 -0
- package/corpus/core/src/client/use-action.ts +12 -0
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +8 -1
- package/corpus/core/src/client/use-agent-engine-configured.ts +17 -3
- package/corpus/core/src/client/use-db-sync.ts +84 -34
- package/corpus/core/src/client/use-demo-mode-status.ts +44 -0
- package/corpus/core/src/client/use-pausing-interval.ts +14 -4
- package/corpus/core/src/integrations/scope-store.ts +1 -1
- package/corpus/core/src/integrations/usage-budget-store.ts +1 -1
- package/corpus/core/src/localization/default-messages.ts +44 -1
- package/corpus/core/src/mcp-client/workspace-servers.ts +4 -4
- package/corpus/core/src/secrets/crypto.ts +28 -8
- package/corpus/core/src/server/action-change.ts +5 -1
- package/corpus/core/src/server/action-routes.ts +8 -0
- package/corpus/core/src/server/poll.ts +1 -0
- package/corpus/core/src/server/realtime-voice.ts +299 -13
- package/corpus/core/src/styles/agent-native.css +2 -2
- package/corpus/core/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/core/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
- package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
- package/corpus/core/src/vite/client.ts +9 -1
- package/corpus/templates/analytics/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +10 -5
- package/corpus/templates/analytics/AGENTS.md +17 -13
- package/corpus/templates/analytics/actions/navigate.ts +59 -5
- package/corpus/templates/analytics/actions/view-screen.ts +3 -6
- package/corpus/templates/analytics/app/components/ProviderCorpusJobNotifier.tsx +20 -3
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +20 -5
- package/corpus/templates/analytics/app/components/layout/Header.tsx +1 -1
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +21 -6
- package/corpus/templates/analytics/app/global.css +19 -20
- package/corpus/templates/analytics/app/lib/demo-chart-trend.ts +156 -0
- package/corpus/templates/analytics/app/lib/provider-corpus-job-sync.ts +26 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +35 -341
- package/corpus/templates/analytics/app/pages/monitoring/ErrorsPanel.tsx +38 -7
- package/corpus/templates/analytics/app/pages/monitoring/MonitoringPage.tsx +1 -1
- package/corpus/templates/analytics/app/pages/monitoring/errors/i18n.ts +3 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +333 -272
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +72 -17
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +297 -108
- package/corpus/templates/analytics/app/root.tsx +28 -8
- package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +2 -1
- package/corpus/templates/analytics/changelog/2026-06-25-referrer-dashboard-panels-show-external-domains.md +2 -1
- package/corpus/templates/analytics/changelog/2026-07-06-mobile-chat-header-is-more-compact-and-the-menu-button-opens-navigation.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-08-dashboard-history-and-chat-context.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-08-settings-are-cleaner-searchable-and-alerts-have-their-own-tab.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-08-sidebar-sections-stay-collapsed-until-opened.md +1 -0
- package/corpus/templates/analytics/changelog/2026-07-11-analytics-no-longer-repeatedly-restarts-background-requests-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-analytics-only-asks-for-replay-storage-when-you-enable-sessi.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-charts-now-show-their-loading-skeleton-while-refreshing.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-realtime-voice-now-shows-a-clear-connection-indicator-comple.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-replay-event-counts-stay-hidden-until-recordings-finish-load.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-replay-storage-key-mismatches-show-actionable-setup-guidance.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-replay-timelines-group-continuous-scrolling-and-dev-tools-co.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-filters-now-stay-compact-in-a-settings-menu-at-mediu.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replay-console-errors-now-link-to-matching-issues-or.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replays-load-large-recordings-faster-and-surface-fai.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replays-preserve-recorded-styles-and-viewport-layout.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-11-session-replays-stay-centered-instead-of-clipping-wide-recor.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-daily-dashboard-emails-reliably-include-the-complete-dashboa.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-12-demo-mode-charts-trend-upward.md +6 -0
- package/corpus/templates/analytics/package.json +1 -1
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +1 -303
- package/corpus/templates/analytics/server/handlers/session-replay.ts +57 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +2 -0
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +17 -33
- package/corpus/templates/analytics/server/lib/demo-dashboards.ts +16 -3
- package/corpus/templates/analytics/server/lib/session-replay-agent-context.ts +12 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +208 -2
- package/corpus/templates/analytics/server/plugins/onboarding.ts +7 -79
- package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks.get.ts +1 -0
- package/corpus/templates/assets/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/assets/app/global.css +4 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +0 -1
- package/corpus/templates/assets/app/routes/library.tsx +0 -1
- package/corpus/templates/brain/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/brain/app/global.css +5 -5
- package/corpus/templates/brain/app/root.tsx +0 -3
- package/corpus/templates/calendar/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/calendar/app/root.tsx +1 -0
- package/corpus/templates/chat/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
- package/corpus/templates/clips/app/hooks/use-auto-title.ts +74 -8
- package/corpus/templates/clips/app/hooks/use-library.ts +12 -25
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +20 -22
- package/corpus/templates/clips/changelog/2026-07-11-clips-no-longer-stay-stuck-on-preparing-clip-when-they-are-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-11-generated-titles-no-longer-cause-continuous-background-refreshes.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-11-s3-storage-setup-stays-collapsed-until-you-choose-it-as-the-.md +6 -0
- package/corpus/templates/content/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +2 -2
- package/corpus/templates/content/app/hooks/use-comments.ts +0 -1
- package/corpus/templates/content/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
- package/corpus/templates/content/changelog/2026-07-12-the-left-sidebar-stays-fixed-horizontally-while-scrolling-on.md +6 -0
- package/corpus/templates/design/app/components/layout/Header.tsx +1 -1
- package/corpus/templates/forms/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -2
- package/corpus/templates/macros/app/root.tsx +0 -3
- package/corpus/templates/mail/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/mail/app/hooks/use-compose-state.ts +1 -0
- package/corpus/templates/mail/app/root.tsx +1 -12
- package/corpus/templates/plan/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/corpus/templates/plan/.agents/skills/visual-plan/references/local-files.md +17 -12
- package/corpus/templates/plan/.agents/skills/visual-recap/references/local-files.md +17 -12
- package/corpus/templates/plan/actions/validate-local-plan-source.ts +4 -4
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +2 -1
- package/corpus/templates/plan/app/hooks/use-navigation-state.ts +5 -2
- package/corpus/templates/plan/app/hooks/use-plans.ts +6 -13
- package/corpus/templates/plan/app/lib/plan-local-bridge.ts +41 -0
- package/corpus/templates/plan/app/lib/plan-tracking.ts +3 -0
- package/corpus/templates/plan/app/pages/PlansPage.tsx +14 -36
- package/corpus/templates/plan/app/root.tsx +13 -3
- package/corpus/templates/plan/changelog/2026-07-11-plan-refreshes-only-when-content-changes.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-12-local-plan-source-now-stays-on-device.md +6 -0
- package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +4 -1
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
- package/corpus/templates/slides/app/components/layout/Header.tsx +1 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +1 -0
- package/corpus/templates/slides/app/hooks/use-slide-comments.ts +7 -31
- package/corpus/templates/slides/changelog/2026-07-11-comments-now-update-from-live-collaboration-events-without-c.md +6 -0
- package/dist/action-change-marker.d.ts +1 -0
- package/dist/action-change-marker.d.ts.map +1 -1
- package/dist/action-change-marker.js +7 -1
- package/dist/action-change-marker.js.map +1 -1
- package/dist/changelog/parse.d.ts +4 -2
- package/dist/changelog/parse.d.ts.map +1 -1
- package/dist/changelog/parse.js +5 -3
- package/dist/changelog/parse.js.map +1 -1
- package/dist/cli/plan-local.d.ts +6 -6
- package/dist/cli/plan-local.d.ts.map +1 -1
- package/dist/cli/plan-local.js +36 -7
- package/dist/cli/plan-local.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +2 -8
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +2 -8
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +22 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +286 -2
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills-content/local-files.d.ts +1 -1
- package/dist/cli/skills-content/local-files.d.ts.map +1 -1
- package/dist/cli/skills-content/local-files.js +17 -12
- package/dist/cli/skills-content/local-files.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +7 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +62 -40
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/analytics.d.ts +8 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +103 -13
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/blocks/library/wireframe-kit.d.ts +2 -0
- package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -1
- package/dist/client/blocks/library/wireframe-kit.js +9 -2
- package/dist/client/blocks/library/wireframe-kit.js.map +1 -1
- package/dist/client/chat-model-groups.d.ts.map +1 -1
- package/dist/client/chat-model-groups.js +24 -0
- package/dist/client/chat-model-groups.js.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts +40 -4
- package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.js +172 -42
- package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +4 -0
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +47 -29
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/composer/VoiceButton.d.ts.map +1 -1
- package/dist/client/composer/VoiceButton.js +2 -2
- package/dist/client/composer/VoiceButton.js.map +1 -1
- package/dist/client/composer/index.d.ts +2 -2
- package/dist/client/composer/index.d.ts.map +1 -1
- package/dist/client/composer/index.js +1 -1
- package/dist/client/composer/index.js.map +1 -1
- package/dist/client/composer/useRealtimeVoiceMode.d.ts +100 -3
- package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/useRealtimeVoiceMode.js +952 -51
- package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/client/index.d.ts +3 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +3 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/progress/RunsTray.d.ts.map +1 -1
- package/dist/client/progress/RunsTray.js +10 -1
- package/dist/client/progress/RunsTray.js.map +1 -1
- package/dist/client/session-replay.d.ts +2 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +7 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +4 -23
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SecretsSection.d.ts +2 -3
- package/dist/client/settings/SecretsSection.d.ts.map +1 -1
- package/dist/client/settings/SecretsSection.js +46 -22
- package/dist/client/settings/SecretsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +9 -0
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +46 -12
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/url-scrub.d.ts.map +1 -1
- package/dist/client/url-scrub.js +14 -0
- package/dist/client/url-scrub.js.map +1 -1
- package/dist/client/use-action.d.ts +2 -0
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +10 -0
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +2 -0
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +7 -1
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/client/use-agent-engine-configured.d.ts.map +1 -1
- package/dist/client/use-agent-engine-configured.js +17 -3
- package/dist/client/use-agent-engine-configured.js.map +1 -1
- package/dist/client/use-db-sync.d.ts +8 -2
- package/dist/client/use-db-sync.d.ts.map +1 -1
- package/dist/client/use-db-sync.js +57 -32
- package/dist/client/use-db-sync.js.map +1 -1
- package/dist/client/use-demo-mode-status.d.ts +12 -0
- package/dist/client/use-demo-mode-status.d.ts.map +1 -0
- package/dist/client/use-demo-mode-status.js +30 -0
- package/dist/client/use-demo-mode-status.js.map +1 -0
- package/dist/client/use-pausing-interval.d.ts.map +1 -1
- package/dist/client/use-pausing-interval.js +16 -4
- package/dist/client/use-pausing-interval.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/integrations/scope-store.js +1 -1
- package/dist/integrations/scope-store.js.map +1 -1
- package/dist/integrations/usage-budget-store.js +1 -1
- package/dist/integrations/usage-budget-store.js.map +1 -1
- package/dist/localization/default-messages.d.ts +41 -1
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +41 -1
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp-client/workspace-servers.js +4 -4
- package/dist/mcp-client/workspace-servers.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/crypto.d.ts +7 -6
- package/dist/secrets/crypto.d.ts.map +1 -1
- package/dist/secrets/crypto.js +30 -11
- package/dist/secrets/crypto.js.map +1 -1
- package/dist/server/action-change.d.ts +1 -0
- package/dist/server/action-change.d.ts.map +1 -1
- package/dist/server/action-change.js +4 -1
- package/dist/server/action-change.js.map +1 -1
- package/dist/server/action-routes.d.ts.map +1 -1
- package/dist/server/action-routes.js +5 -0
- package/dist/server/action-routes.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +1 -0
- package/dist/server/poll.js.map +1 -1
- package/dist/server/realtime-voice.d.ts +7 -0
- package/dist/server/realtime-voice.d.ts.map +1 -1
- package/dist/server/realtime-voice.js +222 -12
- package/dist/server/realtime-voice.js.map +1 -1
- package/dist/styles/agent-native.css +2 -2
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/dist/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
- package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +6 -1
- package/dist/vite/client.js.map +1 -1
- package/docs/content/locales/ar-SA/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/ar-SA/template-plan.mdx +1 -1
- package/docs/content/locales/de-DE/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/de-DE/template-plan.mdx +1 -1
- package/docs/content/locales/es-ES/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/es-ES/template-plan.mdx +1 -1
- package/docs/content/locales/fr-FR/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/fr-FR/template-plan.mdx +1 -1
- package/docs/content/locales/hi-IN/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/hi-IN/template-plan.mdx +1 -1
- package/docs/content/locales/ja-JP/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/ja-JP/template-plan.mdx +1 -1
- package/docs/content/locales/ko-KR/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/ko-KR/template-plan.mdx +1 -1
- package/docs/content/locales/pt-BR/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/pt-BR/template-plan.mdx +1 -1
- package/docs/content/locales/zh-CN/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/zh-CN/template-plan.mdx +1 -1
- package/docs/content/locales/zh-TW/pr-visual-recap.mdx +25 -1
- package/docs/content/locales/zh-TW/template-plan.mdx +1 -1
- package/docs/content/pr-visual-recap.mdx +50 -1
- package/docs/content/template-plan.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +2 -2
- package/src/templates/workspace-core/.agents/skills/secrets/SKILL.md +7 -3
- package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +39 -9
|
@@ -3,7 +3,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { requestAgentChatThreadOpen } from "../agent-chat.js";
|
|
5
5
|
import { SIDEBAR_STATE_CHANGE_EVENT, } from "../agent-sidebar-state.js";
|
|
6
|
-
import { agentNativePath
|
|
6
|
+
import { agentNativePath } from "../api-path.js";
|
|
7
7
|
import { readClientAppState, setClientAppState } from "../application-state.js";
|
|
8
8
|
import { getBrowserTabId } from "../browser-tab-id.js";
|
|
9
9
|
import { useT } from "../i18n.js";
|
|
@@ -11,10 +11,59 @@ import { createRealtimeVoiceAudioLevelStore, normalizeRealtimeVoiceRms, smoothRe
|
|
|
11
11
|
import { realtimeVoiceTranscriptRegistry } from "./realtime-voice-transcript.js";
|
|
12
12
|
import { RealtimeVoiceModeDock, } from "./RealtimeVoiceMode.js";
|
|
13
13
|
const REALTIME_VOICE_STATE_KEY = "realtime-voice-session";
|
|
14
|
+
const REALTIME_VOICE_PREFERENCES_KEY = "realtime-voice-prefs";
|
|
14
15
|
const REALTIME_VOICE_REQUEST_SOURCE = "realtime-voice";
|
|
15
16
|
const REALTIME_VOICE_SESSION_PATH = "/_agent-native/realtime-voice/session";
|
|
16
17
|
const REALTIME_VOICE_TOOL_PATH = "/_agent-native/realtime-voice/tool";
|
|
18
|
+
const REALTIME_VOICE_CAPABILITY_HEADER = "X-Agent-Native-Realtime-Capability";
|
|
17
19
|
const REALTIME_VOICE_CONNECTION_TIMEOUT_MS = 15_000;
|
|
20
|
+
const REALTIME_VOICE_MAX_TOOLS = 32;
|
|
21
|
+
const REALTIME_VOICE_MAX_TOOL_SCHEMA_BYTES = 32_000;
|
|
22
|
+
const REALTIME_VOICE_MAX_SESSION_BYTES = 64_000;
|
|
23
|
+
const REALTIME_VOICE_TOOL_UPDATE_TIMEOUT_MS = 5_000;
|
|
24
|
+
const REALTIME_VOICE_PRIORITY_TOOL_NAMES = [
|
|
25
|
+
"navigate",
|
|
26
|
+
"set-url-path",
|
|
27
|
+
"set-search-params",
|
|
28
|
+
"view-screen",
|
|
29
|
+
"tool-search",
|
|
30
|
+
];
|
|
31
|
+
const REALTIME_VOICE_TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
32
|
+
const REALTIME_VOICE_MICROPHONE_STORAGE_KEY = "agent-native:realtime-voice-microphone";
|
|
33
|
+
export const REALTIME_VOICE_LANGUAGES = [
|
|
34
|
+
"auto",
|
|
35
|
+
"en",
|
|
36
|
+
"es",
|
|
37
|
+
"fr",
|
|
38
|
+
"de",
|
|
39
|
+
"it",
|
|
40
|
+
"pt",
|
|
41
|
+
"ja",
|
|
42
|
+
"ko",
|
|
43
|
+
"zh",
|
|
44
|
+
];
|
|
45
|
+
export const REALTIME_VOICE_INTELLIGENCE_LEVELS = [
|
|
46
|
+
"instant",
|
|
47
|
+
"balanced",
|
|
48
|
+
"deep",
|
|
49
|
+
];
|
|
50
|
+
export const REALTIME_VOICES = [
|
|
51
|
+
"marin",
|
|
52
|
+
"cedar",
|
|
53
|
+
"coral",
|
|
54
|
+
"sage",
|
|
55
|
+
"verse",
|
|
56
|
+
"alloy",
|
|
57
|
+
"ash",
|
|
58
|
+
"ballad",
|
|
59
|
+
"echo",
|
|
60
|
+
"shimmer",
|
|
61
|
+
];
|
|
62
|
+
export const DEFAULT_REALTIME_VOICE_PREFERENCES = {
|
|
63
|
+
language: "auto",
|
|
64
|
+
intelligence: "instant",
|
|
65
|
+
voice: "marin",
|
|
66
|
+
};
|
|
18
67
|
export const REALTIME_VOICE_AUDIO_CONSTRAINTS = {
|
|
19
68
|
// Prefer the browser/OS-selected input instead of whichever physical device
|
|
20
69
|
// happens to be enumerated first. `ideal` keeps browsers without the
|
|
@@ -24,6 +73,271 @@ export const REALTIME_VOICE_AUDIO_CONSTRAINTS = {
|
|
|
24
73
|
noiseSuppression: true,
|
|
25
74
|
autoGainControl: true,
|
|
26
75
|
};
|
|
76
|
+
export function createRealtimeVoiceAudioConstraints(deviceId = "default", exact = false) {
|
|
77
|
+
return {
|
|
78
|
+
...REALTIME_VOICE_AUDIO_CONSTRAINTS,
|
|
79
|
+
deviceId: deviceId === "default"
|
|
80
|
+
? { ideal: "default" }
|
|
81
|
+
: exact
|
|
82
|
+
? { exact: deviceId }
|
|
83
|
+
: { ideal: deviceId },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export async function replaceRealtimeVoiceMicrophone(options) {
|
|
87
|
+
const replacementStream = await options.mediaDevices.getUserMedia({
|
|
88
|
+
audio: createRealtimeVoiceAudioConstraints(options.deviceId, true),
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
const replacementTrack = replacementStream.getAudioTracks()[0];
|
|
92
|
+
const sender = options.peer
|
|
93
|
+
.getSenders()
|
|
94
|
+
.find((candidate) => candidate.track?.kind === "audio");
|
|
95
|
+
if (!replacementTrack || !sender) {
|
|
96
|
+
throw new Error("No active microphone track is available.");
|
|
97
|
+
}
|
|
98
|
+
await sender.replaceTrack(replacementTrack);
|
|
99
|
+
for (const track of options.currentStream?.getTracks() ?? []) {
|
|
100
|
+
track.onended = null;
|
|
101
|
+
track.stop();
|
|
102
|
+
}
|
|
103
|
+
return replacementStream;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
for (const track of replacementStream.getTracks())
|
|
107
|
+
track.stop();
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function readRealtimeVoiceMicrophoneId() {
|
|
112
|
+
if (typeof window === "undefined")
|
|
113
|
+
return "default";
|
|
114
|
+
try {
|
|
115
|
+
return (window.localStorage.getItem(REALTIME_VOICE_MICROPHONE_STORAGE_KEY) ||
|
|
116
|
+
"default");
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return "default";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function writeRealtimeVoiceMicrophoneId(deviceId) {
|
|
123
|
+
try {
|
|
124
|
+
window.localStorage.setItem(REALTIME_VOICE_MICROPHONE_STORAGE_KEY, deviceId);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// A remembered local device is optional and must never block voice mode.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function isOneOf(values, value) {
|
|
131
|
+
return typeof value === "string" && values.includes(value);
|
|
132
|
+
}
|
|
133
|
+
export function normalizeRealtimeVoicePreferences(value) {
|
|
134
|
+
const record = value && typeof value === "object" && !Array.isArray(value)
|
|
135
|
+
? value
|
|
136
|
+
: {};
|
|
137
|
+
return {
|
|
138
|
+
language: isOneOf(REALTIME_VOICE_LANGUAGES, record.language)
|
|
139
|
+
? record.language
|
|
140
|
+
: DEFAULT_REALTIME_VOICE_PREFERENCES.language,
|
|
141
|
+
intelligence: isOneOf(REALTIME_VOICE_INTELLIGENCE_LEVELS, record.intelligence)
|
|
142
|
+
? record.intelligence
|
|
143
|
+
: DEFAULT_REALTIME_VOICE_PREFERENCES.intelligence,
|
|
144
|
+
voice: isOneOf(REALTIME_VOICES, record.voice)
|
|
145
|
+
? record.voice
|
|
146
|
+
: DEFAULT_REALTIME_VOICE_PREFERENCES.voice,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function resolveRealtimeVoiceLanguage(language, browserLanguages = []) {
|
|
150
|
+
if (language !== "auto")
|
|
151
|
+
return language;
|
|
152
|
+
for (const locale of browserLanguages) {
|
|
153
|
+
const primary = locale.trim().split("-")[0]?.toLowerCase();
|
|
154
|
+
if (isOneOf(REALTIME_VOICE_LANGUAGES, primary) && primary !== "auto") {
|
|
155
|
+
return primary;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return "en";
|
|
159
|
+
}
|
|
160
|
+
export function realtimeVoiceReasoningEffort(intelligence) {
|
|
161
|
+
if (intelligence === "balanced")
|
|
162
|
+
return "low";
|
|
163
|
+
if (intelligence === "deep")
|
|
164
|
+
return "medium";
|
|
165
|
+
return "minimal";
|
|
166
|
+
}
|
|
167
|
+
export function createRealtimeVoicePreferenceUpdate(preferences, options = {}) {
|
|
168
|
+
return {
|
|
169
|
+
type: "session.update",
|
|
170
|
+
session: {
|
|
171
|
+
type: "realtime",
|
|
172
|
+
reasoning: {
|
|
173
|
+
effort: realtimeVoiceReasoningEffort(preferences.intelligence),
|
|
174
|
+
},
|
|
175
|
+
audio: {
|
|
176
|
+
input: {
|
|
177
|
+
transcription: {
|
|
178
|
+
model: "gpt-4o-mini-transcribe",
|
|
179
|
+
language: resolveRealtimeVoiceLanguage(preferences.language, options.browserLanguages),
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
...(options.includeVoice
|
|
183
|
+
? { output: { voice: preferences.voice } }
|
|
184
|
+
: {}),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Input transcription is produced by a separate ASR model and can finish after
|
|
191
|
+
* the assistant response. Reserve each message's position when OpenAI adds it
|
|
192
|
+
* to the conversation, then publish only the contiguous completed prefix.
|
|
193
|
+
*/
|
|
194
|
+
export function createRealtimeVoiceTranscriptSequencer(publish) {
|
|
195
|
+
const order = [];
|
|
196
|
+
const items = new Map();
|
|
197
|
+
const settledItemIds = new Set();
|
|
198
|
+
const receivedTranscriptIds = new Set();
|
|
199
|
+
const reserve = (id, role) => {
|
|
200
|
+
if (settledItemIds.has(id))
|
|
201
|
+
return;
|
|
202
|
+
const existing = items.get(id);
|
|
203
|
+
if (existing) {
|
|
204
|
+
existing.role ??= role;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
order.push(id);
|
|
208
|
+
items.set(id, { status: "pending", role });
|
|
209
|
+
};
|
|
210
|
+
const drain = () => {
|
|
211
|
+
while (order.length > 0) {
|
|
212
|
+
const id = order[0];
|
|
213
|
+
const entry = items.get(id);
|
|
214
|
+
if (!entry || entry.status === "pending")
|
|
215
|
+
return;
|
|
216
|
+
order.shift();
|
|
217
|
+
items.delete(id);
|
|
218
|
+
settledItemIds.add(id);
|
|
219
|
+
if (entry.status === "completed" && entry.transcript) {
|
|
220
|
+
publish(entry.transcript);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const skip = (id) => {
|
|
225
|
+
if (typeof id !== "string" || !id)
|
|
226
|
+
return;
|
|
227
|
+
reserve(id);
|
|
228
|
+
const entry = items.get(id);
|
|
229
|
+
if (entry.status === "pending")
|
|
230
|
+
entry.status = "skipped";
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
handle(event) {
|
|
234
|
+
if (event.type === "conversation.item.added" ||
|
|
235
|
+
event.type === "conversation.item.created") {
|
|
236
|
+
const item = event.item;
|
|
237
|
+
if (item && typeof item === "object") {
|
|
238
|
+
const record = item;
|
|
239
|
+
if (record.type === "message" &&
|
|
240
|
+
(record.role === "user" || record.role === "assistant") &&
|
|
241
|
+
typeof record.id === "string") {
|
|
242
|
+
reserve(record.id, record.role);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const transcript = extractCompletedRealtimeVoiceTranscript(event);
|
|
247
|
+
if (transcript) {
|
|
248
|
+
const transcriptId = transcript.providerId
|
|
249
|
+
? `${transcript.role}:${transcript.providerId}`
|
|
250
|
+
: undefined;
|
|
251
|
+
if (transcriptId && receivedTranscriptIds.has(transcriptId))
|
|
252
|
+
return;
|
|
253
|
+
if (transcriptId)
|
|
254
|
+
receivedTranscriptIds.add(transcriptId);
|
|
255
|
+
const itemId = typeof event.item_id === "string" ? event.item_id : undefined;
|
|
256
|
+
if (!itemId) {
|
|
257
|
+
// Older providers may omit item_id. Match the first reserved slot for
|
|
258
|
+
// that role so one incomplete legacy event cannot strand the queue.
|
|
259
|
+
const reservedId = order.find((id) => {
|
|
260
|
+
const entry = items.get(id);
|
|
261
|
+
return (entry?.status === "pending" && entry.role === transcript.role);
|
|
262
|
+
});
|
|
263
|
+
if (reservedId) {
|
|
264
|
+
items.set(reservedId, {
|
|
265
|
+
status: "completed",
|
|
266
|
+
role: transcript.role,
|
|
267
|
+
transcript,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
publish(transcript);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
if (settledItemIds.has(itemId))
|
|
276
|
+
return;
|
|
277
|
+
reserve(itemId, transcript.role);
|
|
278
|
+
items.set(itemId, {
|
|
279
|
+
status: "completed",
|
|
280
|
+
role: transcript.role,
|
|
281
|
+
transcript,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else if (event.type === "conversation.item.input_audio_transcription.failed" ||
|
|
286
|
+
event.type === "conversation.item.deleted") {
|
|
287
|
+
skip(event.item_id);
|
|
288
|
+
}
|
|
289
|
+
else if (event.type === "response.done") {
|
|
290
|
+
const response = event.response;
|
|
291
|
+
const output = response && typeof response === "object"
|
|
292
|
+
? response.output
|
|
293
|
+
: undefined;
|
|
294
|
+
if (Array.isArray(output)) {
|
|
295
|
+
for (const item of output) {
|
|
296
|
+
if (!item || typeof item !== "object")
|
|
297
|
+
continue;
|
|
298
|
+
const record = item;
|
|
299
|
+
if (record.type === "message")
|
|
300
|
+
skip(record.id);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
drain();
|
|
305
|
+
},
|
|
306
|
+
reset() {
|
|
307
|
+
order.length = 0;
|
|
308
|
+
items.clear();
|
|
309
|
+
settledItemIds.clear();
|
|
310
|
+
receivedTranscriptIds.clear();
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
export function createRealtimeVoiceGreetingEvent() {
|
|
315
|
+
return {
|
|
316
|
+
type: "response.create",
|
|
317
|
+
response: {
|
|
318
|
+
output_modalities: ["audio"],
|
|
319
|
+
instructions: 'Say exactly: "How can I help you?" Do not add anything else.',
|
|
320
|
+
// Do not set max_output_tokens here. Realtime counts assistant audio at
|
|
321
|
+
// roughly one token per 50 ms, so a text-sized cap can cut speech off.
|
|
322
|
+
// The exact-response instruction already keeps this greeting bounded.
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
export function createRealtimeVoiceGreetingStarter(send) {
|
|
327
|
+
let started = false;
|
|
328
|
+
return {
|
|
329
|
+
start() {
|
|
330
|
+
if (started)
|
|
331
|
+
return false;
|
|
332
|
+
started = true;
|
|
333
|
+
send(createRealtimeVoiceGreetingEvent());
|
|
334
|
+
return true;
|
|
335
|
+
},
|
|
336
|
+
reset() {
|
|
337
|
+
started = false;
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
27
341
|
/**
|
|
28
342
|
* Voice mode owns the chat only temporarily. Restore the captured transcript
|
|
29
343
|
* when it is still the user's active thread (or the chat has no active thread)
|
|
@@ -56,10 +370,25 @@ export function isRealtimeVoiceAbortError(error) {
|
|
|
56
370
|
typeof error === "object" &&
|
|
57
371
|
error.name === "AbortError");
|
|
58
372
|
}
|
|
373
|
+
export function isRealtimeVoiceSetupRequiredError(error) {
|
|
374
|
+
return Boolean(error &&
|
|
375
|
+
typeof error === "object" &&
|
|
376
|
+
error.status === 409);
|
|
377
|
+
}
|
|
59
378
|
export function createRealtimeVoiceConnectionTimeout(onTimeout, timeoutMs = REALTIME_VOICE_CONNECTION_TIMEOUT_MS) {
|
|
60
379
|
const timeout = window.setTimeout(onTimeout, timeoutMs);
|
|
61
380
|
return () => window.clearTimeout(timeout);
|
|
62
381
|
}
|
|
382
|
+
export function createRealtimeVoiceConnectionGate(onTimeout, timeoutMs = REALTIME_VOICE_CONNECTION_TIMEOUT_MS) {
|
|
383
|
+
const cancel = createRealtimeVoiceConnectionTimeout(onTimeout, timeoutMs);
|
|
384
|
+
return {
|
|
385
|
+
// RTC connectivity alone is not enough: the session may never finish its
|
|
386
|
+
// Realtime handshake. Keep the deadline armed until session.created.
|
|
387
|
+
markTransportReady() { },
|
|
388
|
+
markSessionCreated: cancel,
|
|
389
|
+
cancel,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
63
392
|
async function readErrorResponse(response) {
|
|
64
393
|
const raw = await response.text().catch(() => "");
|
|
65
394
|
if (!raw)
|
|
@@ -72,7 +401,8 @@ async function readErrorResponse(response) {
|
|
|
72
401
|
return raw.slice(0, 500);
|
|
73
402
|
}
|
|
74
403
|
}
|
|
75
|
-
export async function
|
|
404
|
+
export async function createRealtimeVoiceSessionWithCapability(offerSdp, options = {}) {
|
|
405
|
+
const preferences = options.preferences;
|
|
76
406
|
const response = await fetch(agentNativePath(REALTIME_VOICE_SESSION_PATH), {
|
|
77
407
|
method: "POST",
|
|
78
408
|
credentials: "same-origin",
|
|
@@ -81,6 +411,13 @@ export async function createRealtimeVoiceSession(offerSdp, options = {}) {
|
|
|
81
411
|
...(options.browserTabId
|
|
82
412
|
? { "X-Agent-Native-Browser-Tab": options.browserTabId }
|
|
83
413
|
: {}),
|
|
414
|
+
...(preferences
|
|
415
|
+
? {
|
|
416
|
+
"X-Agent-Native-Realtime-Language": resolveRealtimeVoiceLanguage(preferences.language, options.browserLanguages),
|
|
417
|
+
"X-Agent-Native-Realtime-Intelligence": preferences.intelligence,
|
|
418
|
+
"X-Agent-Native-Realtime-Voice": preferences.voice,
|
|
419
|
+
}
|
|
420
|
+
: {}),
|
|
84
421
|
},
|
|
85
422
|
body: offerSdp,
|
|
86
423
|
signal: options.signal,
|
|
@@ -91,7 +428,17 @@ export async function createRealtimeVoiceSession(offerSdp, options = {}) {
|
|
|
91
428
|
error.status = response.status;
|
|
92
429
|
throw error;
|
|
93
430
|
}
|
|
94
|
-
|
|
431
|
+
const sdp = await response.text();
|
|
432
|
+
const capability = response.headers.get(REALTIME_VOICE_CAPABILITY_HEADER);
|
|
433
|
+
return {
|
|
434
|
+
sdp,
|
|
435
|
+
...(capability ? { capability } : {}),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** Backwards-compatible SDP-only session helper for existing client callers. */
|
|
439
|
+
export async function createRealtimeVoiceSession(offerSdp, options = {}) {
|
|
440
|
+
return (await createRealtimeVoiceSessionWithCapability(offerSdp, options))
|
|
441
|
+
.sdp;
|
|
95
442
|
}
|
|
96
443
|
export async function executeRealtimeVoiceTool(input) {
|
|
97
444
|
const response = await fetch(agentNativePath(REALTIME_VOICE_TOOL_PATH), {
|
|
@@ -102,6 +449,9 @@ export async function executeRealtimeVoiceTool(input) {
|
|
|
102
449
|
...(input.browserTabId
|
|
103
450
|
? { "X-Agent-Native-Browser-Tab": input.browserTabId }
|
|
104
451
|
: {}),
|
|
452
|
+
...(input.capability
|
|
453
|
+
? { [REALTIME_VOICE_CAPABILITY_HEADER]: input.capability }
|
|
454
|
+
: {}),
|
|
105
455
|
},
|
|
106
456
|
body: JSON.stringify({
|
|
107
457
|
name: input.name,
|
|
@@ -117,6 +467,217 @@ export async function executeRealtimeVoiceTool(input) {
|
|
|
117
467
|
}
|
|
118
468
|
return (await response.json());
|
|
119
469
|
}
|
|
470
|
+
function jsonByteLength(value) {
|
|
471
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
472
|
+
}
|
|
473
|
+
function normalizeRealtimeVoiceFunctionTool(value) {
|
|
474
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
475
|
+
return null;
|
|
476
|
+
const record = value;
|
|
477
|
+
if (record.type !== "function" ||
|
|
478
|
+
typeof record.name !== "string" ||
|
|
479
|
+
!REALTIME_VOICE_TOOL_NAME.test(record.name)) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
if (record.description !== undefined &&
|
|
483
|
+
typeof record.description !== "string") {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
if (record.parameters !== undefined &&
|
|
487
|
+
(!record.parameters ||
|
|
488
|
+
typeof record.parameters !== "object" ||
|
|
489
|
+
Array.isArray(record.parameters))) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
if (jsonByteLength(record) > REALTIME_VOICE_MAX_TOOL_SCHEMA_BYTES) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
return JSON.parse(JSON.stringify(record));
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
export function extractRealtimeVoiceSessionTools(event) {
|
|
503
|
+
if (event.type !== "session.created" && event.type !== "session.updated") {
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
const session = event.session;
|
|
507
|
+
if (!session || typeof session !== "object")
|
|
508
|
+
return null;
|
|
509
|
+
const tools = session.tools;
|
|
510
|
+
if (!Array.isArray(tools))
|
|
511
|
+
return null;
|
|
512
|
+
return tools
|
|
513
|
+
.map(normalizeRealtimeVoiceFunctionTool)
|
|
514
|
+
.filter((tool) => Boolean(tool));
|
|
515
|
+
}
|
|
516
|
+
function createRealtimeVoiceToolManifestUpdate(tools, eventId) {
|
|
517
|
+
return {
|
|
518
|
+
type: "session.update",
|
|
519
|
+
...(eventId ? { event_id: eventId } : {}),
|
|
520
|
+
session: {
|
|
521
|
+
type: "realtime",
|
|
522
|
+
tools,
|
|
523
|
+
tool_choice: "auto",
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Merge search-discovered schemas into the live manifest. Pinned navigation
|
|
529
|
+
* and discovery tools stay available, then the newest discoveries take the
|
|
530
|
+
* remaining slots ahead of lower-priority tools from the original manifest.
|
|
531
|
+
*/
|
|
532
|
+
export function mergeRealtimeVoiceToolManifest(currentTools, expandedTools) {
|
|
533
|
+
const current = new Map();
|
|
534
|
+
for (const value of currentTools) {
|
|
535
|
+
const tool = normalizeRealtimeVoiceFunctionTool(value);
|
|
536
|
+
if (tool)
|
|
537
|
+
current.set(tool.name, tool);
|
|
538
|
+
}
|
|
539
|
+
const expanded = new Map();
|
|
540
|
+
for (const value of expandedTools) {
|
|
541
|
+
const tool = normalizeRealtimeVoiceFunctionTool(value);
|
|
542
|
+
if (tool)
|
|
543
|
+
expanded.set(tool.name, tool);
|
|
544
|
+
}
|
|
545
|
+
const candidates = [];
|
|
546
|
+
const seen = new Set();
|
|
547
|
+
const add = (tool) => {
|
|
548
|
+
if (!tool || seen.has(tool.name))
|
|
549
|
+
return;
|
|
550
|
+
seen.add(tool.name);
|
|
551
|
+
candidates.push(tool);
|
|
552
|
+
};
|
|
553
|
+
for (const name of REALTIME_VOICE_PRIORITY_TOOL_NAMES) {
|
|
554
|
+
add(expanded.get(name) ?? current.get(name));
|
|
555
|
+
}
|
|
556
|
+
for (const tool of expanded.values())
|
|
557
|
+
add(tool);
|
|
558
|
+
for (const tool of current.values())
|
|
559
|
+
add(tool);
|
|
560
|
+
const packed = [];
|
|
561
|
+
for (const tool of candidates) {
|
|
562
|
+
if (packed.length >= REALTIME_VOICE_MAX_TOOLS)
|
|
563
|
+
break;
|
|
564
|
+
const candidate = [...packed, tool];
|
|
565
|
+
if (jsonByteLength(createRealtimeVoiceToolManifestUpdate(candidate, "realtime_tool_manifest_999999999")) <= REALTIME_VOICE_MAX_SESSION_BYTES) {
|
|
566
|
+
packed.push(tool);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return packed;
|
|
570
|
+
}
|
|
571
|
+
function createRealtimeVoiceToolResultEvent(result) {
|
|
572
|
+
const modelResult = {
|
|
573
|
+
callId: result.callId,
|
|
574
|
+
status: result.status,
|
|
575
|
+
output: result.output,
|
|
576
|
+
...(result.approvalKey ? { approvalKey: result.approvalKey } : {}),
|
|
577
|
+
};
|
|
578
|
+
return {
|
|
579
|
+
type: "conversation.item.create",
|
|
580
|
+
item: {
|
|
581
|
+
type: "function_call_output",
|
|
582
|
+
call_id: result.callId,
|
|
583
|
+
output: JSON.stringify(modelResult),
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Realtime treats `session.update.tools` as a full replacement. Serialize
|
|
589
|
+
* discovery updates and wait for a confirming `session.updated` manifest
|
|
590
|
+
* before returning the search result and allowing the next model response.
|
|
591
|
+
*/
|
|
592
|
+
export function createRealtimeVoiceToolManifestCoordinator(send, timeoutMs = REALTIME_VOICE_TOOL_UPDATE_TIMEOUT_MS) {
|
|
593
|
+
let currentTools = [];
|
|
594
|
+
const queue = [];
|
|
595
|
+
let updateSequence = 0;
|
|
596
|
+
let active;
|
|
597
|
+
const finish = (failureMessage) => {
|
|
598
|
+
if (!active)
|
|
599
|
+
return;
|
|
600
|
+
const { result, timer } = active;
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
active = undefined;
|
|
603
|
+
send(createRealtimeVoiceToolResultEvent(failureMessage
|
|
604
|
+
? {
|
|
605
|
+
callId: result.callId,
|
|
606
|
+
status: "failed",
|
|
607
|
+
output: `${failureMessage} The discovered tools were not added to this voice session. Original tool result: ${result.output}`,
|
|
608
|
+
}
|
|
609
|
+
: result));
|
|
610
|
+
send({ type: "response.create" });
|
|
611
|
+
drain();
|
|
612
|
+
};
|
|
613
|
+
const drain = () => {
|
|
614
|
+
if (active)
|
|
615
|
+
return;
|
|
616
|
+
const result = queue.shift();
|
|
617
|
+
if (!result)
|
|
618
|
+
return;
|
|
619
|
+
const expanded = Array.isArray(result.expandedTools)
|
|
620
|
+
? result.expandedTools
|
|
621
|
+
.map(normalizeRealtimeVoiceFunctionTool)
|
|
622
|
+
.filter((tool) => Boolean(tool))
|
|
623
|
+
: [];
|
|
624
|
+
if (result.status !== "completed" || expanded.length === 0) {
|
|
625
|
+
send(createRealtimeVoiceToolResultEvent(result));
|
|
626
|
+
send({ type: "response.create" });
|
|
627
|
+
drain();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const merged = mergeRealtimeVoiceToolManifest(currentTools, expanded);
|
|
631
|
+
const mergedNames = new Set(merged.map((tool) => tool.name));
|
|
632
|
+
const expectedNames = expanded
|
|
633
|
+
.map((tool) => tool.name)
|
|
634
|
+
.filter((name) => mergedNames.has(name));
|
|
635
|
+
if (expectedNames.length === 0) {
|
|
636
|
+
send(createRealtimeVoiceToolResultEvent(result));
|
|
637
|
+
send({ type: "response.create" });
|
|
638
|
+
drain();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const eventId = `realtime_tool_manifest_${++updateSequence}`;
|
|
642
|
+
active = {
|
|
643
|
+
result,
|
|
644
|
+
expectedNames,
|
|
645
|
+
eventId,
|
|
646
|
+
timer: setTimeout(() => finish("Timed out while installing the discovered tools."), timeoutMs),
|
|
647
|
+
};
|
|
648
|
+
send(createRealtimeVoiceToolManifestUpdate(merged, eventId));
|
|
649
|
+
};
|
|
650
|
+
return {
|
|
651
|
+
enqueue(result) {
|
|
652
|
+
queue.push(result);
|
|
653
|
+
drain();
|
|
654
|
+
},
|
|
655
|
+
setSessionTools(tools) {
|
|
656
|
+
currentTools = mergeRealtimeVoiceToolManifest([], tools);
|
|
657
|
+
if (!active)
|
|
658
|
+
return;
|
|
659
|
+
const names = new Set(currentTools.map((tool) => tool.name));
|
|
660
|
+
if (active.expectedNames.every((name) => names.has(name)))
|
|
661
|
+
finish();
|
|
662
|
+
},
|
|
663
|
+
handleError(eventId, message) {
|
|
664
|
+
if (!active || !eventId || active.eventId !== eventId)
|
|
665
|
+
return false;
|
|
666
|
+
finish(message || "The provider rejected the discovered tool update.");
|
|
667
|
+
return true;
|
|
668
|
+
},
|
|
669
|
+
reset() {
|
|
670
|
+
if (active)
|
|
671
|
+
clearTimeout(active.timer);
|
|
672
|
+
active = undefined;
|
|
673
|
+
queue.length = 0;
|
|
674
|
+
currentTools = [];
|
|
675
|
+
},
|
|
676
|
+
getTools() {
|
|
677
|
+
return currentTools;
|
|
678
|
+
},
|
|
679
|
+
};
|
|
680
|
+
}
|
|
120
681
|
export function extractRealtimeVoiceFunctionCalls(event) {
|
|
121
682
|
if (event.type === "response.function_call_arguments.done") {
|
|
122
683
|
const name = typeof event.name === "string" ? event.name : "";
|
|
@@ -179,15 +740,6 @@ function openOpenAiKeySettings() {
|
|
|
179
740
|
detail: { section: "secrets" },
|
|
180
741
|
}));
|
|
181
742
|
}
|
|
182
|
-
function openMicrophoneSettings() {
|
|
183
|
-
if (typeof window === "undefined")
|
|
184
|
-
return;
|
|
185
|
-
window.location.assign(`${appPath("/settings")}#voice`);
|
|
186
|
-
}
|
|
187
|
-
export function endRealtimeVoiceBeforeNavigation(end, navigate) {
|
|
188
|
-
end();
|
|
189
|
-
window.setTimeout(navigate, 0);
|
|
190
|
-
}
|
|
191
743
|
export function listenForRealtimeVoicePageHide(cleanup) {
|
|
192
744
|
window.addEventListener("pagehide", cleanup);
|
|
193
745
|
return () => window.removeEventListener("pagehide", cleanup);
|
|
@@ -207,7 +759,45 @@ function voiceCopy(t) {
|
|
|
207
759
|
showChat: t("agentPanel.voiceMode.showChat"),
|
|
208
760
|
hideChat: t("agentPanel.voiceMode.hideChat"),
|
|
209
761
|
endVoiceMode: t("agentPanel.voiceMode.end"),
|
|
210
|
-
|
|
762
|
+
voiceSettings: t("agentPanel.voiceMode.voiceSettings"),
|
|
763
|
+
settings: {
|
|
764
|
+
language: t("agentPanel.voiceMode.settings.language"),
|
|
765
|
+
autoLanguage: t("agentPanel.voiceMode.settings.autoLanguage"),
|
|
766
|
+
languages: {
|
|
767
|
+
en: t("agentPanel.voiceMode.settings.languages.en"),
|
|
768
|
+
es: t("agentPanel.voiceMode.settings.languages.es"),
|
|
769
|
+
fr: t("agentPanel.voiceMode.settings.languages.fr"),
|
|
770
|
+
de: t("agentPanel.voiceMode.settings.languages.de"),
|
|
771
|
+
it: t("agentPanel.voiceMode.settings.languages.it"),
|
|
772
|
+
pt: t("agentPanel.voiceMode.settings.languages.pt"),
|
|
773
|
+
ja: t("agentPanel.voiceMode.settings.languages.ja"),
|
|
774
|
+
ko: t("agentPanel.voiceMode.settings.languages.ko"),
|
|
775
|
+
zh: t("agentPanel.voiceMode.settings.languages.zh"),
|
|
776
|
+
},
|
|
777
|
+
intelligence: t("agentPanel.voiceMode.settings.intelligence"),
|
|
778
|
+
intelligenceLevels: {
|
|
779
|
+
instant: t("agentPanel.voiceMode.settings.intelligenceLevels.instant"),
|
|
780
|
+
balanced: t("agentPanel.voiceMode.settings.intelligenceLevels.balanced"),
|
|
781
|
+
deep: t("agentPanel.voiceMode.settings.intelligenceLevels.deep"),
|
|
782
|
+
},
|
|
783
|
+
voiceStyle: t("agentPanel.voiceMode.settings.voiceStyle"),
|
|
784
|
+
microphone: t("agentPanel.voiceMode.settings.microphone"),
|
|
785
|
+
defaultMicrophone: t("agentPanel.voiceMode.settings.defaultMicrophone"),
|
|
786
|
+
microphoneSwitchFailed: t("agentPanel.voiceMode.settings.microphoneSwitchFailed"),
|
|
787
|
+
voiceChangePending: t("agentPanel.voiceMode.settings.voiceChangePending"),
|
|
788
|
+
voiceDescriptions: {
|
|
789
|
+
marin: t("agentPanel.voiceMode.settings.voiceDescriptions.marin"),
|
|
790
|
+
cedar: t("agentPanel.voiceMode.settings.voiceDescriptions.cedar"),
|
|
791
|
+
coral: t("agentPanel.voiceMode.settings.voiceDescriptions.coral"),
|
|
792
|
+
sage: t("agentPanel.voiceMode.settings.voiceDescriptions.sage"),
|
|
793
|
+
verse: t("agentPanel.voiceMode.settings.voiceDescriptions.verse"),
|
|
794
|
+
alloy: t("agentPanel.voiceMode.settings.voiceDescriptions.alloy"),
|
|
795
|
+
ash: t("agentPanel.voiceMode.settings.voiceDescriptions.ash"),
|
|
796
|
+
ballad: t("agentPanel.voiceMode.settings.voiceDescriptions.ballad"),
|
|
797
|
+
echo: t("agentPanel.voiceMode.settings.voiceDescriptions.echo"),
|
|
798
|
+
shimmer: t("agentPanel.voiceMode.settings.voiceDescriptions.shimmer"),
|
|
799
|
+
},
|
|
800
|
+
},
|
|
211
801
|
status: {
|
|
212
802
|
connecting: t("agentPanel.voiceMode.status.connecting"),
|
|
213
803
|
listening: t("agentPanel.voiceMode.status.listening"),
|
|
@@ -221,6 +811,7 @@ function voiceCopy(t) {
|
|
|
221
811
|
responseFailed: t("agentPanel.voiceMode.errors.responseFailed"),
|
|
222
812
|
sessionFailed: t("agentPanel.voiceMode.errors.sessionFailed"),
|
|
223
813
|
channelDisconnected: t("agentPanel.voiceMode.errors.channelDisconnected"),
|
|
814
|
+
connectionTimedOut: t("agentPanel.voiceMode.errors.connectionTimedOut"),
|
|
224
815
|
connectionFailed: t("agentPanel.voiceMode.errors.connectionFailed"),
|
|
225
816
|
offerFailed: t("agentPanel.voiceMode.errors.offerFailed"),
|
|
226
817
|
},
|
|
@@ -234,8 +825,16 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
234
825
|
const [state, setState] = useState("idle");
|
|
235
826
|
const [error, setError] = useState(null);
|
|
236
827
|
const [chatVisible, setChatVisible] = useState(false);
|
|
828
|
+
const [preferences, setPreferences] = useState(DEFAULT_REALTIME_VOICE_PREFERENCES);
|
|
829
|
+
const [voiceChangePending, setVoiceChangePending] = useState(false);
|
|
830
|
+
const [microphones, setMicrophones] = useState([]);
|
|
831
|
+
const [microphoneDeviceId, setMicrophoneDeviceId] = useState(readRealtimeVoiceMicrophoneId);
|
|
832
|
+
const [microphoneSwitching, setMicrophoneSwitching] = useState(false);
|
|
833
|
+
const [microphoneError, setMicrophoneError] = useState(null);
|
|
237
834
|
const [audioLevels] = useState(createRealtimeVoiceAudioLevelStore);
|
|
238
835
|
const stateRef = useRef(state);
|
|
836
|
+
const preferencesRef = useRef(preferences);
|
|
837
|
+
const hasOutputAudioRef = useRef(false);
|
|
239
838
|
const peerRef = useRef(null);
|
|
240
839
|
const channelRef = useRef(null);
|
|
241
840
|
const streamRef = useRef(null);
|
|
@@ -250,17 +849,47 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
250
849
|
const meterFrameRef = useRef(null);
|
|
251
850
|
const lastMeterSampleRef = useRef(0);
|
|
252
851
|
const abortRef = useRef(null);
|
|
253
|
-
const
|
|
852
|
+
const connectionGateRef = useRef(null);
|
|
254
853
|
const handledCallsRef = useRef(new Set());
|
|
255
854
|
const sessionIdRef = useRef(undefined);
|
|
855
|
+
const capabilityRef = useRef(undefined);
|
|
856
|
+
const transportGenerationRef = useRef(0);
|
|
256
857
|
const startedAtRef = useRef(undefined);
|
|
257
858
|
const lastUserTextRef = useRef("");
|
|
258
859
|
const lastAssistantTextRef = useRef("");
|
|
259
860
|
const transcriptThreadIdRef = useRef(undefined);
|
|
260
861
|
const transcriptSequenceRef = useRef(0);
|
|
862
|
+
const preferencesHydratedRef = useRef(false);
|
|
863
|
+
const preferencesEditedRef = useRef(false);
|
|
864
|
+
const preferenceWriteChainRef = useRef(Promise.resolve());
|
|
865
|
+
const toolManifestCoordinator = useMemo(() => createRealtimeVoiceToolManifestCoordinator((event) => {
|
|
866
|
+
sendDataChannelEvent(channelRef.current, event);
|
|
867
|
+
}), []);
|
|
868
|
+
const switchMicrophoneRef = useRef(async () => undefined);
|
|
261
869
|
useEffect(() => {
|
|
262
870
|
stateRef.current = state;
|
|
263
871
|
}, [state]);
|
|
872
|
+
useEffect(() => {
|
|
873
|
+
preferencesRef.current = preferences;
|
|
874
|
+
}, [preferences]);
|
|
875
|
+
const hydratePreferences = useCallback(async () => {
|
|
876
|
+
if (preferencesHydratedRef.current)
|
|
877
|
+
return;
|
|
878
|
+
try {
|
|
879
|
+
const stored = await readClientAppState(REALTIME_VOICE_PREFERENCES_KEY);
|
|
880
|
+
if (!preferencesEditedRef.current && stored != null) {
|
|
881
|
+
const next = normalizeRealtimeVoicePreferences(stored);
|
|
882
|
+
preferencesRef.current = next;
|
|
883
|
+
setPreferences(next);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
catch {
|
|
887
|
+
// Preferences are optional; keep safe defaults when storage is unavailable.
|
|
888
|
+
}
|
|
889
|
+
finally {
|
|
890
|
+
preferencesHydratedRef.current = true;
|
|
891
|
+
}
|
|
892
|
+
}, []);
|
|
264
893
|
const syncAppState = useCallback((nextState) => {
|
|
265
894
|
const value = nextState === "idle"
|
|
266
895
|
? null
|
|
@@ -271,6 +900,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
271
900
|
startedAt: startedAtRef.current,
|
|
272
901
|
sessionId: sessionIdRef.current,
|
|
273
902
|
browserTabId,
|
|
903
|
+
preferences: preferencesRef.current,
|
|
274
904
|
lastUserText: lastUserTextRef.current || undefined,
|
|
275
905
|
lastAssistantText: lastAssistantTextRef.current || undefined,
|
|
276
906
|
};
|
|
@@ -283,6 +913,70 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
283
913
|
setState(nextState);
|
|
284
914
|
syncAppState(nextState);
|
|
285
915
|
}, [syncAppState]);
|
|
916
|
+
const savePreferences = useCallback((next) => {
|
|
917
|
+
preferencesEditedRef.current = true;
|
|
918
|
+
preferencesHydratedRef.current = true;
|
|
919
|
+
preferencesRef.current = next;
|
|
920
|
+
setPreferences(next);
|
|
921
|
+
preferenceWriteChainRef.current = preferenceWriteChainRef.current
|
|
922
|
+
.catch(() => undefined)
|
|
923
|
+
.then(async () => {
|
|
924
|
+
await setClientAppState(REALTIME_VOICE_PREFERENCES_KEY, next, {
|
|
925
|
+
requestSource: REALTIME_VOICE_REQUEST_SOURCE,
|
|
926
|
+
});
|
|
927
|
+
})
|
|
928
|
+
.catch(() => undefined);
|
|
929
|
+
}, []);
|
|
930
|
+
const updateLivePreferences = useCallback((next, includeVoice = false) => {
|
|
931
|
+
sendDataChannelEvent(channelRef.current, createRealtimeVoicePreferenceUpdate(next, {
|
|
932
|
+
browserLanguages: typeof navigator === "undefined" ? [] : navigator.languages,
|
|
933
|
+
includeVoice,
|
|
934
|
+
}));
|
|
935
|
+
}, []);
|
|
936
|
+
const setLanguage = useCallback((language) => {
|
|
937
|
+
const next = { ...preferencesRef.current, language };
|
|
938
|
+
savePreferences(next);
|
|
939
|
+
updateLivePreferences(next);
|
|
940
|
+
}, [savePreferences, updateLivePreferences]);
|
|
941
|
+
const setIntelligence = useCallback((intelligence) => {
|
|
942
|
+
const next = { ...preferencesRef.current, intelligence };
|
|
943
|
+
savePreferences(next);
|
|
944
|
+
updateLivePreferences(next);
|
|
945
|
+
}, [savePreferences, updateLivePreferences]);
|
|
946
|
+
const setVoice = useCallback((voice) => {
|
|
947
|
+
const next = { ...preferencesRef.current, voice };
|
|
948
|
+
savePreferences(next);
|
|
949
|
+
if (hasOutputAudioRef.current) {
|
|
950
|
+
setVoiceChangePending(true);
|
|
951
|
+
updateLivePreferences(next);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
setVoiceChangePending(false);
|
|
955
|
+
updateLivePreferences(next, true);
|
|
956
|
+
}, [savePreferences, updateLivePreferences]);
|
|
957
|
+
const refreshMicrophones = useCallback(async () => {
|
|
958
|
+
const mediaDevices = navigator.mediaDevices;
|
|
959
|
+
if (!mediaDevices?.enumerateDevices)
|
|
960
|
+
return null;
|
|
961
|
+
try {
|
|
962
|
+
const devices = await mediaDevices.enumerateDevices();
|
|
963
|
+
const inputs = devices
|
|
964
|
+
.filter((device) => device.kind === "audioinput" &&
|
|
965
|
+
device.deviceId &&
|
|
966
|
+
device.deviceId !== "default" &&
|
|
967
|
+
device.deviceId !== "communications")
|
|
968
|
+
.map((device, index) => ({
|
|
969
|
+
deviceId: device.deviceId,
|
|
970
|
+
label: device.label || `Microphone ${index + 1}`,
|
|
971
|
+
}));
|
|
972
|
+
setMicrophones(inputs);
|
|
973
|
+
return inputs;
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
// Enumeration is progressive enhancement; system default remains usable.
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
}, []);
|
|
286
980
|
const startMeterLoop = useCallback(() => {
|
|
287
981
|
if (meterFrameRef.current !== null)
|
|
288
982
|
return;
|
|
@@ -342,17 +1036,65 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
342
1036
|
// Audio metering is visual-only; keep the realtime call healthy.
|
|
343
1037
|
}
|
|
344
1038
|
}, [startMeterLoop]);
|
|
1039
|
+
const setMicrophone = useCallback(async (deviceId, force = false) => {
|
|
1040
|
+
if ((!force && deviceId === microphoneDeviceId) || microphoneSwitching) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const mediaDevices = navigator.mediaDevices;
|
|
1044
|
+
const peer = peerRef.current;
|
|
1045
|
+
if (!mediaDevices?.getUserMedia || !peer)
|
|
1046
|
+
return;
|
|
1047
|
+
setMicrophoneSwitching(true);
|
|
1048
|
+
setMicrophoneError(null);
|
|
1049
|
+
try {
|
|
1050
|
+
const replacementStream = await replaceRealtimeVoiceMicrophone({
|
|
1051
|
+
mediaDevices,
|
|
1052
|
+
peer,
|
|
1053
|
+
currentStream: streamRef.current,
|
|
1054
|
+
deviceId,
|
|
1055
|
+
});
|
|
1056
|
+
const replacementTrack = replacementStream.getAudioTracks()[0];
|
|
1057
|
+
streamRef.current = replacementStream;
|
|
1058
|
+
if (replacementTrack) {
|
|
1059
|
+
replacementTrack.onended = () => {
|
|
1060
|
+
void switchMicrophoneRef.current("default", true);
|
|
1061
|
+
};
|
|
1062
|
+
}
|
|
1063
|
+
attachAudioMeter(streamRef.current, "input");
|
|
1064
|
+
writeRealtimeVoiceMicrophoneId(deviceId);
|
|
1065
|
+
setMicrophoneDeviceId(deviceId);
|
|
1066
|
+
await refreshMicrophones();
|
|
1067
|
+
}
|
|
1068
|
+
catch {
|
|
1069
|
+
setMicrophoneError(copy?.settings.microphoneSwitchFailed ??
|
|
1070
|
+
"Could not switch microphones. Your current microphone is still active.");
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
setMicrophoneSwitching(false);
|
|
1074
|
+
}
|
|
1075
|
+
}, [
|
|
1076
|
+
attachAudioMeter,
|
|
1077
|
+
copy,
|
|
1078
|
+
microphoneDeviceId,
|
|
1079
|
+
microphoneSwitching,
|
|
1080
|
+
refreshMicrophones,
|
|
1081
|
+
]);
|
|
1082
|
+
switchMicrophoneRef.current = setMicrophone;
|
|
345
1083
|
const cleanupTransport = useCallback(() => {
|
|
346
|
-
|
|
347
|
-
|
|
1084
|
+
connectionGateRef.current?.cancel();
|
|
1085
|
+
connectionGateRef.current = null;
|
|
1086
|
+
transportGenerationRef.current += 1;
|
|
1087
|
+
capabilityRef.current = undefined;
|
|
348
1088
|
abortRef.current?.abort();
|
|
349
1089
|
abortRef.current = null;
|
|
350
1090
|
channelRef.current?.close();
|
|
351
1091
|
channelRef.current = null;
|
|
352
1092
|
peerRef.current?.close();
|
|
353
1093
|
peerRef.current = null;
|
|
354
|
-
for (const track of streamRef.current?.getTracks() ?? [])
|
|
1094
|
+
for (const track of streamRef.current?.getTracks() ?? []) {
|
|
1095
|
+
track.onended = null;
|
|
355
1096
|
track.stop();
|
|
1097
|
+
}
|
|
356
1098
|
streamRef.current = null;
|
|
357
1099
|
if (audioRef.current) {
|
|
358
1100
|
audioRef.current.pause();
|
|
@@ -378,7 +1120,8 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
378
1120
|
void audioContext.close().catch(() => undefined);
|
|
379
1121
|
audioLevels.reset();
|
|
380
1122
|
handledCallsRef.current.clear();
|
|
381
|
-
|
|
1123
|
+
toolManifestCoordinator.reset();
|
|
1124
|
+
}, [audioLevels, toolManifestCoordinator]);
|
|
382
1125
|
const fail = useCallback((message, options) => {
|
|
383
1126
|
cleanupTransport();
|
|
384
1127
|
setError(message);
|
|
@@ -390,6 +1133,8 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
390
1133
|
if (handledCallsRef.current.has(call.callId))
|
|
391
1134
|
return;
|
|
392
1135
|
handledCallsRef.current.add(call.callId);
|
|
1136
|
+
const transportGeneration = transportGenerationRef.current;
|
|
1137
|
+
const transportChannel = channelRef.current;
|
|
393
1138
|
transition("working");
|
|
394
1139
|
let result;
|
|
395
1140
|
try {
|
|
@@ -400,6 +1145,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
400
1145
|
callId: call.callId,
|
|
401
1146
|
sessionId: sessionIdRef.current,
|
|
402
1147
|
browserTabId,
|
|
1148
|
+
capability: capabilityRef.current,
|
|
403
1149
|
signal: abortRef.current?.signal,
|
|
404
1150
|
});
|
|
405
1151
|
}
|
|
@@ -410,20 +1156,15 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
410
1156
|
output: errorMessage(toolError),
|
|
411
1157
|
};
|
|
412
1158
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
});
|
|
421
|
-
sendDataChannelEvent(channelRef.current, { type: "response.create" });
|
|
422
|
-
}, [browserTabId, transition]);
|
|
423
|
-
const persistCompletedTranscript = useCallback((event) => {
|
|
424
|
-
const transcript = extractCompletedRealtimeVoiceTranscript(event);
|
|
1159
|
+
if (transportGeneration !== transportGenerationRef.current ||
|
|
1160
|
+
channelRef.current !== transportChannel) {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
toolManifestCoordinator.enqueue(result);
|
|
1164
|
+
}, [browserTabId, toolManifestCoordinator, transition]);
|
|
1165
|
+
const publishCompletedTranscript = useCallback((transcript) => {
|
|
425
1166
|
const threadId = transcriptThreadIdRef.current;
|
|
426
|
-
if (!
|
|
1167
|
+
if (!threadId)
|
|
427
1168
|
return;
|
|
428
1169
|
const sessionIdentity = sessionIdRef.current ?? startedAtRef.current ?? "pending";
|
|
429
1170
|
const providerIdentity = transcript.providerId ?? `sequence-${++transcriptSequenceRef.current}`;
|
|
@@ -435,15 +1176,27 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
435
1176
|
createdAt: new Date().toISOString(),
|
|
436
1177
|
});
|
|
437
1178
|
}, []);
|
|
1179
|
+
const transcriptSequencer = useMemo(() => createRealtimeVoiceTranscriptSequencer(publishCompletedTranscript), [publishCompletedTranscript]);
|
|
1180
|
+
const greetingStarter = useMemo(() => createRealtimeVoiceGreetingStarter((event) => {
|
|
1181
|
+
sendDataChannelEvent(channelRef.current, event);
|
|
1182
|
+
}), []);
|
|
438
1183
|
const handleServerEvent = useCallback((event) => {
|
|
1184
|
+
transcriptSequencer.handle(event);
|
|
1185
|
+
const sessionTools = extractRealtimeVoiceSessionTools(event);
|
|
1186
|
+
if (sessionTools)
|
|
1187
|
+
toolManifestCoordinator.setSessionTools(sessionTools);
|
|
439
1188
|
if (event.type === "session.created") {
|
|
1189
|
+
connectionGateRef.current?.markSessionCreated();
|
|
1190
|
+
connectionGateRef.current = null;
|
|
440
1191
|
const session = event.session;
|
|
441
1192
|
if (session && typeof session === "object") {
|
|
442
1193
|
const id = session.id;
|
|
443
1194
|
if (typeof id === "string")
|
|
444
1195
|
sessionIdRef.current = id;
|
|
445
1196
|
}
|
|
446
|
-
|
|
1197
|
+
updateLivePreferences(preferencesRef.current, true);
|
|
1198
|
+
greetingStarter.start();
|
|
1199
|
+
transition("working");
|
|
447
1200
|
}
|
|
448
1201
|
else if (event.type === "input_audio_buffer.speech_started") {
|
|
449
1202
|
transition("listening");
|
|
@@ -452,10 +1205,14 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
452
1205
|
transition("working");
|
|
453
1206
|
}
|
|
454
1207
|
else if (event.type === "response.created") {
|
|
1208
|
+
// From this point onward the response is committed to audio output, so
|
|
1209
|
+
// changing voices risks violating Realtime's per-session voice lock.
|
|
1210
|
+
hasOutputAudioRef.current = true;
|
|
455
1211
|
lastAssistantTextRef.current = "";
|
|
456
1212
|
transition("working");
|
|
457
1213
|
}
|
|
458
1214
|
else if (event.type === "response.output_audio_transcript.delta") {
|
|
1215
|
+
hasOutputAudioRef.current = true;
|
|
459
1216
|
if (typeof event.delta === "string") {
|
|
460
1217
|
lastAssistantTextRef.current += event.delta;
|
|
461
1218
|
}
|
|
@@ -465,14 +1222,12 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
465
1222
|
if (typeof event.transcript === "string") {
|
|
466
1223
|
lastAssistantTextRef.current = event.transcript;
|
|
467
1224
|
}
|
|
468
|
-
persistCompletedTranscript(event);
|
|
469
1225
|
syncAppState("speaking");
|
|
470
1226
|
}
|
|
471
1227
|
else if (event.type === "conversation.item.input_audio_transcription.completed") {
|
|
472
1228
|
if (typeof event.transcript === "string") {
|
|
473
1229
|
lastUserTextRef.current = event.transcript;
|
|
474
1230
|
}
|
|
475
|
-
persistCompletedTranscript(event);
|
|
476
1231
|
syncAppState("working");
|
|
477
1232
|
}
|
|
478
1233
|
else if (event.type === "response.done") {
|
|
@@ -493,6 +1248,12 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
493
1248
|
: typeof detail === "string"
|
|
494
1249
|
? detail
|
|
495
1250
|
: "";
|
|
1251
|
+
const expansionEventId = detail && typeof detail === "object"
|
|
1252
|
+
? (detail.event_id ?? event.event_id)
|
|
1253
|
+
: event.event_id;
|
|
1254
|
+
if (toolManifestCoordinator.handleError(typeof expansionEventId === "string" ? expansionEventId : undefined, message)) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
496
1257
|
fail(message ||
|
|
497
1258
|
copy?.errors.sessionFailed ||
|
|
498
1259
|
"The realtime voice session encountered an error.");
|
|
@@ -507,10 +1268,13 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
507
1268
|
}, [
|
|
508
1269
|
copy,
|
|
509
1270
|
fail,
|
|
1271
|
+
greetingStarter,
|
|
510
1272
|
handleFunctionCall,
|
|
511
|
-
persistCompletedTranscript,
|
|
512
1273
|
syncAppState,
|
|
1274
|
+
transcriptSequencer,
|
|
513
1275
|
transition,
|
|
1276
|
+
toolManifestCoordinator,
|
|
1277
|
+
updateLivePreferences,
|
|
514
1278
|
]);
|
|
515
1279
|
const start = useCallback(async () => {
|
|
516
1280
|
if (stateRef.current !== "idle")
|
|
@@ -526,31 +1290,32 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
526
1290
|
lastUserTextRef.current = "";
|
|
527
1291
|
lastAssistantTextRef.current = "";
|
|
528
1292
|
sessionIdRef.current = undefined;
|
|
1293
|
+
hasOutputAudioRef.current = false;
|
|
1294
|
+
greetingStarter.reset();
|
|
1295
|
+
setVoiceChangePending(false);
|
|
529
1296
|
transcriptThreadIdRef.current =
|
|
530
1297
|
realtimeVoiceTranscriptRegistry.activeThreadId();
|
|
531
1298
|
transcriptSequenceRef.current = 0;
|
|
1299
|
+
transcriptSequencer.reset();
|
|
532
1300
|
transition("connecting");
|
|
533
1301
|
setChatVisible(false);
|
|
534
1302
|
window.dispatchEvent(new Event("agent-panel:close"));
|
|
1303
|
+
await hydratePreferences();
|
|
1304
|
+
if (stateRef.current !== "connecting")
|
|
1305
|
+
return;
|
|
535
1306
|
const abortController = new AbortController();
|
|
536
1307
|
abortRef.current = abortController;
|
|
537
1308
|
const isCurrentAttempt = () => abortRef.current === abortController && !abortController.signal.aborted;
|
|
538
|
-
const
|
|
539
|
-
if (!isCurrentAttempt())
|
|
540
|
-
return;
|
|
541
|
-
cancelConnectionTimeoutRef.current?.();
|
|
542
|
-
cancelConnectionTimeoutRef.current = null;
|
|
543
|
-
transition("listening");
|
|
544
|
-
};
|
|
545
|
-
cancelConnectionTimeoutRef.current = createRealtimeVoiceConnectionTimeout(() => {
|
|
1309
|
+
const connectionGate = createRealtimeVoiceConnectionGate(() => {
|
|
546
1310
|
if (!isCurrentAttempt())
|
|
547
1311
|
return;
|
|
548
|
-
fail(copy?.errors.
|
|
1312
|
+
fail(copy?.errors.connectionTimedOut ??
|
|
549
1313
|
"The realtime voice connection timed out.");
|
|
550
1314
|
});
|
|
1315
|
+
connectionGateRef.current = connectionGate;
|
|
551
1316
|
try {
|
|
552
1317
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
553
|
-
audio:
|
|
1318
|
+
audio: createRealtimeVoiceAudioConstraints(readRealtimeVoiceMicrophoneId()),
|
|
554
1319
|
});
|
|
555
1320
|
if (!isCurrentAttempt()) {
|
|
556
1321
|
for (const track of stream.getTracks())
|
|
@@ -558,7 +1323,23 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
558
1323
|
return;
|
|
559
1324
|
}
|
|
560
1325
|
streamRef.current = stream;
|
|
1326
|
+
for (const track of stream.getAudioTracks()) {
|
|
1327
|
+
track.onended = () => {
|
|
1328
|
+
void switchMicrophoneRef.current("default", true);
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
561
1331
|
attachAudioMeter(stream, "input");
|
|
1332
|
+
void refreshMicrophones().then((inputs) => {
|
|
1333
|
+
const selected = readRealtimeVoiceMicrophoneId();
|
|
1334
|
+
if (inputs &&
|
|
1335
|
+
selected !== "default" &&
|
|
1336
|
+
!inputs.some((device) => device.deviceId === selected)) {
|
|
1337
|
+
// The persisted device was only an ideal preference, so getUserMedia
|
|
1338
|
+
// already selected a usable fallback for this new session.
|
|
1339
|
+
writeRealtimeVoiceMicrophoneId("default");
|
|
1340
|
+
setMicrophoneDeviceId("default");
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
562
1343
|
const peer = new RTCPeerConnection();
|
|
563
1344
|
peerRef.current = peer;
|
|
564
1345
|
for (const track of stream.getAudioTracks())
|
|
@@ -576,7 +1357,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
576
1357
|
};
|
|
577
1358
|
const channel = peer.createDataChannel("oai-events");
|
|
578
1359
|
channelRef.current = channel;
|
|
579
|
-
channel.onopen =
|
|
1360
|
+
channel.onopen = connectionGate.markTransportReady;
|
|
580
1361
|
channel.onmessage = (messageEvent) => {
|
|
581
1362
|
if (!isCurrentAttempt())
|
|
582
1363
|
return;
|
|
@@ -596,8 +1377,9 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
596
1377
|
peer.onconnectionstatechange = () => {
|
|
597
1378
|
if (!isCurrentAttempt())
|
|
598
1379
|
return;
|
|
599
|
-
if (peer.connectionState === "connected")
|
|
600
|
-
|
|
1380
|
+
if (peer.connectionState === "connected") {
|
|
1381
|
+
connectionGate.markTransportReady();
|
|
1382
|
+
}
|
|
601
1383
|
if (peer.connectionState === "failed") {
|
|
602
1384
|
fail(copy?.errors.connectionFailed ??
|
|
603
1385
|
"The realtime voice connection failed.");
|
|
@@ -613,13 +1395,16 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
613
1395
|
throw new Error(copy?.errors.offerFailed ??
|
|
614
1396
|
"The browser did not create an audio offer.");
|
|
615
1397
|
}
|
|
616
|
-
const
|
|
1398
|
+
const answer = await createRealtimeVoiceSessionWithCapability(offer.sdp, {
|
|
617
1399
|
browserTabId,
|
|
618
1400
|
signal: abortController.signal,
|
|
1401
|
+
preferences: preferencesRef.current,
|
|
1402
|
+
browserLanguages: navigator.languages,
|
|
619
1403
|
});
|
|
620
1404
|
if (!isCurrentAttempt())
|
|
621
1405
|
return;
|
|
622
|
-
|
|
1406
|
+
capabilityRef.current = answer.capability;
|
|
1407
|
+
await peer.setRemoteDescription({ type: "answer", sdp: answer.sdp });
|
|
623
1408
|
}
|
|
624
1409
|
catch (startError) {
|
|
625
1410
|
// Ending a connection aborts the SDP request by design. A superseded
|
|
@@ -630,15 +1415,36 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
630
1415
|
abortRef.current !== abortController) {
|
|
631
1416
|
return;
|
|
632
1417
|
}
|
|
1418
|
+
if (isRealtimeVoiceSetupRequiredError(startError)) {
|
|
1419
|
+
// Credentials can disappear after the entry-point status check. Treat
|
|
1420
|
+
// the authoritative setup response as a gate, not as an active voice
|
|
1421
|
+
// session failure: clean up the mic/RTC attempt, reopen chat, and
|
|
1422
|
+
// refresh both the chat and voice provider setup surfaces.
|
|
1423
|
+
cleanupTransport();
|
|
1424
|
+
setError(null);
|
|
1425
|
+
startedAtRef.current = undefined;
|
|
1426
|
+
sessionIdRef.current = undefined;
|
|
1427
|
+
transcriptThreadIdRef.current = undefined;
|
|
1428
|
+
transition("idle");
|
|
1429
|
+
setChatVisible(true);
|
|
1430
|
+
window.dispatchEvent(new Event("agent-panel:open"));
|
|
1431
|
+
window.dispatchEvent(new Event("agent-engine:configured-changed"));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
633
1434
|
const status = startError?.status;
|
|
634
1435
|
fail(errorMessage(startError), { openKeySettings: status === 400 });
|
|
635
1436
|
}
|
|
636
1437
|
}, [
|
|
637
1438
|
attachAudioMeter,
|
|
638
1439
|
browserTabId,
|
|
1440
|
+
cleanupTransport,
|
|
639
1441
|
copy,
|
|
640
1442
|
fail,
|
|
1443
|
+
greetingStarter,
|
|
641
1444
|
handleServerEvent,
|
|
1445
|
+
hydratePreferences,
|
|
1446
|
+
refreshMicrophones,
|
|
1447
|
+
transcriptSequencer,
|
|
642
1448
|
transition,
|
|
643
1449
|
]);
|
|
644
1450
|
const end = useCallback(() => {
|
|
@@ -671,6 +1477,23 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
671
1477
|
setChatVisible((current) => !current);
|
|
672
1478
|
window.dispatchEvent(new Event("agent-panel:toggle"));
|
|
673
1479
|
}, []);
|
|
1480
|
+
useEffect(() => {
|
|
1481
|
+
const mediaDevices = navigator.mediaDevices;
|
|
1482
|
+
if (!mediaDevices?.addEventListener)
|
|
1483
|
+
return;
|
|
1484
|
+
const handleDeviceChange = () => {
|
|
1485
|
+
const selected = readRealtimeVoiceMicrophoneId();
|
|
1486
|
+
void refreshMicrophones().then((inputs) => {
|
|
1487
|
+
if (inputs &&
|
|
1488
|
+
selected !== "default" &&
|
|
1489
|
+
!inputs.some((device) => device.deviceId === selected)) {
|
|
1490
|
+
void setMicrophone("default");
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
};
|
|
1494
|
+
mediaDevices.addEventListener("devicechange", handleDeviceChange);
|
|
1495
|
+
return () => mediaDevices.removeEventListener("devicechange", handleDeviceChange);
|
|
1496
|
+
}, [refreshMicrophones, setMicrophone]);
|
|
674
1497
|
useEffect(() => {
|
|
675
1498
|
const onSidebarState = (event) => {
|
|
676
1499
|
const detail = event
|
|
@@ -695,6 +1518,16 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
695
1518
|
errorMessage: error,
|
|
696
1519
|
chatVisible,
|
|
697
1520
|
audioLevels,
|
|
1521
|
+
preferences,
|
|
1522
|
+
microphones,
|
|
1523
|
+
microphoneDeviceId,
|
|
1524
|
+
microphoneSwitching,
|
|
1525
|
+
microphoneError,
|
|
1526
|
+
voiceChangePending,
|
|
1527
|
+
setLanguage,
|
|
1528
|
+
setIntelligence,
|
|
1529
|
+
setVoice,
|
|
1530
|
+
setMicrophone,
|
|
698
1531
|
start,
|
|
699
1532
|
end,
|
|
700
1533
|
toggleChat,
|
|
@@ -705,8 +1538,76 @@ export function RealtimeVoiceModeProvider({ children, browserTabId, }) {
|
|
|
705
1538
|
const resolvedBrowserTabId = useMemo(() => browserTabId ?? getBrowserTabId(), [browserTabId]);
|
|
706
1539
|
const copy = useRealtimeVoiceModeCopy();
|
|
707
1540
|
const voice = useRealtimeVoiceModeController(resolvedBrowserTabId, copy);
|
|
1541
|
+
const inlineSettings = useMemo(() => ({
|
|
1542
|
+
dialogLabel: copy.voiceSettings,
|
|
1543
|
+
...(voice.voiceChangePending
|
|
1544
|
+
? { appliesNextConversationNote: copy.settings.voiceChangePending }
|
|
1545
|
+
: {}),
|
|
1546
|
+
...(voice.microphoneError
|
|
1547
|
+
? { microphoneError: voice.microphoneError }
|
|
1548
|
+
: {}),
|
|
1549
|
+
microphone: {
|
|
1550
|
+
label: copy.settings.microphone,
|
|
1551
|
+
value: voice.microphoneDeviceId,
|
|
1552
|
+
disabled: voice.microphoneSwitching,
|
|
1553
|
+
options: [
|
|
1554
|
+
{
|
|
1555
|
+
value: "default",
|
|
1556
|
+
label: copy.settings.defaultMicrophone,
|
|
1557
|
+
},
|
|
1558
|
+
...voice.microphones.map((microphone) => ({
|
|
1559
|
+
value: microphone.deviceId,
|
|
1560
|
+
label: microphone.label,
|
|
1561
|
+
})),
|
|
1562
|
+
],
|
|
1563
|
+
onValueChange: (value) => {
|
|
1564
|
+
void voice.setMicrophone(value);
|
|
1565
|
+
},
|
|
1566
|
+
},
|
|
1567
|
+
language: {
|
|
1568
|
+
label: copy.settings.language,
|
|
1569
|
+
value: voice.preferences.language,
|
|
1570
|
+
options: REALTIME_VOICE_LANGUAGES.map((language) => ({
|
|
1571
|
+
value: language,
|
|
1572
|
+
label: language === "auto"
|
|
1573
|
+
? copy.settings.autoLanguage
|
|
1574
|
+
: copy.settings.languages[language],
|
|
1575
|
+
})),
|
|
1576
|
+
onValueChange: (value) => {
|
|
1577
|
+
if (isOneOf(REALTIME_VOICE_LANGUAGES, value)) {
|
|
1578
|
+
voice.setLanguage(value);
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
},
|
|
1582
|
+
intelligence: {
|
|
1583
|
+
label: copy.settings.intelligence,
|
|
1584
|
+
value: voice.preferences.intelligence,
|
|
1585
|
+
options: REALTIME_VOICE_INTELLIGENCE_LEVELS.map((intelligence) => ({
|
|
1586
|
+
value: intelligence,
|
|
1587
|
+
label: copy.settings.intelligenceLevels[intelligence],
|
|
1588
|
+
})),
|
|
1589
|
+
onValueChange: (value) => {
|
|
1590
|
+
if (isOneOf(REALTIME_VOICE_INTELLIGENCE_LEVELS, value)) {
|
|
1591
|
+
voice.setIntelligence(value);
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
},
|
|
1595
|
+
voiceStyle: {
|
|
1596
|
+
label: copy.settings.voiceStyle,
|
|
1597
|
+
value: voice.preferences.voice,
|
|
1598
|
+
options: REALTIME_VOICES.map((voiceName) => ({
|
|
1599
|
+
value: voiceName,
|
|
1600
|
+
label: `${voiceName[0].toUpperCase()}${voiceName.slice(1)}`,
|
|
1601
|
+
description: copy.settings.voiceDescriptions[voiceName],
|
|
1602
|
+
})),
|
|
1603
|
+
onValueChange: (value) => {
|
|
1604
|
+
if (isOneOf(REALTIME_VOICES, value))
|
|
1605
|
+
voice.setVoice(value);
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
}), [copy, voice]);
|
|
708
1609
|
return (_jsxs(RealtimeVoiceModeContext.Provider, { value: voice, children: [children, voice.active && typeof document !== "undefined"
|
|
709
|
-
? createPortal(_jsx(RealtimeVoiceModeDock, { state: voice.state === "idle" ? "ending" : voice.state, copy: copy, chatVisible: voice.chatVisible, audioLevels: voice.audioLevels, onToggleChat: voice.toggleChat, onEndVoiceMode: voice.end,
|
|
1610
|
+
? createPortal(_jsx(RealtimeVoiceModeDock, { state: voice.state === "idle" ? "ending" : voice.state, copy: copy, chatVisible: voice.chatVisible, audioLevels: voice.audioLevels, onToggleChat: voice.toggleChat, onEndVoiceMode: voice.end, settings: inlineSettings, errorMessage: voice.errorMessage }), document.body)
|
|
710
1611
|
: null] }));
|
|
711
1612
|
}
|
|
712
1613
|
/**
|