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/assets/{index-BgqNq-ww.js → index-CpxqGhXj.js} +157 -157
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/client/Icons/MicOffSVG.tsx +35 -0
- package/src/client/Icons/index.tsx +2 -0
- package/src/client/VZSidebar/AIChat/ChatInput.tsx +58 -15
- package/src/client/VZSidebar/AIChat/useSpeechRecognition.ts +117 -0
- package/src/client/VZSidebar/VisualEditor.tsx +120 -133
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-
|
|
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.
|
|
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
|
-
<
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
? '
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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]:
|
|
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
|
-
//
|
|
113
|
-
|
|
114
|
-
string
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
setIsDragging(property);
|
|
196
|
-
setActiveSlider(property);
|
|
197
|
-
};
|
|
163
|
+
// Update the ref with the new config
|
|
164
|
+
previousConfigRef.current = newConfigData;
|
|
198
165
|
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
171
|
+
// Find changed top-level properties
|
|
172
|
+
const changedProperties: { [key: string]: any } = {};
|
|
206
173
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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"
|