vzcode 1.43.0 → 1.45.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 (38) hide show
  1. package/dist/assets/bindings_wasm_bg-CH5ekNIA.wasm +0 -0
  2. package/dist/assets/buildWorker-C_tGhWXG.js +682 -0
  3. package/dist/assets/index-C1lTrzOZ.js +457 -0
  4. package/dist/assets/{index-CP9o_hN5.css → index-DeeOcIzw.css} +1 -1
  5. package/dist/assets/worker-CekYspLa.js +453 -0
  6. package/dist/index.html +2 -2
  7. package/package.json +20 -18
  8. package/src/client/CodeEditor/{getOrCreateEditor.ts → getOrCreateEditor.tsx} +129 -1
  9. package/src/client/CodeEditor/index.tsx +4 -0
  10. package/src/client/Icons/SparklesSVG.tsx +9 -3
  11. package/src/client/ImageViewer/index.tsx +91 -0
  12. package/src/client/ImageViewer/style.scss +41 -0
  13. package/src/client/VZCodeContext.tsx +34 -6
  14. package/src/client/VZMiddle.tsx +22 -7
  15. package/src/client/VZRight.tsx +7 -2
  16. package/src/client/VZSidebar/AIChat/ChatInput.tsx +8 -8
  17. package/src/client/VZSidebar/AIChat/DiffView.scss +125 -0
  18. package/src/client/VZSidebar/AIChat/DiffView.tsx +72 -0
  19. package/src/client/VZSidebar/AIChat/Message.tsx +11 -0
  20. package/src/client/VZSidebar/AIChat/MessageList.tsx +1 -0
  21. package/src/client/VZSidebar/AIChat/index.tsx +9 -7
  22. package/src/client/VZSidebar/aiCopyPaste.ts +64 -1
  23. package/src/client/VZSidebar/index.tsx +18 -1
  24. package/src/client/VZSidebar/styles.scss +9 -0
  25. package/src/client/VZSidebar/useDragAndDrop.tsx +133 -8
  26. package/src/client/featureFlags.ts +2 -0
  27. package/src/client/useRuntimeError.ts +32 -0
  28. package/src/client/utils/isImageFile.ts +12 -0
  29. package/src/server/aiChatHandler/aiEditing.ts +19 -0
  30. package/src/server/aiChatHandler/chatOperations.ts +40 -0
  31. package/src/server/aiChatHandler/index.ts +13 -0
  32. package/src/server/computeInitialDocument.ts +25 -6
  33. package/src/server/index.ts +31 -2
  34. package/src/utils/fileDiff.ts +114 -0
  35. package/dist/assets/bindings_wasm_bg-BQfW5T_2.wasm +0 -0
  36. package/dist/assets/buildWorker-B-vXZ9gA.js +0 -579
  37. package/dist/assets/index-B83lKfVG.js +0 -329
  38. package/dist/assets/worker-DkYJN5WQ.js +0 -453
@@ -0,0 +1,125 @@
1
+ .diff-view {
2
+ margin: 12px 0;
3
+
4
+ .diff-summary {
5
+ margin-bottom: 8px;
6
+
7
+ .diff-stats {
8
+ display: flex;
9
+ align-items: center;
10
+ gap: 8px;
11
+ font-size: 12px;
12
+ color: var(--bs-secondary-color);
13
+
14
+ .files-changed {
15
+ color: white;
16
+ font-weight: 600;
17
+ }
18
+
19
+ .additions {
20
+ color: #28a745;
21
+ font-weight: 600;
22
+ }
23
+
24
+ .deletions {
25
+ color: #dc3545;
26
+ font-weight: 600;
27
+ }
28
+ }
29
+ }
30
+
31
+ .diff-files {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: 4px;
35
+
36
+ // Fix diff2html styling issues for dark theme and layout
37
+ .d2h-wrapper {
38
+ // Override diff2html variables for dark theme
39
+ --d2h-bg-color: rgb(41, 44, 52);
40
+ --d2h-color: #e6edf3;
41
+ --d2h-border-color: #30363d;
42
+ --d2h-dim-color: #6e7681;
43
+ --d2h-info-bg-color: #161b22;
44
+ --d2h-file-header-bg-color: #161b22;
45
+ --d2h-file-header-border-color: #30363d;
46
+ --d2h-line-border-color: #21262d;
47
+ --d2h-ins-bg-color: rgba(46, 160, 67, 0.15);
48
+ --d2h-ins-border-color: rgba(46, 160, 67, 0.4);
49
+ --d2h-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
50
+ --d2h-ins-label-color: #3fb950;
51
+ --d2h-del-bg-color: rgba(248, 81, 73, 0.1);
52
+ --d2h-del-border-color: rgba(248, 81, 73, 0.4);
53
+ --d2h-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
54
+ --d2h-del-label-color: #f85149;
55
+
56
+ background-color: var(--d2h-bg-color);
57
+ color: var(--d2h-color);
58
+ border-radius: 6px;
59
+ overflow: hidden;
60
+ }
61
+
62
+ // Fix layout issues with line numbers
63
+ .d2h-diff-table {
64
+ position: relative;
65
+ background-color: var(--d2h-bg-color);
66
+ }
67
+
68
+ .d2h-code-linenumber {
69
+ position: relative !important;
70
+ float: none !important;
71
+ display: table-cell !important;
72
+ width: auto !important;
73
+ background-color: var(--d2h-bg-color) !important;
74
+ border-color: var(--d2h-line-border-color) !important;
75
+ color: var(--d2h-dim-color) !important;
76
+ }
77
+
78
+ .d2h-code-side-linenumber {
79
+ position: relative !important;
80
+ float: none !important;
81
+ display: table-cell !important;
82
+ width: auto !important;
83
+ background-color: var(--d2h-bg-color) !important;
84
+ border-color: var(--d2h-line-border-color) !important;
85
+ color: var(--d2h-dim-color) !important;
86
+ }
87
+
88
+ // Ensure proper table layout
89
+ .d2h-diff-tbody tr {
90
+ display: table-row;
91
+ }
92
+
93
+ .d2h-diff-tbody td {
94
+ display: table-cell;
95
+ vertical-align: top;
96
+ }
97
+
98
+ // Fix file header styling
99
+ .d2h-file-header {
100
+ background-color: var(
101
+ --d2h-file-header-bg-color
102
+ ) !important;
103
+ border-bottom: 1px solid
104
+ var(--d2h-file-header-border-color) !important;
105
+ color: var(--d2h-color);
106
+ }
107
+
108
+ // Ensure proper line styling
109
+ .d2h-del {
110
+ background-color: var(--d2h-del-bg-color) !important;
111
+ border-color: var(--d2h-del-border-color) !important;
112
+ }
113
+
114
+ .d2h-ins {
115
+ background-color: var(--d2h-ins-bg-color) !important;
116
+ border-color: var(--d2h-ins-border-color) !important;
117
+ }
118
+
119
+ // Fix wrapper border
120
+ .d2h-file-wrapper {
121
+ border: 1px solid var(--d2h-border-color) !important;
122
+ background-color: var(--d2h-bg-color);
123
+ }
124
+ }
125
+ }
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import {
3
+ UnifiedFilesDiff,
4
+ parseUnifiedDiffStats,
5
+ combineUnifiedDiffs,
6
+ } from '../../../utils/fileDiff';
7
+ import * as Diff2Html from 'diff2html';
8
+ import 'diff2html/bundles/css/diff2html.min.css';
9
+ import './DiffView.scss';
10
+
11
+ interface DiffViewProps {
12
+ diffData: UnifiedFilesDiff;
13
+ }
14
+
15
+ export const DiffView: React.FC<DiffViewProps> = ({
16
+ diffData,
17
+ }) => {
18
+ const unifiedDiffs = Object.values(diffData).filter(
19
+ (diff) => diff.length > 0,
20
+ );
21
+
22
+ if (unifiedDiffs.length === 0) {
23
+ return null;
24
+ }
25
+
26
+ // Calculate statistics from all unified diffs
27
+ let totalAdditions = 0;
28
+ let totalDeletions = 0;
29
+
30
+ for (const unifiedDiff of unifiedDiffs) {
31
+ const stats = parseUnifiedDiffStats(unifiedDiff);
32
+ totalAdditions += stats.additions;
33
+ totalDeletions += stats.deletions;
34
+ }
35
+
36
+ // Combine all unified diffs and convert to HTML using diff2html
37
+ const combinedUnifiedDiff = combineUnifiedDiffs(diffData);
38
+ const diffHtml = Diff2Html.html(combinedUnifiedDiff, {
39
+ drawFileList: false,
40
+ matching: 'words',
41
+ diffStyle: 'word',
42
+ outputFormat: 'line-by-line',
43
+ });
44
+
45
+ return (
46
+ <div className="diff-view">
47
+ <div className="diff-summary">
48
+ <div className="diff-stats">
49
+ <span className="files-changed">
50
+ {unifiedDiffs.length} file
51
+ {unifiedDiffs.length !== 1 ? 's' : ''} changed
52
+ </span>
53
+ {totalAdditions > 0 && (
54
+ <span className="additions">
55
+ +{totalAdditions}
56
+ </span>
57
+ )}
58
+ {totalDeletions > 0 && (
59
+ <span className="deletions">
60
+ -{totalDeletions}
61
+ </span>
62
+ )}
63
+ </div>
64
+ </div>
65
+
66
+ <div
67
+ className="diff-files"
68
+ dangerouslySetInnerHTML={{ __html: diffHtml }}
69
+ />
70
+ </div>
71
+ );
72
+ };
@@ -2,6 +2,9 @@ import { timestampToDate } from '@vizhub/viz-utils';
2
2
  import Markdown from 'react-markdown';
3
3
  import remarkGfm from 'remark-gfm';
4
4
  import { useMemo, memo } from 'react';
5
+ import { DiffView } from './DiffView';
6
+ import { UnifiedFilesDiff } from '../../../utils/fileDiff';
7
+ import { enableDiffView } from '../../featureFlags';
5
8
 
6
9
  interface MessageProps {
7
10
  id: string;
@@ -9,6 +12,7 @@ interface MessageProps {
9
12
  content: string;
10
13
  timestamp: number;
11
14
  isStreaming?: boolean;
15
+ diffData?: UnifiedFilesDiff;
12
16
  }
13
17
 
14
18
  const MessageComponent = ({
@@ -16,6 +20,7 @@ const MessageComponent = ({
16
20
  content,
17
21
  timestamp,
18
22
  isStreaming,
23
+ diffData,
19
24
  }: MessageProps) => {
20
25
  // Memoize date formatting to avoid repeated computation
21
26
  const formattedTime = useMemo(() => {
@@ -32,12 +37,18 @@ const MessageComponent = ({
32
37
  const messageClassName = useMemo(() => {
33
38
  return `ai-chat-message ${role}${isStreaming ? ' streaming' : ''}`;
34
39
  }, [role, isStreaming]);
40
+
35
41
  return (
36
42
  <div className={messageClassName}>
37
43
  <div className="ai-chat-message-content">
38
44
  <Markdown remarkPlugins={[remarkGfm]}>
39
45
  {content}
40
46
  </Markdown>
47
+ {enableDiffView &&
48
+ diffData &&
49
+ Object.keys(diffData).length > 0 && (
50
+ <DiffView diffData={diffData} />
51
+ )}
41
52
  </div>
42
53
  <div className="ai-chat-message-time">
43
54
  {formattedTime}
@@ -48,6 +48,7 @@ const MessageListComponent = ({
48
48
  role={msg.role}
49
49
  content={msg.content}
50
50
  timestamp={msg.timestamp}
51
+ diffData={(msg as any).diffData}
51
52
  />
52
53
  ))}
53
54
 
@@ -13,7 +13,9 @@ import './styles.scss';
13
13
  const defaultAIChatEndpoint = '/ai-chat-message';
14
14
 
15
15
  export const AIChat = () => {
16
- const [message, setMessage] = useState('');
16
+ const { aiChatMessage, setAIChatMessage } =
17
+ useContext(VZCodeContext);
18
+
17
19
  const [isLoading, setIsLoading] = useState(false);
18
20
  const [currentChatId] = useState(() => uuidv4());
19
21
 
@@ -40,9 +42,9 @@ export const AIChat = () => {
40
42
  );
41
43
 
42
44
  const handleSendMessage = useCallback(async () => {
43
- if (!message.trim() || isLoading) return;
45
+ if (!aiChatMessage.trim() || isLoading) return;
44
46
 
45
- setMessage('');
47
+ setAIChatMessage('');
46
48
  setIsLoading(true);
47
49
 
48
50
  // Call backend endpoint for AI response
@@ -55,7 +57,7 @@ export const AIChat = () => {
55
57
  },
56
58
  body: JSON.stringify({
57
59
  ...aiChatOptions,
58
- content: message.trim(),
60
+ content: aiChatMessage.trim(),
59
61
  chatId: currentChatId,
60
62
  }),
61
63
  });
@@ -75,7 +77,7 @@ export const AIChat = () => {
75
77
  setIsLoading(false);
76
78
  }
77
79
  }, [
78
- message,
80
+ aiChatMessage,
79
81
  isLoading,
80
82
  aiChatEndpoint,
81
83
  aiChatOptions,
@@ -90,8 +92,8 @@ export const AIChat = () => {
90
92
  isLoading={isLoading}
91
93
  />
92
94
  <ChatInput
93
- message={message}
94
- setMessage={setMessage}
95
+ aiChatMessage={aiChatMessage}
96
+ setAIChatMessage={setAIChatMessage}
95
97
  onSendMessage={handleSendMessage}
96
98
  isLoading={isLoading}
97
99
  focused={aiChatFocused}
@@ -8,12 +8,14 @@ import {
8
8
  } from 'editcodewithai';
9
9
  import { VizFiles } from '@vizhub/viz-types';
10
10
  import { vizFilesToFileCollection } from '@vizhub/viz-utils';
11
+ import JSZip from 'jszip';
11
12
 
12
13
  export const createAICopyPasteHandlers = (
13
14
  files: VizFiles,
14
15
  submitOperation: any,
15
16
  setCopyButtonText: (text: string) => void,
16
17
  setPasteButtonText: (text: string) => void,
18
+ setExportButtonText: (text: string) => void,
17
19
  ) => {
18
20
  // Copy for AI - formats all files and copies to clipboard
19
21
  const handleCopyForAI = async () => {
@@ -79,8 +81,16 @@ export const createAICopyPasteHandlers = (
79
81
  return;
80
82
  }
81
83
 
84
+ // Preprocess to remove formatting instructions section to avoid creating extra files
85
+ const preprocessed = normalized
86
+ .split('## Formatting Instructions')[0]
87
+ .trim();
88
+
82
89
  // Parse the markdown files format
83
- const parsed = parseMarkdownFiles(normalized, 'bold');
90
+ const parsed = parseMarkdownFiles(
91
+ preprocessed,
92
+ 'bold',
93
+ );
84
94
 
85
95
  if (
86
96
  parsed.files &&
@@ -124,8 +134,61 @@ export const createAICopyPasteHandlers = (
124
134
  }
125
135
  };
126
136
 
137
+ // Export to ZIP - creates and downloads a ZIP file with all viz files
138
+ const handleExportToZip = async () => {
139
+ if (!files || Object.keys(files).length === 0) {
140
+ setExportButtonText('No files to export');
141
+ setTimeout(
142
+ () => setExportButtonText('Export to ZIP'),
143
+ 2000,
144
+ );
145
+ return;
146
+ }
147
+
148
+ try {
149
+ // Create the ZIP
150
+ const zip = new JSZip();
151
+
152
+ // Add each file to the ZIP
153
+ Object.entries(files).forEach(([fileId, file]) => {
154
+ const fileName = file.name || fileId;
155
+ const fileContent = file.text || '';
156
+ zip.file(fileName, fileContent);
157
+ });
158
+
159
+ // Generate and download
160
+ const blob = await zip.generateAsync({
161
+ type: 'blob',
162
+ });
163
+ const url = URL.createObjectURL(blob);
164
+ const a = document.createElement('a');
165
+ a.href = url;
166
+ a.download = 'viz-files.zip';
167
+ a.click();
168
+ URL.revokeObjectURL(url);
169
+
170
+ // Show success feedback
171
+ setExportButtonText('Exported!');
172
+ setTimeout(
173
+ () => setExportButtonText('Export to ZIP'),
174
+ 2000,
175
+ );
176
+ } catch (error) {
177
+ console.error(
178
+ 'Failed to export files to ZIP:',
179
+ error,
180
+ );
181
+ setExportButtonText('Error');
182
+ setTimeout(
183
+ () => setExportButtonText('Export to ZIP'),
184
+ 2000,
185
+ );
186
+ }
187
+ };
188
+
127
189
  return {
128
190
  handleCopyForAI,
129
191
  handlePasteForAI,
192
+ handleExportToZip,
130
193
  };
131
194
  };
@@ -164,15 +164,22 @@ export const VZSidebar = ({
164
164
  useState('Copy for AI');
165
165
  const [pasteButtonText, setPasteButtonText] =
166
166
  useState('Paste for AI');
167
+ const [exportButtonText, setExportButtonText] =
168
+ useState('Export to ZIP');
167
169
 
168
170
  // Create AI copy/paste handlers
169
- const { handleCopyForAI, handlePasteForAI } = useMemo(
171
+ const {
172
+ handleCopyForAI,
173
+ handlePasteForAI,
174
+ handleExportToZip,
175
+ } = useMemo(
170
176
  () =>
171
177
  createAICopyPasteHandlers(
172
178
  files,
173
179
  submitOperation,
174
180
  setCopyButtonText,
175
181
  setPasteButtonText,
182
+ setExportButtonText,
176
183
  ),
177
184
  [files, submitOperation],
178
185
  );
@@ -548,6 +555,15 @@ export const VZSidebar = ({
548
555
  </div>
549
556
  {!isAIChatOpen && !isSearchOpen && (
550
557
  <div className="ai-buttons">
558
+ {filesExist && (
559
+ <button
560
+ className="ai-button export-button"
561
+ onClick={handleExportToZip}
562
+ title="Export files to ZIP"
563
+ >
564
+ {exportButtonText}
565
+ </button>
566
+ )}
551
567
  {filesExist && (
552
568
  <button
553
569
  className="ai-button copy-button"
@@ -557,6 +573,7 @@ export const VZSidebar = ({
557
573
  {copyButtonText}
558
574
  </button>
559
575
  )}
576
+
560
577
  <button
561
578
  className="ai-button paste-button"
562
579
  onClick={handlePasteForAI}
@@ -335,6 +335,15 @@
335
335
  }
336
336
  }
337
337
 
338
+ &.export-button {
339
+ border-color: var(--vh-color-primary-03);
340
+
341
+ &:hover {
342
+ background: var(--vh-color-hover-dark);
343
+ border-color: var(--vh-color-neutral-04);
344
+ }
345
+ }
346
+
338
347
  &.paste-button {
339
348
  border-color: var(--vh-color-success-03);
340
349
 
@@ -78,6 +78,23 @@ export const useDragAndDrop = () => {
78
78
  return isText;
79
79
  };
80
80
 
81
+ const isImageFile = (file: File): boolean => {
82
+ const imageTypes = ['image/'];
83
+ const isImage =
84
+ imageTypes.some((type) =>
85
+ file.type.startsWith(type),
86
+ ) ||
87
+ file.name.match(
88
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
89
+ ) !== null;
90
+
91
+ DEBUG &&
92
+ console.log(
93
+ `[useDragAndDrop] File ${file.name} is${isImage ? '' : ' not'} an image file`,
94
+ );
95
+ return isImage;
96
+ };
97
+
81
98
  const readFileAsText = (file: File): Promise<string> => {
82
99
  return new Promise((resolve, reject) => {
83
100
  DEBUG &&
@@ -127,6 +144,98 @@ export const useDragAndDrop = () => {
127
144
  });
128
145
  };
129
146
 
147
+ const readImageAsBase64 = (
148
+ file: File,
149
+ ): Promise<string> => {
150
+ return new Promise((resolve, reject) => {
151
+ DEBUG &&
152
+ console.log(
153
+ `[useDragAndDrop] Reading image file: ${file.name}`,
154
+ );
155
+
156
+ if (!isImageFile(file)) {
157
+ DEBUG &&
158
+ console.log(
159
+ `[useDragAndDrop] Rejected non-image file: ${file.name}`,
160
+ );
161
+ reject(
162
+ new Error(
163
+ `File ${file.name} is not an image file`,
164
+ ),
165
+ );
166
+ return;
167
+ }
168
+
169
+ // For SVG files, read as text if they're text-based
170
+ if (
171
+ file.type === 'image/svg+xml' ||
172
+ file.name.toLowerCase().endsWith('.svg')
173
+ ) {
174
+ const textReader = new FileReader();
175
+ textReader.onload = (e) => {
176
+ if (e.target?.result) {
177
+ DEBUG &&
178
+ console.log(
179
+ `[useDragAndDrop] Successfully read SVG file as text: ${file.name}`,
180
+ );
181
+ resolve(e.target.result as string);
182
+ } else {
183
+ reject(
184
+ new Error(
185
+ `Failed to read SVG file ${file.name}`,
186
+ ),
187
+ );
188
+ }
189
+ };
190
+ textReader.onerror = () =>
191
+ reject(
192
+ new Error(
193
+ `Error reading SVG file ${file.name}`,
194
+ ),
195
+ );
196
+ textReader.readAsText(file);
197
+ return;
198
+ }
199
+
200
+ const reader = new FileReader();
201
+ reader.onload = (e) => {
202
+ if (e.target?.result) {
203
+ DEBUG &&
204
+ console.log(
205
+ `[useDragAndDrop] Successfully read image file: ${file.name}`,
206
+ );
207
+ // Return just the base64 part without the data URL prefix
208
+ const base64 = (e.target.result as string).split(
209
+ ',',
210
+ )[1];
211
+ resolve(base64);
212
+ } else {
213
+ DEBUG &&
214
+ console.log(
215
+ `[useDragAndDrop] Failed to read image file: ${file.name}`,
216
+ );
217
+ reject(
218
+ new Error(
219
+ `Failed to read image file ${file.name}`,
220
+ ),
221
+ );
222
+ }
223
+ };
224
+ reader.onerror = () => {
225
+ DEBUG &&
226
+ console.log(
227
+ `[useDragAndDrop] Error reading image file: ${file.name}`,
228
+ );
229
+ reject(
230
+ new Error(
231
+ `Error reading image file ${file.name}`,
232
+ ),
233
+ );
234
+ };
235
+ reader.readAsDataURL(file);
236
+ });
237
+ };
238
+
130
239
  const processEntry = async (
131
240
  entry: FileSystemEntry,
132
241
  path: string,
@@ -144,12 +253,21 @@ export const useDragAndDrop = () => {
144
253
  );
145
254
  entry.file(async (file) => {
146
255
  try {
147
- const content = await readFileAsText(file);
148
- DEBUG &&
149
- console.log(
150
- `[useDragAndDrop] Creating file: ${path}${file.name}`,
151
- );
152
- createFile(`${path}${file.name}`, content);
256
+ if (isImageFile(file)) {
257
+ const content = await readImageAsBase64(file);
258
+ DEBUG &&
259
+ console.log(
260
+ `[useDragAndDrop] Creating image file: ${path}${file.name}`,
261
+ );
262
+ createFile(`${path}${file.name}`, content);
263
+ } else {
264
+ const content = await readFileAsText(file);
265
+ DEBUG &&
266
+ console.log(
267
+ `[useDragAndDrop] Creating text file: ${path}${file.name}`,
268
+ );
269
+ createFile(`${path}${file.name}`, content);
270
+ }
153
271
  } catch (error) {
154
272
  console.error(
155
273
  `Error processing file ${file.name}:`,
@@ -237,8 +355,15 @@ export const useDragAndDrop = () => {
237
355
  `[useDragAndDrop] Processing dropped file: ${file.name}`,
238
356
  );
239
357
  try {
240
- const content = await readFileAsText(file);
241
- createFile(file.name, content);
358
+ if (isImageFile(file)) {
359
+ const content =
360
+ await readImageAsBase64(file);
361
+ createFile(file.name, content);
362
+ } else {
363
+ const content =
364
+ await readFileAsText(file);
365
+ createFile(file.name, content);
366
+ }
242
367
  } catch (error) {
243
368
  console.error(
244
369
  `Error processing file ${file.name}:`,
@@ -2,3 +2,5 @@ export const enableLiveKit =
2
2
  import.meta.env.VITE_ENABLE_LIVEKIT === 'true';
3
3
 
4
4
  export const enableAIChat = true;
5
+
6
+ export const enableDiffView = true;
@@ -0,0 +1,32 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ // Custom hook to manage runtime error state
4
+ export const useRuntimeError = () => {
5
+ // State to hold the runtime error message
6
+ // `null` means no errors
7
+ // If this is a string, it's the
8
+ // error message from runtime execution.
9
+ const [runtimeError, setRuntimeError] = useState<
10
+ string | null
11
+ >(null);
12
+
13
+ // Callback function to handle runtime errors
14
+ // This will be passed to the createRuntime function
15
+ const handleRuntimeError = useCallback(
16
+ (formattedErrorMessage: string) => {
17
+ setRuntimeError(formattedErrorMessage);
18
+ },
19
+ [],
20
+ );
21
+
22
+ // Function to clear runtime errors
23
+ const clearRuntimeError = useCallback(() => {
24
+ setRuntimeError(null);
25
+ }, []);
26
+
27
+ return {
28
+ runtimeError,
29
+ handleRuntimeError,
30
+ clearRuntimeError,
31
+ };
32
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Utility function to check if a file is an image based on its name.
3
+ * @param fileName - The name of the file (e.g., "image.png", "photo.jpg")
4
+ * @returns true if the file is an image, false otherwise
5
+ */
6
+ export const isImageFile = (fileName: string): boolean => {
7
+ return (
8
+ fileName.match(
9
+ /\.(png|jpg|jpeg|gif|bmp|svg|webp)$/i,
10
+ ) !== null
11
+ );
12
+ };