vzcode 2.0.0 → 2.2.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,7 +20,7 @@
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-BgqNq-ww.js"></script>
23
+ <script type="module" crossorigin src="/assets/index-CpxqGhXj.js"></script>
24
24
  <link rel="stylesheet" crossorigin href="/assets/index-BM8tDh6M.css">
25
25
  </head>
26
26
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vzcode",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Multiplayer code editor system",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -160,6 +160,7 @@
160
160
  "jszip": "^3.10.1",
161
161
  "livekit-server-sdk": "^2.13.1",
162
162
  "llm-code-format": "^3.0.0",
163
+ "lucide-react": "^0.539.0",
163
164
  "npm": "^11.5.2",
164
165
  "open": "^10.2.0",
165
166
  "prettier-plugin-svelte": "^3.4.0",
@@ -0,0 +1,35 @@
1
+ export const MicOffSVG = () => (
2
+ <svg
3
+ fill="#000000"
4
+ height="24"
5
+ width="24"
6
+ stroke="currentColor"
7
+ version="1.1"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 512 512"
10
+ enableBackground="new 0 0 512 512"
11
+ >
12
+ <g>
13
+ <g>
14
+ <path
15
+ fill="currentColor"
16
+ d="m439.5,236c0-11.3-9.1-20.4-20.4-20.4s-20.4,9.1-20.4,20.4c0,70-64,126.9-142.7,126.9-78.7,0-142.7-56.9-142.7-126.9 0-11.3-9.1-20.4-20.4-20.4s-20.4,9.1-20.4,20.4c0,86.2 71.5,157.4 163.1,166.7v57.5h-23.6c-11.3,0-20.4,9.1-20.4,20.4 0,11.3 9.1,20.4 20.4,20.4h88c11.3,0 20.4-9.1 20.4-20.4 0-11.3-9.1-20.4-20.4-20.4h-23.6v-57.5c91.6-9.3 163.1-80.5 163.1-166.7z"
17
+ />
18
+ <path
19
+ fill="currentColor"
20
+ d="m256,323.5c51,0 92.3-41.3 92.3-92.3v-127.9c0-51-41.3-92.3-92.3-92.3s-92.3,41.3-92.3,92.3v127.9c0,51 41.3,92.3 92.3,92.3zm-52.3-220.2c0-28.8 23.5-52.3 52.3-52.3s52.3,23.5 52.3,52.3v127.9c0,28.8-23.5,52.3-52.3,52.3s-52.3-23.5-52.3-52.3v-127.9z"
21
+ />
22
+ {/* Diagonal line to indicate "off" state */}
23
+ <line
24
+ x1="100"
25
+ y1="100"
26
+ x2="412"
27
+ y2="412"
28
+ stroke="currentColor"
29
+ strokeWidth="40"
30
+ strokeLinecap="round"
31
+ />
32
+ </g>
33
+ </g>
34
+ </svg>
35
+ );
@@ -21,4 +21,6 @@ export { HtmlSVG } from './HtmlSVG';
21
21
  export { CssSVG } from './CssSVG';
22
22
  export { JsonSVG } from './JsonSVG';
23
23
  export { MarkdownSVG } from './MarkdownSVG';
24
+ export { MicSVG } from './MicSVG';
25
+ export { MicOffSVG } from './MicOffSVG';
24
26
  export { AdjustmentSVG } from './AdjustmentSVG';
@@ -11,6 +11,8 @@ import {
11
11
  ToggleButton,
12
12
  } from '../../bootstrap';
13
13
  import { enableAskMode } from '../../featureFlags';
14
+ import { useSpeechRecognition } from './useSpeechRecognition';
15
+ import { MicSVG, MicOffSVG } from '../../Icons';
14
16
 
15
17
  interface ChatInputProps {
16
18
  aiChatMessage: string;
@@ -39,6 +41,13 @@ const ChatInputComponent = ({
39
41
  }: ChatInputProps) => {
40
42
  const inputRef = useRef<HTMLTextAreaElement>(null);
41
43
 
44
+ // Use the speech recognition hook
45
+ const {
46
+ isSpeaking,
47
+ toggleSpeechRecognition,
48
+ stopSpeaking,
49
+ } = useSpeechRecognition(setAIChatMessage);
50
+
42
51
  useEffect(() => {
43
52
  // Focus the input when the AI chat is focused
44
53
  if (focused && inputRef.current) {
@@ -92,8 +101,12 @@ const ChatInputComponent = ({
92
101
  );
93
102
 
94
103
  const handleSendClick = useCallback(() => {
104
+ // If speech recognition is active, stop it
105
+ if (isSpeaking) {
106
+ stopSpeaking();
107
+ }
95
108
  onSendMessage();
96
- }, [onSendMessage]);
109
+ }, [onSendMessage, isSpeaking, stopSpeaking]);
97
110
 
98
111
  const handleChange = useCallback(
99
112
  (event: React.ChangeEvent<HTMLTextAreaElement>) => {
@@ -173,20 +186,50 @@ const ChatInputComponent = ({
173
186
  <span className="ai-chat-hint">
174
187
  {aiChatMessage ? 'Press Enter to send' : ''}
175
188
  </span>
176
- <Button
177
- variant={
178
- aiChatMessage.trim()
179
- ? 'primary'
180
- : 'outline-secondary'
181
- }
182
- onClick={handleSendClick}
183
- disabled={!aiChatMessage.trim() || isLoading}
184
- className="ai-chat-send-button"
185
- aria-label="Send message"
186
- title="Send message (Enter)"
187
- >
188
- Send
189
- </Button>
189
+ <div style={{ display: 'flex', gap: '8px' }}>
190
+ <Button
191
+ variant={
192
+ isSpeaking ? 'danger' : 'outline-secondary'
193
+ }
194
+ size="sm"
195
+ onClick={toggleSpeechRecognition}
196
+ disabled={isLoading}
197
+ aria-label={
198
+ isSpeaking
199
+ ? 'Stop voice typing'
200
+ : 'Start voice typing'
201
+ }
202
+ title={
203
+ isSpeaking
204
+ ? 'Stop voice typing'
205
+ : 'Start voice typing'
206
+ }
207
+ style={{
208
+ borderColor: isSpeaking
209
+ ? undefined
210
+ : '#dee2e6',
211
+ borderRadius: '0.375rem',
212
+ borderWidth: '1px',
213
+ borderStyle: 'solid',
214
+ }}
215
+ >
216
+ {isSpeaking ? <MicOffSVG /> : <MicSVG />}
217
+ </Button>
218
+ <Button
219
+ variant={
220
+ aiChatMessage.trim()
221
+ ? 'primary'
222
+ : 'outline-secondary'
223
+ }
224
+ onClick={handleSendClick}
225
+ disabled={!aiChatMessage.trim() || isLoading}
226
+ className="ai-chat-send-button"
227
+ aria-label="Send message"
228
+ title="Send message (Enter)"
229
+ >
230
+ Send
231
+ </Button>
232
+ </div>
190
233
  </div>
191
234
  </Form.Group>
192
235
  </div>
@@ -0,0 +1,117 @@
1
+ import {
2
+ useState,
3
+ useEffect,
4
+ useCallback,
5
+ Dispatch,
6
+ SetStateAction,
7
+ } from 'react';
8
+
9
+ export interface UseSpeechRecognitionResult {
10
+ isSpeaking: boolean;
11
+ toggleSpeechRecognition: () => void;
12
+ stopSpeaking: () => void;
13
+ }
14
+
15
+ export const useSpeechRecognition = (
16
+ onTranscriptChange: Dispatch<SetStateAction<string>>,
17
+ ): UseSpeechRecognitionResult => {
18
+ const [isSpeaking, setIsSpeaking] =
19
+ useState<boolean>(false);
20
+ const [recognition, setRecognition] =
21
+ // @ts-ignore
22
+ useState<SpeechRecognition | null>(null);
23
+ const [finalTranscript, setFinalTranscript] =
24
+ useState<string>('');
25
+
26
+ // Speech recognition setup
27
+ useEffect(() => {
28
+ // Check if browser supports SpeechRecognition
29
+ const SpeechRecognition =
30
+ // @ts-ignore
31
+ window.SpeechRecognition ||
32
+ // @ts-ignore
33
+ window.webkitSpeechRecognition;
34
+
35
+ if (SpeechRecognition) {
36
+ const recognitionInstance = new SpeechRecognition();
37
+ recognitionInstance.continuous = true;
38
+ recognitionInstance.interimResults = true;
39
+
40
+ recognitionInstance.onresult = (event) => {
41
+ let finalText = '';
42
+ let interimText = '';
43
+
44
+ // Process all results to separate final and interim text
45
+ for (let i = 0; i < event.results.length; i++) {
46
+ const result = event.results[i];
47
+ if (result.isFinal) {
48
+ finalText += result[0].transcript;
49
+ } else {
50
+ interimText += result[0].transcript;
51
+ }
52
+ }
53
+
54
+ // Update our final transcript state
55
+ setFinalTranscript(finalText);
56
+
57
+ // Combine final and interim text and update the prompt
58
+ const fullTranscript = finalText + interimText;
59
+ onTranscriptChange(fullTranscript);
60
+ };
61
+
62
+ recognitionInstance.onerror = (event) => {
63
+ console.error(
64
+ 'Speech recognition error',
65
+ event.error,
66
+ );
67
+ setIsSpeaking(false);
68
+ };
69
+
70
+ recognitionInstance.onend = () => {
71
+ setIsSpeaking(false);
72
+ // Reset final transcript when recognition ends
73
+ setFinalTranscript('');
74
+ };
75
+
76
+ setRecognition(recognitionInstance);
77
+
78
+ return () => {
79
+ recognitionInstance.abort();
80
+ };
81
+ }
82
+ }, [onTranscriptChange]);
83
+
84
+ const toggleSpeechRecognition = useCallback(() => {
85
+ if (!recognition) {
86
+ console.error(
87
+ 'Speech recognition not supported in this browser',
88
+ );
89
+ return;
90
+ }
91
+
92
+ if (isSpeaking) {
93
+ recognition.stop();
94
+ setIsSpeaking(false);
95
+ } else {
96
+ // Reset final transcript when starting new recognition
97
+ setFinalTranscript('');
98
+ recognition.start();
99
+ setIsSpeaking(true);
100
+ }
101
+ }, [isSpeaking, recognition]);
102
+
103
+ const stopSpeaking = useCallback(() => {
104
+ if (recognition && isSpeaking) {
105
+ recognition.stop();
106
+ setIsSpeaking(false);
107
+ // Reset final transcript when manually stopping
108
+ setFinalTranscript('');
109
+ }
110
+ }, [isSpeaking, recognition]);
111
+
112
+ return {
113
+ isSpeaking,
114
+ toggleSpeechRecognition,
115
+ stopSpeaking,
116
+ };
117
+ };
@@ -12,13 +12,36 @@ import { EmptyState } from './EmptyState';
12
12
 
13
13
  const CONFIG_FILE_NAME = 'config.json';
14
14
 
15
+ // Helper function to calculate percentage for progress fill
16
+ const calculatePercentage = (
17
+ value: number,
18
+ min: number,
19
+ max: number,
20
+ ) => {
21
+ return ((value - min) / (max - min)) * 100;
22
+ };
23
+
24
+ // Helper function to format values nicely
25
+ const formatValue = (
26
+ value: number,
27
+ min: number,
28
+ max: number,
29
+ ) => {
30
+ // Determine decimal places based on the range
31
+ const range = max - min;
32
+ const decimalPlaces =
33
+ range < 10 ? 2 : range < 100 ? 1 : 0;
34
+ return Number(value).toFixed(decimalPlaces);
35
+ };
36
+
15
37
  export const VisualEditor = () => {
16
- const {
17
- files,
18
- submitOperation,
19
- runPrettierRef,
20
- iframeRef,
21
- } = useContext(VZCodeContext);
38
+ const { files, submitOperation, iframeRef } =
39
+ useContext(VZCodeContext);
40
+
41
+ // Local state to track slider values during user interaction
42
+ const [localValues, setLocalValues] = useState<{
43
+ [key: string]: number;
44
+ }>({});
22
45
 
23
46
  let configFileId: VizFileId | null = null;
24
47
  for (const fileId in files) {
@@ -70,22 +93,23 @@ export const VisualEditor = () => {
70
93
  );
71
94
  }
72
95
 
73
- const onInputUpdate = useCallback(
74
- (
75
- property: string,
76
- previousValue: any,
77
- ): React.FormEventHandler<HTMLInputElement> =>
78
- (event) => {
79
- //TODO: test race condition in which someone is editing the config file as another user uses the visual editor
96
+ const onSliderChange = useCallback(
97
+ (property: string) =>
98
+ (event: React.ChangeEvent<HTMLInputElement>) => {
99
+ const newValue = parseFloat(
100
+ event.currentTarget.value,
101
+ );
80
102
 
81
- const newValueOfConsistentType =
82
- typeof previousValue === 'number'
83
- ? parseFloat(event.currentTarget.value)
84
- : event.currentTarget.value;
103
+ // Update local state immediately for responsive UI
104
+ setLocalValues((prev) => ({
105
+ ...prev,
106
+ [property]: newValue,
107
+ }));
85
108
 
109
+ // Update config.json
86
110
  const newConfigData = {
87
111
  ...configData,
88
- [property]: newValueOfConsistentType,
112
+ [property]: newValue,
89
113
  };
90
114
 
91
115
  submitOperation((document: VizContent) => ({
@@ -98,140 +122,104 @@ export const VisualEditor = () => {
98
122
  },
99
123
  },
100
124
  }));
101
-
102
- iframeRef.current.contentWindow.postMessage({
103
- [property]: newValueOfConsistentType,
104
- });
105
125
  },
106
- [configData, files, configFileId],
126
+ [configData, files, configFileId, setLocalValues],
107
127
  );
108
128
 
109
129
  const visualEditorWidgets: VisualEditorConfigEntry[] =
110
130
  configData.visualEditorWidgets;
111
131
 
112
- // State for advanced interactions
113
- const [activeSlider, setActiveSlider] = useState<
114
- string | null
115
- >(null);
116
- const [isDragging, setIsDragging] = useState<
117
- string | null
118
- >(null);
119
- const [hoverValue, setHoverValue] = useState<
120
- number | null
121
- >(null);
122
- const sliderRefs = useRef<{
123
- [key: string]: HTMLInputElement | null;
124
- }>({});
125
-
126
- // Helper function to format values nicely
127
- const formatValue = (
128
- value: number,
129
- min: number,
130
- max: number,
131
- ) => {
132
- // Determine decimal places based on the range
133
- const range = max - min;
134
- const decimalPlaces =
135
- range < 10 ? 2 : range < 100 ? 1 : 0;
136
- return Number(value).toFixed(decimalPlaces);
137
- };
138
-
139
- // Helper function to calculate percentage for progress fill
140
- const calculatePercentage = (
141
- value: number,
142
- min: number,
143
- max: number,
144
- ) => {
145
- return ((value - min) / (max - min)) * 100;
146
- };
147
-
148
- // Enhanced input handler with animations
149
- const onInputUpdateEnhanced = useCallback(
150
- (
151
- property: string,
152
- previousValue: any,
153
- widgetConfig: VisualEditorConfigEntry,
154
- ): React.FormEventHandler<HTMLInputElement> =>
155
- (event) => {
156
- const newValueOfConsistentType =
157
- typeof previousValue === 'number'
158
- ? parseFloat(event.currentTarget.value)
159
- : event.currentTarget.value;
132
+ // Sync local values with config data when it changes (including remote updates)
133
+ useEffect(() => {
134
+ const newLocalValues: { [key: string]: number } = {};
135
+ visualEditorWidgets.forEach((widget) => {
136
+ if (widget.type === 'number') {
137
+ newLocalValues[widget.property] =
138
+ configData[widget.property];
139
+ }
140
+ });
141
+ setLocalValues(newLocalValues);
142
+ }, [configData, visualEditorWidgets]);
160
143
 
161
- const newConfigData = {
162
- ...configData,
163
- [property]: newValueOfConsistentType,
164
- };
144
+ // Track previous config state to detect changes from any source (remote clients, text editor, etc.)
145
+ const previousConfigRef = useRef<any>(null);
165
146
 
166
- submitOperation((document: VizContent) => ({
167
- ...document,
168
- files: {
169
- ...files,
170
- [configFileId]: {
171
- name: 'config.json',
172
- text: JSON.stringify(newConfigData, null, 2),
173
- },
174
- },
175
- }));
147
+ // Detect config.json changes and send updates to iframe
148
+ useEffect(() => {
149
+ if (!configFileId || !files || !files[configFileId]) {
150
+ return;
151
+ }
176
152
 
177
- iframeRef.current.contentWindow.postMessage({
178
- [property]: newValueOfConsistentType,
179
- });
153
+ let newConfigData;
154
+ try {
155
+ newConfigData = JSON.parse(files[configFileId].text);
156
+ } catch (error) {
157
+ // If config is invalid JSON, we can't process changes
158
+ return;
159
+ }
180
160
 
181
- // Trigger value change animation
182
- const sliderElement = sliderRefs.current[property];
183
- if (sliderElement) {
184
- sliderElement.classList.add('value-changed');
185
- setTimeout(() => {
186
- sliderElement.classList.remove('value-changed');
187
- }, 300);
188
- }
189
- },
190
- [configData, files, configFileId],
191
- );
161
+ const previousConfig = previousConfigRef.current;
192
162
 
193
- // Mouse event handlers for enhanced interactions
194
- const handleMouseDown = (property: string) => {
195
- setIsDragging(property);
196
- setActiveSlider(property);
197
- };
163
+ // Update the ref with the new config
164
+ previousConfigRef.current = newConfigData;
198
165
 
199
- const handleMouseUp = () => {
200
- setIsDragging(null);
201
- };
166
+ // Skip processing if this is the first time or if there's no previous config
167
+ if (!previousConfig) {
168
+ return;
169
+ }
202
170
 
203
- const handleMouseEnter = (property: string) => {
204
- setActiveSlider(property);
205
- };
171
+ // Find changed top-level properties
172
+ const changedProperties: { [key: string]: any } = {};
206
173
 
207
- const handleMouseLeave = () => {
208
- setActiveSlider(null);
209
- setHoverValue(null);
210
- };
174
+ // Check all properties in the new config
175
+ for (const key in newConfigData) {
176
+ if (newConfigData[key] !== previousConfig[key]) {
177
+ // Deep comparison for objects to detect actual changes
178
+ if (
179
+ typeof newConfigData[key] === 'object' &&
180
+ typeof previousConfig[key] === 'object'
181
+ ) {
182
+ if (
183
+ JSON.stringify(newConfigData[key]) !==
184
+ JSON.stringify(previousConfig[key])
185
+ ) {
186
+ changedProperties[key] = newConfigData[key];
187
+ }
188
+ } else {
189
+ changedProperties[key] = newConfigData[key];
190
+ }
191
+ }
192
+ }
211
193
 
212
- // Add global mouse up listener
213
- useEffect(() => {
214
- const handleGlobalMouseUp = () => {
215
- setIsDragging(null);
216
- };
194
+ // Check for deleted properties (properties that existed before but don't exist now)
195
+ for (const key in previousConfig) {
196
+ if (!(key in newConfigData)) {
197
+ changedProperties[key] = undefined;
198
+ }
199
+ }
217
200
 
218
- document.addEventListener(
219
- 'mouseup',
220
- handleGlobalMouseUp,
221
- );
222
- return () => {
223
- document.removeEventListener(
224
- 'mouseup',
225
- handleGlobalMouseUp,
226
- );
227
- };
228
- }, []);
201
+ // Send changed properties to iframe if any changes were detected
202
+ if (Object.keys(changedProperties).length > 0) {
203
+ try {
204
+ iframeRef.current.contentWindow.postMessage(
205
+ changedProperties,
206
+ );
207
+ } catch (error) {
208
+ console.error(
209
+ 'Failed to send config changes to iframe:',
210
+ error,
211
+ );
212
+ }
213
+ }
214
+ }, [files, configFileId, iframeRef]);
229
215
 
230
216
  return (
231
217
  <div className="visual-editor">
232
218
  {visualEditorWidgets.map((widgetConfig, index) => {
233
219
  if (widgetConfig.type === 'number') {
220
+ // Use local value if available, otherwise fall back to config value
234
221
  const currentValue =
222
+ localValues[widgetConfig.property] ??
235
223
  configData[widgetConfig.property];
236
224
  const percentage = calculatePercentage(
237
225
  currentValue,
@@ -267,11 +255,10 @@ export const VisualEditor = () => {
267
255
  min={widgetConfig.min}
268
256
  max={widgetConfig.max}
269
257
  step="any"
270
- onInput={onInputUpdate(
258
+ value={currentValue}
259
+ onChange={onSliderChange(
271
260
  widgetConfig.property,
272
- configData[widgetConfig.property],
273
261
  )}
274
- defaultValue={currentValue}
275
262
  />
276
263
  <div
277
264
  className="slider-track-fill"