@assistant-ui/react 0.15.31 → 0.15.33
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/dist/_virtual/_rolldown/runtime.js +1 -2
- package/dist/adapters/GeminiLiveVoiceAdapter.d.ts +26 -0
- package/dist/adapters/GeminiLiveVoiceAdapter.d.ts.map +1 -0
- package/dist/adapters/GeminiLiveVoiceAdapter.js +346 -0
- package/dist/adapters/GeminiLiveVoiceAdapter.js.map +1 -0
- package/dist/adapters/OpenAIRealtimeVoiceAdapter.js +1 -2
- package/dist/adapters/gemini-live/audio.d.ts +27 -0
- package/dist/adapters/gemini-live/audio.d.ts.map +1 -0
- package/dist/adapters/gemini-live/audio.js +291 -0
- package/dist/adapters/gemini-live/audio.js.map +1 -0
- package/dist/assistant-transport.js +1 -2
- package/dist/cloud-renderer/CloudRendererHost.js +1 -2
- package/dist/context/ReadonlyStore.js +1 -2
- package/dist/context/providers/MessageProvider.js +1 -2
- package/dist/context/providers/ThreadViewportProvider.js +1 -2
- package/dist/context/react/ThreadViewportContext.js +1 -2
- package/dist/context/react/utils/createContextHook.js +1 -2
- package/dist/context/react/utils/createContextStoreHook.js +1 -2
- package/dist/context/stores/ThreadViewport.js +2 -3
- package/dist/devtools/DevToolsHooks.js +1 -2
- package/dist/hooks/useMessageQuote.js +1 -2
- package/dist/hooks/useMessageTiming.js +1 -2
- package/dist/hooks/useToolCallElapsed.js +1 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/internal.js +2 -3
- package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -2
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +4 -4
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/McpAppsRemoteHost.js +1 -2
- package/dist/mcp-apps/app-frame.js +2 -3
- package/dist/mcp-apps/bridge.js +1 -2
- package/dist/mcp-apps/csp.js +1 -2
- package/dist/mcp-apps/types.js +1 -2
- package/dist/mcp-apps/utils.js +1 -2
- package/dist/model-context/frame/useAssistantFrameHost.js +1 -2
- package/dist/model-context/makeAssistantVisible.js +1 -2
- package/dist/primitives/actionBar/ActionBarCopy.js +1 -2
- package/dist/primitives/actionBar/ActionBarEdit.js +1 -2
- package/dist/primitives/actionBar/ActionBarExportMarkdown.js +1 -2
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -2
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -2
- package/dist/primitives/actionBar/ActionBarInteractionContext.js +1 -2
- package/dist/primitives/actionBar/ActionBarReload.js +1 -2
- package/dist/primitives/actionBar/ActionBarRoot.js +1 -2
- package/dist/primitives/actionBar/ActionBarSpeak.js +1 -2
- package/dist/primitives/actionBar/ActionBarStopSpeaking.js +1 -2
- package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -2
- package/dist/primitives/actionBar.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreContent.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreItem.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreRoot.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreSeparator.js +1 -2
- package/dist/primitives/actionBarMore/ActionBarMoreTrigger.js +1 -2
- package/dist/primitives/actionBarMore/scope.js +1 -2
- package/dist/primitives/actionBarMore.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalAnchor.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalContent.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -2
- package/dist/primitives/assistantModal/AssistantModalTrigger.js +1 -2
- package/dist/primitives/assistantModal/popoverRenderPrimitives.js +1 -2
- package/dist/primitives/assistantModal/scope.js +1 -2
- package/dist/primitives/assistantModal.js +1 -2
- package/dist/primitives/attachment/AttachmentName.js +1 -2
- package/dist/primitives/attachment/AttachmentRemove.js +1 -2
- package/dist/primitives/attachment/AttachmentRoot.js +1 -2
- package/dist/primitives/attachment/AttachmentThumb.js +1 -2
- package/dist/primitives/attachment.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerCount.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerNext.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerPrevious.js +1 -2
- package/dist/primitives/branchPicker/BranchPickerRoot.js +1 -2
- package/dist/primitives/branchPicker.js +1 -2
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts +5 -4
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +57 -20
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtRoot.js +1 -2
- package/dist/primitives/chainOfThought.js +1 -2
- package/dist/primitives/composer/ComposerAddAttachment.js +1 -2
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -2
- package/dist/primitives/composer/ComposerCancel.js +1 -2
- package/dist/primitives/composer/ComposerCompactContext.js +1 -2
- package/dist/primitives/composer/ComposerDictate.js +1 -2
- package/dist/primitives/composer/ComposerDictationTranscript.js +1 -2
- package/dist/primitives/composer/ComposerInput.js +1 -2
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -2
- package/dist/primitives/composer/ComposerQuote.js +1 -2
- package/dist/primitives/composer/ComposerResume.js +1 -2
- package/dist/primitives/composer/ComposerRoot.js +1 -2
- package/dist/primitives/composer/ComposerSend.js +1 -2
- package/dist/primitives/composer/ComposerStopDictation.js +1 -2
- package/dist/primitives/composer/composerInputSelector.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopover.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverBack.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverResource.js +1 -2
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +1 -2
- package/dist/primitives/composer/trigger/detectTrigger.js +1 -2
- package/dist/primitives/composer/trigger/index.js +1 -2
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +1 -2
- package/dist/primitives/composer/trigger/triggerDetectionResource.js +1 -2
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +1 -2
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +1 -2
- package/dist/primitives/composer/trigger/triggerSelectionResource.js +1 -2
- package/dist/primitives/composer/useComposerCancelWithFocus.js +1 -2
- package/dist/primitives/composer/useComposerInputState.js +1 -2
- package/dist/primitives/composer.js +1 -2
- package/dist/primitives/dropdownMenuRenderPrimitives.js +1 -2
- package/dist/primitives/error/ErrorMessage.js +1 -2
- package/dist/primitives/error/ErrorRoot.js +1 -2
- package/dist/primitives/error.js +1 -2
- package/dist/primitives/message/MessageError.js +1 -2
- package/dist/primitives/message/MessageIf.js +1 -2
- package/dist/primitives/message/MessageParts.js +1 -2
- package/dist/primitives/message/MessagePartsGrouped.js +2 -3
- package/dist/primitives/message/MessageRoot.js +1 -2
- package/dist/primitives/message.js +1 -2
- package/dist/primitives/messagePart/MessagePartImage.js +1 -2
- package/dist/primitives/messagePart/MessagePartText.js +1 -2
- package/dist/primitives/messagePart/useMessagePartData.js +1 -2
- package/dist/primitives/messagePart/useMessagePartFile.js +1 -2
- package/dist/primitives/messagePart/useMessagePartImage.js +1 -2
- package/dist/primitives/messagePart/useMessagePartReasoning.js +1 -2
- package/dist/primitives/messagePart/useMessagePartSource.js +1 -2
- package/dist/primitives/messagePart/useMessagePartText.js +1 -2
- package/dist/primitives/messagePart.js +1 -2
- package/dist/primitives/queueItem/QueueItemRemove.js +1 -2
- package/dist/primitives/queueItem/QueueItemSteer.js +1 -2
- package/dist/primitives/queueItem/QueueItemText.js +1 -2
- package/dist/primitives/queueItem.js +1 -2
- package/dist/primitives/reasoning/useScrollLock.d.ts.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +41 -25
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -2
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +1 -2
- package/dist/primitives/selectionToolbar.js +1 -2
- package/dist/primitives/suggestion/SuggestionDescription.js +1 -2
- package/dist/primitives/suggestion/SuggestionTitle.js +1 -2
- package/dist/primitives/suggestion/SuggestionTrigger.js +1 -2
- package/dist/primitives/suggestion.js +1 -2
- package/dist/primitives/thread/ThreadEmpty.js +1 -2
- package/dist/primitives/thread/ThreadIf.js +1 -2
- package/dist/primitives/thread/ThreadLoadEarlier.js +1 -2
- package/dist/primitives/thread/ThreadRoot.js +1 -2
- package/dist/primitives/thread/ThreadRootElementContext.js +1 -2
- package/dist/primitives/thread/ThreadScrollToBottom.js +1 -2
- package/dist/primitives/thread/ThreadSuggestion.js +1 -2
- package/dist/primitives/thread/ThreadViewport.js +1 -2
- package/dist/primitives/thread/ThreadViewportFooter.js +1 -2
- package/dist/primitives/thread/topAnchor/computeTopAnchorSlack.js +1 -2
- package/dist/primitives/thread/topAnchor/createReserveObservers.js +1 -2
- package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js +1 -2
- package/dist/primitives/thread/topAnchor/topAnchorTurn.js +1 -2
- package/dist/primitives/thread/topAnchor/topAnchorUtils.js +1 -2
- package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +1 -2
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +1 -2
- package/dist/primitives/thread.js +1 -2
- package/dist/primitives/threadList/ThreadListLoadMore.js +1 -2
- package/dist/primitives/threadList/ThreadListNew.js +1 -2
- package/dist/primitives/threadList/ThreadListRoot.js +1 -2
- package/dist/primitives/threadList.js +1 -2
- package/dist/primitives/threadListFocusGroup.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemArchive.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemDelete.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.js +2 -3
- package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js +1 -2
- package/dist/primitives/threadListItem/ThreadListItemUnarchive.js +1 -2
- package/dist/primitives/threadListItem.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.js +1 -2
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.js +1 -2
- package/dist/primitives/threadListItemMore/scope.js +1 -2
- package/dist/primitives/threadListItemMore.js +1 -2
- package/dist/sandbox-host/SandboxHost.js +2 -3
- package/dist/unstable/useComposerInput.js +1 -2
- package/dist/unstable/useComposerInputHistory.js +2 -3
- package/dist/unstable/useLiveCompletionAdapter.js +1 -2
- package/dist/unstable/useMentionAdapter.js +1 -2
- package/dist/unstable/useMessageStallDetection.js +1 -2
- package/dist/unstable/useModelContextSnapshot.js +1 -2
- package/dist/unstable/useSlashCommandAdapter.js +1 -2
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +1 -2
- package/dist/unstable/webmcp/convertTools.js +1 -2
- package/dist/unstable/webmcp/useWebMcpProvider.js +1 -2
- package/dist/unstable/webmcp/webmcp-host.js +1 -2
- package/dist/utils/Primitive.js +1 -2
- package/dist/utils/createActionButton.js +1 -2
- package/dist/utils/getSelectionMessageId.js +1 -2
- package/dist/utils/hooks/useManagedRef.js +1 -2
- package/dist/utils/hooks/useMediaQuery.js +1 -2
- package/dist/utils/hooks/useOnScrollToBottom.js +1 -2
- package/dist/utils/hooks/useSizeHandle.js +1 -2
- package/dist/utils/isCompositionKey.js +1 -2
- package/dist/utils/smooth/SmoothContext.js +1 -2
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +11 -4
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/dist/utils/useIsomorphicLayoutEffect.js +0 -1
- package/package.json +8 -8
- package/src/adapters/GeminiLiveVoiceAdapter.test.ts +992 -0
- package/src/adapters/GeminiLiveVoiceAdapter.ts +454 -0
- package/src/adapters/gemini-live/audio.test.ts +880 -0
- package/src/adapters/gemini-live/audio.ts +333 -0
- package/src/index.test.ts +34 -0
- package/src/index.ts +4 -0
- package/src/mcp-apps/McpAppRenderer.composer.test.tsx +111 -0
- package/src/mcp-apps/McpAppRenderer.test.tsx +24 -23
- package/src/mcp-apps/McpAppRenderer.tsx +9 -1
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.test.tsx +91 -0
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.tsx +54 -0
- package/src/primitives/reasoning/useScrollLock.test.tsx +71 -2
- package/src/primitives/reasoning/useScrollLock.ts +78 -51
- package/src/primitives/threadListItem/ThreadListItemRoot.tsx +4 -1
- package/src/primitives/threadListItem/ThreadListKeyboardNav.test.tsx +25 -0
- package/src/utils/smooth/useSmooth.test.tsx +113 -4
- package/src/utils/smooth/useSmooth.ts +16 -2
- package/src/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.ts +0 -49
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createVoiceSession,
|
|
3
|
+
type RealtimeVoiceAdapter,
|
|
4
|
+
} from "@assistant-ui/core";
|
|
5
|
+
import {
|
|
6
|
+
createPcmPlayer,
|
|
7
|
+
openMicrophone,
|
|
8
|
+
type MicrophoneCapture,
|
|
9
|
+
type PcmPlayer,
|
|
10
|
+
} from "./gemini-live/audio";
|
|
11
|
+
|
|
12
|
+
/** Options for a browser WebSocket connection to the Gemini Live API. */
|
|
13
|
+
export type GeminiLiveVoiceAdapterOptions = {
|
|
14
|
+
/** Return a short-lived ephemeral token minted by your authenticated server. Keep the API key and session configuration on the server. */
|
|
15
|
+
getToken: (options: { signal: AbortSignal }) => Promise<string>;
|
|
16
|
+
/** Live model to connect to, such as `gemini-3.8-live`; it must match the model your token was minted for. */
|
|
17
|
+
model: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ENDPOINT =
|
|
21
|
+
"wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContentConstrained";
|
|
22
|
+
const INPUT_RATE = 16_000;
|
|
23
|
+
const OUTPUT_RATE = 24_000;
|
|
24
|
+
|
|
25
|
+
const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
|
26
|
+
value !== null && typeof value === "object" && !Array.isArray(value)
|
|
27
|
+
? (value as Record<string, unknown>)
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Connects browser microphone audio directly to the Gemini Live API over a
|
|
32
|
+
* WebSocket. Configure the model, voice, and transcription on the server
|
|
33
|
+
* through the ephemeral token's connection constraints. Each connection starts
|
|
34
|
+
* a new conversation; existing thread history and assistant-ui tools are not
|
|
35
|
+
* sent to the voice model, and sessions end when Google's session limit is
|
|
36
|
+
* reached.
|
|
37
|
+
*/
|
|
38
|
+
export class GeminiLiveVoiceAdapter implements RealtimeVoiceAdapter {
|
|
39
|
+
private readonly options: GeminiLiveVoiceAdapterOptions;
|
|
40
|
+
|
|
41
|
+
constructor(options: GeminiLiveVoiceAdapterOptions) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
connect(options: {
|
|
46
|
+
abortSignal?: AbortSignal;
|
|
47
|
+
}): RealtimeVoiceAdapter.Session {
|
|
48
|
+
return createVoiceSession(options, async (session) => {
|
|
49
|
+
if (
|
|
50
|
+
typeof WebSocket === "undefined" ||
|
|
51
|
+
typeof AudioContext === "undefined" ||
|
|
52
|
+
typeof navigator === "undefined" ||
|
|
53
|
+
!navigator.mediaDevices?.getUserMedia
|
|
54
|
+
) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"Gemini Live voice requires a browser with WebSockets, Web Audio, and microphone access.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
let socket: WebSocket | undefined;
|
|
62
|
+
let microphone: MicrophoneCapture | undefined;
|
|
63
|
+
let player: PcmPlayer | undefined;
|
|
64
|
+
let muted = false;
|
|
65
|
+
let closed = false;
|
|
66
|
+
let ending = false;
|
|
67
|
+
let ready = false;
|
|
68
|
+
let generating = false;
|
|
69
|
+
let cancelled = false;
|
|
70
|
+
let goingAway = false;
|
|
71
|
+
let holdingAssistant = false;
|
|
72
|
+
let assistantText = "";
|
|
73
|
+
let userText = "";
|
|
74
|
+
let userFinished = false;
|
|
75
|
+
let nextUserText = "";
|
|
76
|
+
let nextUserFinals: string[] = [];
|
|
77
|
+
let deferred: Record<string, unknown>[] | undefined;
|
|
78
|
+
let volumeTimer: ReturnType<typeof setInterval> | undefined;
|
|
79
|
+
let connectionTimer: ReturnType<typeof setTimeout> | undefined;
|
|
80
|
+
let pendingText:
|
|
81
|
+
| {
|
|
82
|
+
awaitingInterrupt: boolean;
|
|
83
|
+
resolve: () => void;
|
|
84
|
+
reject: (error: Error) => void;
|
|
85
|
+
timer: ReturnType<typeof setTimeout>;
|
|
86
|
+
}
|
|
87
|
+
| undefined;
|
|
88
|
+
|
|
89
|
+
const settleText = (error?: Error) => {
|
|
90
|
+
if (!pendingText) return;
|
|
91
|
+
clearTimeout(pendingText.timer);
|
|
92
|
+
const { resolve, reject } = pendingText;
|
|
93
|
+
pendingText = undefined;
|
|
94
|
+
if (error) reject(error);
|
|
95
|
+
else resolve();
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const disconnect = () => {
|
|
99
|
+
if (closed) return;
|
|
100
|
+
closed = true;
|
|
101
|
+
controller.abort();
|
|
102
|
+
clearInterval(volumeTimer);
|
|
103
|
+
clearTimeout(connectionTimer);
|
|
104
|
+
deferred = undefined;
|
|
105
|
+
settleText(
|
|
106
|
+
new Error(
|
|
107
|
+
"Gemini Live session disconnected before accepting the message.",
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
if (socket) {
|
|
111
|
+
socket.onopen = socket.onmessage = socket.onclose = null;
|
|
112
|
+
socket.onerror = null;
|
|
113
|
+
if (
|
|
114
|
+
socket.readyState === WebSocket.CONNECTING ||
|
|
115
|
+
socket.readyState === WebSocket.OPEN
|
|
116
|
+
)
|
|
117
|
+
socket.close();
|
|
118
|
+
}
|
|
119
|
+
void microphone?.close().catch(() => {});
|
|
120
|
+
void player?.close().catch(() => {});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const end = (reason: "finished" | "error", error?: unknown) => {
|
|
124
|
+
if (ending) return;
|
|
125
|
+
ending = true;
|
|
126
|
+
if (ready && !closed) {
|
|
127
|
+
// The deferral timer skips disconnected sessions, so route queued frames before finishing the turn.
|
|
128
|
+
try {
|
|
129
|
+
while (deferred !== undefined) {
|
|
130
|
+
const queue = deferred;
|
|
131
|
+
deferred = undefined;
|
|
132
|
+
for (const item of queue) routeContent(item);
|
|
133
|
+
}
|
|
134
|
+
} catch (drainError) {
|
|
135
|
+
reason = "error";
|
|
136
|
+
error = drainError;
|
|
137
|
+
}
|
|
138
|
+
finishTurn();
|
|
139
|
+
flushUser();
|
|
140
|
+
}
|
|
141
|
+
session.end(reason, error);
|
|
142
|
+
disconnect();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const send = (message: Record<string, unknown>) => {
|
|
146
|
+
if (closed || ending || !ready || socket?.readyState !== WebSocket.OPEN)
|
|
147
|
+
throw new Error("Gemini Live session is not connected.");
|
|
148
|
+
socket.send(JSON.stringify(message));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const flushUser = () => {
|
|
152
|
+
userFinished = true;
|
|
153
|
+
if (!userText) return;
|
|
154
|
+
const text = userText;
|
|
155
|
+
userText = "";
|
|
156
|
+
session.emitTranscript({ role: "user", text, isFinal: true });
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const emitAssistant = (isFinal: boolean) => {
|
|
160
|
+
if (!assistantText || holdingAssistant) return;
|
|
161
|
+
session.emitTranscript({
|
|
162
|
+
role: "assistant",
|
|
163
|
+
text: assistantText,
|
|
164
|
+
isFinal,
|
|
165
|
+
});
|
|
166
|
+
if (isFinal) assistantText = "";
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const stopPlayback = () => {
|
|
170
|
+
if (!player?.isPlaying()) return;
|
|
171
|
+
player.clear();
|
|
172
|
+
session.emitMode("listening");
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const releaseAssistant = () => {
|
|
176
|
+
if (!holdingAssistant) return;
|
|
177
|
+
flushUser();
|
|
178
|
+
holdingAssistant = false;
|
|
179
|
+
emitAssistant(false);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const startReply = (typed: boolean) => {
|
|
183
|
+
generating = true;
|
|
184
|
+
cancelled = false;
|
|
185
|
+
holdingAssistant = !typed && !userFinished;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const finishTurn = () => {
|
|
189
|
+
generating = false;
|
|
190
|
+
holdingAssistant = false;
|
|
191
|
+
flushUser();
|
|
192
|
+
emitAssistant(true);
|
|
193
|
+
for (const text of nextUserFinals)
|
|
194
|
+
session.emitTranscript({ role: "user", text, isFinal: true });
|
|
195
|
+
userFinished = nextUserFinals.length > 0 && !nextUserText;
|
|
196
|
+
userText = nextUserText;
|
|
197
|
+
nextUserText = "";
|
|
198
|
+
nextUserFinals = [];
|
|
199
|
+
if (pendingText) pendingText.awaitingInterrupt = false;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleContent = (content: Record<string, unknown>) => {
|
|
203
|
+
const parts = asRecord(content.modelTurn)?.parts;
|
|
204
|
+
const output = asRecord(content.outputTranscription)?.text;
|
|
205
|
+
const input = asRecord(content.inputTranscription);
|
|
206
|
+
const nextTurn = generating && !holdingAssistant;
|
|
207
|
+
if (typeof input?.text === "string" && input.text) {
|
|
208
|
+
if (nextTurn) nextUserText += input.text;
|
|
209
|
+
else {
|
|
210
|
+
if (!generating) userFinished = false;
|
|
211
|
+
userText += input.text;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (input?.finished === true) {
|
|
215
|
+
if (nextTurn) {
|
|
216
|
+
if (nextUserText) nextUserFinals.push(nextUserText);
|
|
217
|
+
nextUserText = "";
|
|
218
|
+
} else if (generating) releaseAssistant();
|
|
219
|
+
else flushUser();
|
|
220
|
+
}
|
|
221
|
+
if (content.interrupted === true) {
|
|
222
|
+
stopPlayback();
|
|
223
|
+
finishTurn();
|
|
224
|
+
cancelled = true;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (Array.isArray(parts) && !generating) startReply(false);
|
|
228
|
+
if (generating) {
|
|
229
|
+
if (Array.isArray(parts) && !pendingText?.awaitingInterrupt)
|
|
230
|
+
for (const part of parts) {
|
|
231
|
+
const data = asRecord(asRecord(part)?.inlineData)?.data;
|
|
232
|
+
if (typeof data === "string" && player) {
|
|
233
|
+
const wasPlaying = player.isPlaying();
|
|
234
|
+
player.enqueue(data);
|
|
235
|
+
if (!wasPlaying) session.emitMode("speaking");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (typeof output === "string" && output) {
|
|
239
|
+
assistantText += output;
|
|
240
|
+
emitAssistant(false);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (content.turnComplete === true) {
|
|
244
|
+
if (cancelled) cancelled = false;
|
|
245
|
+
else finishTurn();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const routeContent = (content: Record<string, unknown>) => {
|
|
250
|
+
if (deferred) {
|
|
251
|
+
deferred.push(content);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (
|
|
255
|
+
pendingText &&
|
|
256
|
+
!pendingText.awaitingInterrupt &&
|
|
257
|
+
content.modelTurn !== undefined
|
|
258
|
+
) {
|
|
259
|
+
const input = asRecord(content.inputTranscription)?.text;
|
|
260
|
+
if (typeof input === "string") userText += input;
|
|
261
|
+
flushUser();
|
|
262
|
+
settleText();
|
|
263
|
+
startReply(true);
|
|
264
|
+
const queue = (deferred = [
|
|
265
|
+
{ ...content, inputTranscription: undefined },
|
|
266
|
+
]);
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
deferred = undefined;
|
|
269
|
+
if (closed) return;
|
|
270
|
+
try {
|
|
271
|
+
for (const item of queue) routeContent(item);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
end("error", error);
|
|
274
|
+
}
|
|
275
|
+
}, 0);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
handleContent(content);
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const handleMessage = (message: Record<string, unknown>) => {
|
|
282
|
+
if (closed) return;
|
|
283
|
+
if (message.setupComplete !== undefined) {
|
|
284
|
+
if (ready) return;
|
|
285
|
+
ready = true;
|
|
286
|
+
clearTimeout(connectionTimer);
|
|
287
|
+
session.setStatus({ type: "running" });
|
|
288
|
+
if (closed) return;
|
|
289
|
+
session.emitMode("listening");
|
|
290
|
+
if (closed) return;
|
|
291
|
+
volumeTimer = setInterval(() => {
|
|
292
|
+
if (closed) return;
|
|
293
|
+
session.emitVolume(
|
|
294
|
+
Math.min(
|
|
295
|
+
1,
|
|
296
|
+
Math.max(
|
|
297
|
+
muted ? 0 : (microphone?.level() ?? 0),
|
|
298
|
+
player?.level() ?? 0,
|
|
299
|
+
),
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
}, 100);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const content = asRecord(message.serverContent);
|
|
306
|
+
if (!content) {
|
|
307
|
+
const goAway = asRecord(message.goAway);
|
|
308
|
+
if (goAway) {
|
|
309
|
+
goingAway = true;
|
|
310
|
+
console.warn("Gemini Live session is closing.", goAway.timeLeft);
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
routeContent(content);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const open = (token: string) => {
|
|
318
|
+
socket = new WebSocket(`${ENDPOINT}?access_token=${token}`);
|
|
319
|
+
socket.binaryType = "arraybuffer";
|
|
320
|
+
const decoder = new TextDecoder();
|
|
321
|
+
socket.onopen = () => {
|
|
322
|
+
if (closed) return;
|
|
323
|
+
socket?.send(
|
|
324
|
+
JSON.stringify({
|
|
325
|
+
setup: {
|
|
326
|
+
model: `models/${this.options.model}`,
|
|
327
|
+
generationConfig: { responseModalities: ["AUDIO"] },
|
|
328
|
+
inputAudioTranscription: {},
|
|
329
|
+
outputAudioTranscription: {},
|
|
330
|
+
},
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
};
|
|
334
|
+
socket.onmessage = ({ data }) => {
|
|
335
|
+
let message: Record<string, unknown> | undefined;
|
|
336
|
+
try {
|
|
337
|
+
message = asRecord(
|
|
338
|
+
JSON.parse(
|
|
339
|
+
typeof data === "string" ? data : decoder.decode(data),
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
} catch {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!message) return;
|
|
346
|
+
try {
|
|
347
|
+
handleMessage(message);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
end("error", error);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
socket.onclose = ({ code, reason }) => {
|
|
353
|
+
if (ready) {
|
|
354
|
+
if (code === 1000 || goingAway) end("finished");
|
|
355
|
+
else
|
|
356
|
+
end(
|
|
357
|
+
"error",
|
|
358
|
+
new Error(
|
|
359
|
+
`Gemini Live connection closed (${code}${reason ? `: ${reason}` : ""}).`,
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
} else
|
|
363
|
+
end(
|
|
364
|
+
"error",
|
|
365
|
+
new Error(
|
|
366
|
+
`Gemini Live connection closed before setup completed (${code}${reason ? `: ${reason}` : ""}).`,
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
socket.onerror = () =>
|
|
371
|
+
end("error", new Error("Gemini Live connection failed."));
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const start = async () => {
|
|
375
|
+
player = createPcmPlayer({
|
|
376
|
+
sampleRate: OUTPUT_RATE,
|
|
377
|
+
onIdle: () => {
|
|
378
|
+
if (!closed) session.emitMode("listening");
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
const capture = await openMicrophone({
|
|
382
|
+
sampleRate: INPUT_RATE,
|
|
383
|
+
signal: controller.signal,
|
|
384
|
+
onChunk: (data) => {
|
|
385
|
+
if (closed || !ready || socket?.readyState !== WebSocket.OPEN)
|
|
386
|
+
return;
|
|
387
|
+
socket.send(
|
|
388
|
+
JSON.stringify({
|
|
389
|
+
realtimeInput: {
|
|
390
|
+
audio: { data, mimeType: `audio/pcm;rate=${INPUT_RATE}` },
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
if (closed || session.isDisposed()) {
|
|
397
|
+
void capture.close().catch(() => {});
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
microphone = capture;
|
|
401
|
+
capture.setMuted(muted);
|
|
402
|
+
connectionTimer = setTimeout(
|
|
403
|
+
() => end("error", new Error("Gemini Live connection timed out.")),
|
|
404
|
+
30_000,
|
|
405
|
+
);
|
|
406
|
+
const token = await this.options.getToken({
|
|
407
|
+
signal: controller.signal,
|
|
408
|
+
});
|
|
409
|
+
if (closed) return;
|
|
410
|
+
open(token);
|
|
411
|
+
};
|
|
412
|
+
void start().catch((error: unknown) => end("error", error));
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
disconnect,
|
|
416
|
+
mute: () => {
|
|
417
|
+
muted = true;
|
|
418
|
+
microphone?.setMuted(true);
|
|
419
|
+
if (ready && socket?.readyState === WebSocket.OPEN)
|
|
420
|
+
send({ realtimeInput: { audioStreamEnd: true } });
|
|
421
|
+
},
|
|
422
|
+
unmute: () => {
|
|
423
|
+
muted = false;
|
|
424
|
+
microphone?.setMuted(false);
|
|
425
|
+
},
|
|
426
|
+
sendText: (text) =>
|
|
427
|
+
new Promise<void>((resolve, reject) => {
|
|
428
|
+
settleText(
|
|
429
|
+
new Error("Gemini Live replaced the message with a newer one."),
|
|
430
|
+
);
|
|
431
|
+
send({
|
|
432
|
+
clientContent: {
|
|
433
|
+
turns: [{ role: "user", parts: [{ text }] }],
|
|
434
|
+
turnComplete: true,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
stopPlayback();
|
|
438
|
+
pendingText = {
|
|
439
|
+
awaitingInterrupt: generating,
|
|
440
|
+
resolve,
|
|
441
|
+
reject,
|
|
442
|
+
timer: setTimeout(() => {
|
|
443
|
+
settleText(
|
|
444
|
+
new Error(
|
|
445
|
+
"Gemini Live did not start a reply to the message.",
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
}, 10_000),
|
|
449
|
+
};
|
|
450
|
+
}),
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|