vzcode 2.29.0 → 2.31.0

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.
Files changed (35) hide show
  1. package/dist/assets/bindings_wasm_bg-9w8E-TmY.wasm +0 -0
  2. package/dist/assets/buildWorker-DylWm2VX.js +687 -0
  3. package/dist/assets/index-BfDRE_ou.js +364 -0
  4. package/dist/assets/index-Dyt9tpmu.css +1 -0
  5. package/dist/assets/worker-D8p1JlHW.js +342 -0
  6. package/dist/assets/worker-b9bkZkXK.js +116 -0
  7. package/dist/assets/{worker-CoJaqtpi.js → worker-k7HZ3t-V.js} +12 -12
  8. package/dist/index.html +2 -2
  9. package/dist/llm-streaming-server/chatOperations.js +40 -10
  10. package/package.json +1 -1
  11. package/src/client/App/index.tsx +38 -24
  12. package/src/client/CodeEditor/getOrCreateEditor.tsx +6 -1
  13. package/src/client/CodeEditor/index.tsx +4 -0
  14. package/src/client/VZCodeContext/useVZCodeState.ts +1 -0
  15. package/src/client/VZLeft.tsx +17 -2
  16. package/src/client/VZMiddle.tsx +12 -0
  17. package/src/client/VZSidebar/VisualEditor/VisualEditor.tsx +101 -48
  18. package/src/client/VZSidebar/VisualEditor/utils.ts +113 -0
  19. package/src/client/VZSidebar/index.tsx +23 -6
  20. package/src/client/aiChatButtonEvent.ts +61 -0
  21. package/src/client/featureFlags.ts +8 -0
  22. package/src/client/index.ts +6 -0
  23. package/src/client/main.jsx +22 -0
  24. package/src/llm-streaming-server/chatOperations.ts +53 -12
  25. package/src/llm-streaming-server/llmStreaming.ts +0 -3
  26. package/src/llm-streaming-ui/components/ChatInput.tsx +49 -29
  27. package/src/llm-streaming-ui/components/Message.tsx +0 -2
  28. package/src/llm-streaming-ui/components/useSpeechRecognition.ts +84 -60
  29. package/src/vite-env.d.ts +1 -0
  30. package/dist/assets/bindings_wasm_bg--2fP_mkJ.wasm +0 -0
  31. package/dist/assets/buildWorker-BV4WiKg8.js +0 -687
  32. package/dist/assets/index-CCmGeE_1.css +0 -1
  33. package/dist/assets/index-JcHUw7hN.js +0 -403
  34. package/dist/assets/worker-AbbeoMbH.js +0 -340
  35. package/dist/assets/worker-B71kfwKL.js +0 -125
@@ -40,14 +40,22 @@ import { useDragAndDrop } from './useDragAndDrop';
40
40
  import { createAICopyPasteHandlers } from './aiCopyPaste';
41
41
  import {
42
42
  enableLiveKit,
43
- enableAIChat,
43
+ enableAIChat as enableAIChatFeatureFlag,
44
44
  } from '../featureFlags';
45
+ import { handleAIChatButtonClick } from '../aiChatButtonEvent';
45
46
  import './styles.scss';
46
47
  import { VisualEditor } from './VisualEditor';
47
48
 
48
49
  const enableConnectionStatus = true;
49
50
 
50
51
  export const VZSidebar = ({
52
+ // Host-provided override so consumers (e.g. VizHub Studio, which has
53
+ // its own top-bar "AI Edit" entry point) can hide the "Edit with AI"
54
+ // sidebar button. Defaults to the compile-time feature flag.
55
+ enableAIChat = enableAIChatFeatureFlag,
56
+ // When set, clicking the button dispatches this custom event instead
57
+ // of opening VZCode's built-in AI chat, letting the host handle it.
58
+ aiChatButtonEvent,
51
59
  createFileTooltipText = (
52
60
  <>
53
61
  <strong>New file</strong>
@@ -135,6 +143,8 @@ export const VZSidebar = ({
135
143
  voiceChatToolTipText?: React.ReactNode;
136
144
  aiChatToolTipText?: React.ReactNode;
137
145
  visualEditorToolTipText?: React.ReactNode;
146
+ enableAIChat?: boolean;
147
+ aiChatButtonEvent?: string;
138
148
  }) => {
139
149
  const {
140
150
  files,
@@ -341,11 +351,17 @@ export const VZSidebar = ({
341
351
  <i
342
352
  id="ai-chat-icon"
343
353
  className="icon-button icon-button-dark"
344
- onClick={() => {
345
- setIsAIChatOpen(true);
346
- setIsSearchOpen(false);
347
- setSidebarView(true); // Switch to AI chat view
348
- }}
354
+ onClick={() =>
355
+ handleAIChatButtonClick({
356
+ aiChatButtonEvent,
357
+ target: window,
358
+ openAIChat: () => {
359
+ setIsAIChatOpen(true);
360
+ setIsSearchOpen(false);
361
+ setSidebarView(true); // Switch to AI chat view
362
+ },
363
+ })
364
+ }
349
365
  >
350
366
  <SparklesSVG />
351
367
  </i>
@@ -411,6 +427,7 @@ export const VZSidebar = ({
411
427
  setIsVisualEditorOpen(true);
412
428
  setIsAIChatOpen(false);
413
429
  setIsSearchOpen(false);
430
+ setSidebarView(false); // Switch to files view width for visual editor
414
431
  }}
415
432
  >
416
433
  <AdjustmentSVG />
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Host-facing event emitted when the VZCode sidebar "Edit with AI"
3
+ * button is clicked in "emit" mode.
4
+ *
5
+ * A host opts in by passing `aiChatButtonEvent` to `VZSidebar` /
6
+ * `VZLeft`. When set, clicking `#ai-chat-icon` dispatches this event
7
+ * instead of opening VZCode's built-in AI chat panel, so the host can
8
+ * route the click to its own AI editing entry point (e.g. VizHub
9
+ * Studio's top-bar "AI Edit" action).
10
+ *
11
+ * The constant is exported so hosts don't hardcode the event name.
12
+ */
13
+ export const AI_CHAT_BUTTON_EVENT =
14
+ 'vzcode:ai-chat-button-click';
15
+
16
+ export interface AIChatButtonEventDetail {
17
+ source: 'vzcode-sidebar';
18
+ }
19
+
20
+ /**
21
+ * Dispatch the host-facing AI chat button event.
22
+ * Kept as a standalone function so it is unit-testable without a DOM.
23
+ */
24
+ export function emitAIChatButtonEvent(
25
+ eventName: string,
26
+ target: EventTarget,
27
+ ): void {
28
+ target.dispatchEvent(
29
+ new CustomEvent<AIChatButtonEventDetail>(eventName, {
30
+ bubbles: true,
31
+ composed: true,
32
+ detail: { source: 'vzcode-sidebar' },
33
+ }),
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Decide what clicking the sidebar AI chat button should do.
39
+ *
40
+ * - When `aiChatButtonEvent` is set, emit the event and leave all
41
+ * VZCode state untouched (the host owns the action).
42
+ * - Otherwise, fall back to opening VZCode's built-in AI chat.
43
+ *
44
+ * Extracted from the component so the branching is unit-testable
45
+ * without rendering or a real DOM.
46
+ */
47
+ export function handleAIChatButtonClick({
48
+ aiChatButtonEvent,
49
+ target,
50
+ openAIChat,
51
+ }: {
52
+ aiChatButtonEvent?: string;
53
+ target: EventTarget;
54
+ openAIChat: () => void;
55
+ }): void {
56
+ if (aiChatButtonEvent) {
57
+ emitAIChatButtonEvent(aiChatButtonEvent, target);
58
+ return;
59
+ }
60
+ openAIChat();
61
+ }
@@ -1,6 +1,14 @@
1
1
  export const enableLiveKit =
2
2
  import.meta.env.VITE_ENABLE_LIVEKIT === 'true';
3
3
 
4
+ // Enables the speech-to-text microphone button in the AI chat
5
+ // input. Off by default: speech recognition is a browser media
6
+ // feature, and merely constructing a `SpeechRecognition` instance
7
+ // can make the browser show a microphone permission prompt. Set
8
+ // `VITE_ENABLE_VOICE_INPUT=true` to opt in.
9
+ export const enableVoiceInput =
10
+ import.meta.env.VITE_ENABLE_VOICE_INPUT === 'true';
11
+
4
12
  export const enableAIChat = true;
5
13
 
6
14
  export const enableDiffView = true;
@@ -11,6 +11,12 @@ export { VZKeyboardShortcutsDoc } from './VZKeyboardShortcutsDoc';
11
11
  export { VZResizer } from './VZResizer';
12
12
  export { VZLeft } from './VZLeft';
13
13
  export { VZMiddle } from './VZMiddle';
14
+ export {
15
+ AI_CHAT_BUTTON_EVENT,
16
+ emitAIChatButtonEvent,
17
+ handleAIChatButtonClick,
18
+ } from './aiChatButtonEvent';
19
+ export type { AIChatButtonEventDetail } from './aiChatButtonEvent';
14
20
  export {
15
21
  SplitPaneResizeProvider,
16
22
  SplitPaneResizeContext,
@@ -3,6 +3,28 @@ import * as ReactDOM from 'react-dom/client';
3
3
  import App from './App';
4
4
  import { BrowserRouter } from 'react-router-dom';
5
5
 
6
+ // Global error handler to catch CodeMirror DOM position errors
7
+ // These can occur when CodeMirror instances are created but not yet
8
+ // attached to the DOM, or when presence indicators try to render
9
+ // on offscreen editor instances.
10
+ window.addEventListener('error', (event) => {
11
+ // Check if this is the specific CodeMirror posFromDOM/posAtDOM error
12
+ if (
13
+ event.error instanceof RangeError &&
14
+ event.error.message.includes(
15
+ 'Trying to find position for a DOM position outside of the document',
16
+ )
17
+ ) {
18
+ // Log the error for debugging purposes
19
+ console.log(
20
+ 'Suppressed CodeMirror DOM position error:',
21
+ event.error,
22
+ );
23
+ // Prevent the error from being logged to console by returning true
24
+ return true;
25
+ }
26
+ });
27
+
6
28
  ReactDOM.createRoot(document.getElementById('root')).render(
7
29
  <BrowserRouter>
8
30
  <App />
@@ -335,30 +335,66 @@ export const finalizeAIMessage = (
335
335
  };
336
336
 
337
337
  /**
338
- * Adds diff data to the most recent AI message
338
+ * Adds diff data to a specific AI message by ID
339
+ * Phase 3 Fix: Use explicit message ID instead of assuming
340
+ * last message is AI, preventing bugs with rapid message sends
339
341
  */
340
342
  export const addDiffToAIMessage = (
341
343
  shareDBDoc: ShareDBDoc<VizContent>,
342
344
  chatId: VizChatId,
343
345
  diffData: any,
344
- beforeCommitId?: string, // Commit ID before AI changes for VizHub integration
346
+ beforeCommitId?: string, // Commit ID before AI changes
347
+ messageId?: string, // Explicit AI message ID to update
345
348
  ) => {
346
349
  const chat = shareDBDoc.data.chats[chatId];
350
+ if (!chat || !chat.messages) {
351
+ console.warn(
352
+ `Chat ${chatId} not found or has no messages`,
353
+ );
354
+ return;
355
+ }
356
+
347
357
  const messages = [...chat.messages];
358
+ let targetMessageIndex = -1;
348
359
 
349
- // Find the most recent AI message
350
- const lastAIMessageIndex = messages.length - 1;
351
- if (
352
- lastAIMessageIndex >= 0 &&
353
- messages[lastAIMessageIndex].role === 'assistant'
354
- ) {
360
+ // Phase 3 Fix: Use explicit message ID if provided
361
+ if (messageId) {
362
+ targetMessageIndex = messages.findIndex(
363
+ (msg) => msg.id === messageId,
364
+ );
365
+ if (
366
+ targetMessageIndex === -1 ||
367
+ messages[targetMessageIndex].role !== 'assistant'
368
+ ) {
369
+ console.warn(
370
+ `AI message with id ${messageId} not found in chat ${chatId}`,
371
+ );
372
+ return;
373
+ }
374
+ } else {
375
+ // Fallback: Find the most recent AI message (safer than
376
+ // just assuming last message)
377
+ for (let i = messages.length - 1; i >= 0; i--) {
378
+ if (messages[i].role === 'assistant') {
379
+ targetMessageIndex = i;
380
+ break;
381
+ }
382
+ }
383
+
384
+ if (targetMessageIndex === -1) {
385
+ console.warn(`No AI message found in chat ${chatId}`);
386
+ return;
387
+ }
388
+ }
389
+
390
+ // Update the target message with diff data
391
+ if (targetMessageIndex >= 0) {
355
392
  const newMessage = {
356
- ...messages[lastAIMessageIndex],
393
+ ...messages[targetMessageIndex],
357
394
  diffData,
358
- ...(beforeCommitId && { beforeCommitId }), // Add beforeCommitId for VizHub integration
395
+ ...(beforeCommitId && { beforeCommitId }),
359
396
  };
360
- // Use type assertion to extend the message with diffData
361
- (messages[lastAIMessageIndex] as any) = newMessage;
397
+ (messages[targetMessageIndex] as any) = newMessage;
362
398
 
363
399
  const messageOp = diff(shareDBDoc.data, {
364
400
  ...shareDBDoc.data,
@@ -373,6 +409,11 @@ export const addDiffToAIMessage = (
373
409
  });
374
410
 
375
411
  shareDBDoc.submitOp(messageOp);
412
+
413
+ DEBUG &&
414
+ console.log(
415
+ `addDiffToAIMessage: Successfully added diff to message ${targetMessageIndex} in chat ${chatId}`,
416
+ );
376
417
  }
377
418
  };
378
419
 
@@ -78,7 +78,6 @@ export const createLLMFunction = ({
78
78
  },
79
79
  });
80
80
 
81
-
82
81
  let fullContent = '';
83
82
  let generationId = '';
84
83
  let currentEditingFileName = null;
@@ -253,8 +252,6 @@ export const createLLMFunction = ({
253
252
  const chunks = [];
254
253
  let reasoningContent = '';
255
254
 
256
-
257
-
258
255
  // Configure reasoning tokens based on enableReasoningTokens flag
259
256
  const requestConfig: any = {
260
257
  model: modelName,
@@ -10,10 +10,50 @@ import {
10
10
  ButtonGroup,
11
11
  ToggleButton,
12
12
  } from '../../client/bootstrap';
13
- import { enableAskMode } from '../../client/featureFlags';
13
+ import {
14
+ enableAskMode,
15
+ enableVoiceInput,
16
+ } from '../../client/featureFlags';
14
17
  import { useSpeechRecognition } from './useSpeechRecognition';
15
18
  import { MicSVG, MicOffSVG } from '../../client/Icons';
16
19
 
20
+ /**
21
+ * Microphone toggle for voice typing. Purely presentational: the
22
+ * speech recognition state lives in `ChatInputComponent`, and is only
23
+ * ever instantiated when the user actually toggles the button.
24
+ *
25
+ * Rendered only when `enableVoiceInput` is set. See `featureFlags`.
26
+ */
27
+ const VoiceInputButton = ({
28
+ isSpeaking,
29
+ onToggle,
30
+ }: {
31
+ isSpeaking: boolean;
32
+ onToggle: () => void;
33
+ }) => {
34
+ const label = isSpeaking
35
+ ? 'Stop voice typing'
36
+ : 'Start voice typing';
37
+
38
+ return (
39
+ <Button
40
+ variant={isSpeaking ? 'danger' : 'outline-secondary'}
41
+ size="sm"
42
+ onClick={onToggle}
43
+ aria-label={label}
44
+ title={label}
45
+ style={{
46
+ borderColor: isSpeaking ? undefined : '#dee2e6',
47
+ borderRadius: '0.375rem',
48
+ borderWidth: '1px',
49
+ borderStyle: 'solid',
50
+ }}
51
+ >
52
+ {isSpeaking ? <MicOffSVG /> : <MicSVG />}
53
+ </Button>
54
+ );
55
+ };
56
+
17
57
  interface ChatInputProps {
18
58
  aiChatMessage: string;
19
59
  setAIChatMessage: (message: string) => void;
@@ -39,7 +79,8 @@ const ChatInputComponent = ({
39
79
  }: ChatInputProps) => {
40
80
  const inputRef = useRef<HTMLTextAreaElement>(null);
41
81
 
42
- // Use the speech recognition hook
82
+ // Speech recognition is created lazily on first toggle, so simply
83
+ // mounting this hook does not touch any browser media APIs.
43
84
  const {
44
85
  isSpeaking,
45
86
  toggleSpeechRecognition,
@@ -186,33 +227,12 @@ const ChatInputComponent = ({
186
227
  {aiChatMessage ? 'Press Enter to send' : ''}
187
228
  </span>
188
229
  <div style={{ display: 'flex', gap: '8px' }}>
189
- <Button
190
- variant={
191
- isSpeaking ? 'danger' : 'outline-secondary'
192
- }
193
- size="sm"
194
- onClick={toggleSpeechRecognition}
195
- aria-label={
196
- isSpeaking
197
- ? 'Stop voice typing'
198
- : 'Start voice typing'
199
- }
200
- title={
201
- isSpeaking
202
- ? 'Stop voice typing'
203
- : 'Start voice typing'
204
- }
205
- style={{
206
- borderColor: isSpeaking
207
- ? undefined
208
- : '#dee2e6',
209
- borderRadius: '0.375rem',
210
- borderWidth: '1px',
211
- borderStyle: 'solid',
212
- }}
213
- >
214
- {isSpeaking ? <MicOffSVG /> : <MicSVG />}
215
- </Button>
230
+ {enableVoiceInput && (
231
+ <VoiceInputButton
232
+ isSpeaking={isSpeaking}
233
+ onToggle={toggleSpeechRecognition}
234
+ />
235
+ )}
216
236
  <Button
217
237
  variant={
218
238
  aiChatMessage.trim()
@@ -13,8 +13,6 @@ import { DiffView, DiffViewRef } from './DiffView';
13
13
  import { UnifiedFilesDiff } from '../../utils/fileDiff';
14
14
  import { enableDiffView } from '../../client/featureFlags';
15
15
 
16
- const DEBUG = true;
17
-
18
16
  interface MessageProps {
19
17
  id: string;
20
18
  role: 'user' | 'assistant';
@@ -2,6 +2,7 @@ import {
2
2
  useState,
3
3
  useEffect,
4
4
  useCallback,
5
+ useRef,
5
6
  Dispatch,
6
7
  SetStateAction,
7
8
  } from 'react';
@@ -12,18 +13,41 @@ export interface UseSpeechRecognitionResult {
12
13
  stopSpeaking: () => void;
13
14
  }
14
15
 
16
+ /**
17
+ * Speech-to-text input for the AI chat box.
18
+ *
19
+ * The underlying `SpeechRecognition` instance is created lazily, on the
20
+ * first call to `toggleSpeechRecognition`, rather than eagerly when the
21
+ * hook mounts. Eager construction can make some browsers show a
22
+ * microphone permission prompt as soon as the editor loads, before the
23
+ * user has asked for voice input.
24
+ */
15
25
  export const useSpeechRecognition = (
16
26
  onTranscriptChange: Dispatch<SetStateAction<string>>,
17
27
  ): UseSpeechRecognitionResult => {
18
28
  const [isSpeaking, setIsSpeaking] =
19
29
  useState<boolean>(false);
20
- const [recognition, setRecognition] =
21
- // @ts-ignore
22
- useState<SpeechRecognition | null>(null);
23
- const [, setFinalTranscript] = useState<string>('');
24
-
25
- // Speech recognition setup
30
+ // `SpeechRecognition` is not part of the DOM lib, so the type
31
+ // reference is suppressed here, as with the constructor lookups below.
32
+ // @ts-ignore
33
+ const recognitionRef = useRef<SpeechRecognition | null>(
34
+ null,
35
+ );
36
+
37
+ // Keep the latest callback in a ref so the lazily-created
38
+ // recognition instance always calls the current handler, without
39
+ // having to tear down and recreate the instance.
40
+ const onTranscriptChangeRef = useRef(onTranscriptChange);
26
41
  useEffect(() => {
42
+ onTranscriptChangeRef.current = onTranscriptChange;
43
+ }, [onTranscriptChange]);
44
+
45
+ // Create the SpeechRecognition instance on first use.
46
+ const getRecognition = useCallback(() => {
47
+ if (recognitionRef.current) {
48
+ return recognitionRef.current;
49
+ }
50
+
27
51
  // Check if browser supports SpeechRecognition
28
52
  const SpeechRecognition =
29
53
  // @ts-ignore
@@ -31,56 +55,52 @@ export const useSpeechRecognition = (
31
55
  // @ts-ignore
32
56
  window.webkitSpeechRecognition;
33
57
 
34
- if (SpeechRecognition) {
35
- const recognitionInstance = new SpeechRecognition();
36
- recognitionInstance.continuous = true;
37
- recognitionInstance.interimResults = true;
38
-
39
- recognitionInstance.onresult = (event) => {
40
- let finalText = '';
41
- let interimText = '';
42
-
43
- // Process all results to separate final and interim text
44
- for (let i = 0; i < event.results.length; i++) {
45
- const result = event.results[i];
46
- if (result.isFinal) {
47
- finalText += result[0].transcript;
48
- } else {
49
- interimText += result[0].transcript;
50
- }
58
+ if (!SpeechRecognition) {
59
+ return null;
60
+ }
61
+
62
+ const recognitionInstance = new SpeechRecognition();
63
+ recognitionInstance.continuous = true;
64
+ recognitionInstance.interimResults = true;
65
+
66
+ recognitionInstance.onresult = (event) => {
67
+ let finalText = '';
68
+ let interimText = '';
69
+
70
+ // Process all results to separate final and interim text
71
+ for (let i = 0; i < event.results.length; i++) {
72
+ const result = event.results[i];
73
+ if (result.isFinal) {
74
+ finalText += result[0].transcript;
75
+ } else {
76
+ interimText += result[0].transcript;
51
77
  }
78
+ }
52
79
 
53
- // Update our final transcript state
54
- setFinalTranscript(finalText);
55
-
56
- // Combine final and interim text and update the prompt
57
- const fullTranscript = finalText + interimText;
58
- onTranscriptChange(fullTranscript);
59
- };
60
-
61
- recognitionInstance.onerror = (event) => {
62
- console.error(
63
- 'Speech recognition error',
64
- event.error,
65
- );
66
- setIsSpeaking(false);
67
- };
68
-
69
- recognitionInstance.onend = () => {
70
- setIsSpeaking(false);
71
- // Reset final transcript when recognition ends
72
- setFinalTranscript('');
73
- };
74
-
75
- setRecognition(recognitionInstance);
76
-
77
- return () => {
78
- recognitionInstance.abort();
79
- };
80
- }
81
- }, [onTranscriptChange]);
80
+ // Combine final and interim text and update the prompt
81
+ onTranscriptChangeRef.current(
82
+ finalText + interimText,
83
+ );
84
+ };
85
+
86
+ recognitionInstance.onerror = (event) => {
87
+ console.error(
88
+ 'Speech recognition error',
89
+ event.error,
90
+ );
91
+ setIsSpeaking(false);
92
+ };
93
+
94
+ recognitionInstance.onend = () => {
95
+ setIsSpeaking(false);
96
+ };
97
+
98
+ recognitionRef.current = recognitionInstance;
99
+ return recognitionInstance;
100
+ }, []);
82
101
 
83
102
  const toggleSpeechRecognition = useCallback(() => {
103
+ const recognition = getRecognition();
84
104
  if (!recognition) {
85
105
  console.error(
86
106
  'Speech recognition not supported in this browser',
@@ -92,21 +112,25 @@ export const useSpeechRecognition = (
92
112
  recognition.stop();
93
113
  setIsSpeaking(false);
94
114
  } else {
95
- // Reset final transcript when starting new recognition
96
- setFinalTranscript('');
97
115
  recognition.start();
98
116
  setIsSpeaking(true);
99
117
  }
100
- }, [isSpeaking, recognition]);
118
+ }, [getRecognition, isSpeaking]);
101
119
 
102
120
  const stopSpeaking = useCallback(() => {
103
- if (recognition && isSpeaking) {
104
- recognition.stop();
121
+ if (recognitionRef.current && isSpeaking) {
122
+ recognitionRef.current.stop();
105
123
  setIsSpeaking(false);
106
- // Reset final transcript when manually stopping
107
- setFinalTranscript('');
108
124
  }
109
- }, [isSpeaking, recognition]);
125
+ }, [isSpeaking]);
126
+
127
+ // Release the microphone if the component unmounts while listening.
128
+ useEffect(
129
+ () => () => {
130
+ recognitionRef.current?.abort();
131
+ },
132
+ [],
133
+ );
110
134
 
111
135
  return {
112
136
  isSpeaking,
package/src/vite-env.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  interface ImportMetaEnv {
4
4
  readonly VITE_ENABLE_LIVEKIT: string;
5
+ readonly VITE_ENABLE_VOICE_INPUT: string;
5
6
  // Add other environment variables here as needed
6
7
  }
7
8