vzcode 2.29.0 → 2.30.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.
package/dist/index.html CHANGED
@@ -20,8 +20,8 @@
20
20
  href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
21
21
  rel="stylesheet"
22
22
  />
23
- <script type="module" crossorigin src="/assets/index-JcHUw7hN.js"></script>
24
- <link rel="stylesheet" crossorigin href="/assets/index-CCmGeE_1.css">
23
+ <script type="module" crossorigin src="/assets/index-CD0x02Rq.js"></script>
24
+ <link rel="stylesheet" crossorigin href="/assets/index-Dyt9tpmu.css">
25
25
  </head>
26
26
  <body>
27
27
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "2.29.0",
3
+ "version": "2.30.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -26,6 +26,7 @@ import {
26
26
  import './style.scss';
27
27
  import { useShareDB } from './useShareDB';
28
28
  import { useESLint } from '../useESLint';
29
+ import { enableLiveKit } from '../featureFlags';
29
30
 
30
31
  // Instantiate the Prettier worker.
31
32
  const prettierWorker = new PrettierWorker();
@@ -99,6 +100,29 @@ function App() {
99
100
  // Clicking on it accepts it but should not.
100
101
  const enableCopilot = false;
101
102
 
103
+ // The editor UI itself. Rendered with or without the LiveKit
104
+ // provider around it, so that LiveKit (and its audio capture) is
105
+ // only mounted when explicitly enabled.
106
+ const appContent = (
107
+ <>
108
+ <div className="app">
109
+ <VZLeft />
110
+ <VZMiddle
111
+ aiCopilotEndpoint={
112
+ enableCopilot ? '/ai-copilot' : null
113
+ }
114
+ esLintSource={esLintSource}
115
+ />
116
+ {enableRightPanel ? <VZRight /> : null}
117
+ <VZResizer side="left" />
118
+ {enableRightPanel ? (
119
+ <VZResizer side="right" />
120
+ ) : null}
121
+ </div>
122
+ <PersistUsername />
123
+ </>
124
+ );
125
+
102
126
  return (
103
127
  <SplitPaneResizeProvider>
104
128
  <VZCodeProvider
@@ -120,30 +144,20 @@ function App() {
120
144
  aiChatEndpoint="/ai-chat-message"
121
145
  aiChatOptions={{}}
122
146
  >
123
- <LiveKitRoom
124
- audio={true}
125
- token={liveKitToken}
126
- serverUrl={serverUrl}
127
- connect={liveKitConnection}
128
- style={{ height: '100%' }}
129
- >
130
- <div className="app">
131
- <VZLeft />
132
- <VZMiddle
133
- aiCopilotEndpoint={
134
- enableCopilot ? '/ai-copilot' : null
135
- }
136
- esLintSource={esLintSource}
137
- />
138
- {enableRightPanel ? <VZRight /> : null}
139
- <VZResizer side="left" />
140
- {enableRightPanel ? (
141
- <VZResizer side="right" />
142
- ) : null}
143
- </div>
144
- <PersistUsername />
145
- <RoomAudioRenderer />
146
- </LiveKitRoom>
147
+ {enableLiveKit ? (
148
+ <LiveKitRoom
149
+ audio={true}
150
+ token={liveKitToken}
151
+ serverUrl={serverUrl}
152
+ connect={liveKitConnection}
153
+ style={{ height: '100%' }}
154
+ >
155
+ {appContent}
156
+ <RoomAudioRenderer />
157
+ </LiveKitRoom>
158
+ ) : (
159
+ appContent
160
+ )}
147
161
  </VZCodeProvider>
148
162
  </SplitPaneResizeProvider>
149
163
  );
@@ -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;
@@ -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()
@@ -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