@aprovan/patchwork-editor 0.1.2-dev.68f15f3 → 0.1.2-dev.78c0b14

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 (41) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/dist/components/CodePreview.d.ts +2 -3
  3. package/dist/components/MarkdownEditor.d.ts +1 -1
  4. package/dist/components/MarkdownPreview.d.ts +1 -1
  5. package/dist/components/SaveStatusButton.d.ts +1 -1
  6. package/dist/components/ServicesInspector.d.ts +1 -1
  7. package/dist/components/WidgetPreview.d.ts +1 -1
  8. package/dist/components/edit/CodeBlockView.d.ts +1 -1
  9. package/dist/components/edit/EditHistory.d.ts +1 -1
  10. package/dist/components/edit/EditModal.d.ts +1 -1
  11. package/dist/components/edit/FileTree.d.ts +1 -1
  12. package/dist/components/edit/MediaPreview.d.ts +1 -1
  13. package/dist/components/edit/SaveConfirmDialog.d.ts +1 -1
  14. package/dist/components/edit/index.d.ts +1 -0
  15. package/dist/components/edit/useEditSession.d.ts +1 -1
  16. package/dist/components/edit/useProjectState.d.ts +17 -0
  17. package/dist/index.d.ts +11 -3
  18. package/dist/index.js +414 -403
  19. package/dist/lib/vfs.d.ts +2 -3
  20. package/package.json +6 -4
  21. package/src/components/CodePreview.tsx +7 -10
  22. package/src/components/MarkdownEditor.tsx +3 -3
  23. package/src/components/MarkdownPreview.tsx +2 -2
  24. package/src/components/ServicesInspector.tsx +1 -1
  25. package/src/components/WidgetPreview.tsx +1 -1
  26. package/src/components/edit/CodeBlockView.tsx +6 -2
  27. package/src/components/edit/EditHistory.tsx +1 -1
  28. package/src/components/edit/EditModal.tsx +6 -6
  29. package/src/components/edit/FileTree.tsx +2 -3
  30. package/src/components/edit/MediaPreview.tsx +1 -1
  31. package/src/components/edit/index.ts +4 -0
  32. package/src/components/edit/useEditSession.ts +19 -109
  33. package/src/components/edit/useProjectState.ts +122 -0
  34. package/src/index.ts +15 -18
  35. package/src/lib/code-extractor.ts +1 -1
  36. package/src/lib/vfs.ts +4 -5
  37. package/tsconfig.json +2 -1
  38. package/dist/components/index.d.ts +0 -7
  39. package/dist/lib/index.d.ts +0 -4
  40. package/src/components/index.ts +0 -7
  41. package/src/lib/index.ts +0 -4
package/dist/index.js CHANGED
@@ -2,17 +2,18 @@ import { Node, textblockTypeInputRule } from '@tiptap/core';
2
2
  import { ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent, useEditor, EditorContent } from '@tiptap/react';
3
3
  import { useRef, useCallback, useMemo, useState, useEffect } from 'react';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
+ import { HttpBackend, VFSStore, createSingleFileProject, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
5
6
  import { Folder, Pin, X, Loader2, AlertCircle, FileImage, FileVideo, Pencil, RotateCcw, FileCode, FolderTree, Eye, Code, Send, MessageSquare, Server, ChevronDown, File, PinOff, ChevronRight, ChevronsDown, Upload, AlertTriangle, Save } from 'lucide-react';
6
- import { createSingleFileProject, HttpBackend, VFSStore, detectMainFile, createProjectFromFiles } from '@aprovan/patchwork-compiler';
7
7
  import Markdown from 'react-markdown';
8
8
  import remarkGfm from 'remark-gfm';
9
- import StarterKit from '@tiptap/starter-kit';
9
+ import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
10
10
  import Placeholder from '@tiptap/extension-placeholder';
11
11
  import Typography from '@tiptap/extension-typography';
12
- import { Markdown as Markdown$1 } from 'tiptap-markdown';
13
12
  import { TextSelection } from '@tiptap/pm/state';
13
+ import StarterKit from '@tiptap/starter-kit';
14
+ import { Markdown as Markdown$1 } from 'tiptap-markdown';
15
+ import DOMPurify from 'dompurify';
14
16
  import { createHighlighter } from 'shiki';
15
- import { Bobbin, serializeChangesToYAML } from '@aprovan/bobbin';
16
17
  import { clsx } from 'clsx';
17
18
  import { twMerge } from 'tailwind-merge';
18
19
 
@@ -180,6 +181,69 @@ var CodeBlockExtension = Node.create({
180
181
  };
181
182
  }
182
183
  });
184
+ var VFS_BASE_URL = "/vfs";
185
+ var vfsConfigCache = null;
186
+ async function getVFSConfig() {
187
+ if (vfsConfigCache) return vfsConfigCache;
188
+ try {
189
+ const res = await fetch(`${VFS_BASE_URL}/config`);
190
+ if (res.ok) {
191
+ vfsConfigCache = await res.json();
192
+ return vfsConfigCache;
193
+ }
194
+ } catch {
195
+ }
196
+ return { usePaths: false };
197
+ }
198
+ var storeInstance = null;
199
+ function getVFSStore() {
200
+ if (!storeInstance) {
201
+ const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
202
+ storeInstance = new VFSStore(provider, {
203
+ sync: true,
204
+ conflictStrategy: "local-wins"
205
+ });
206
+ }
207
+ return storeInstance;
208
+ }
209
+ async function saveProject(project) {
210
+ const store = getVFSStore();
211
+ await store.saveProject(project);
212
+ }
213
+ async function loadProject(id) {
214
+ const store = getVFSStore();
215
+ return store.loadProject(id);
216
+ }
217
+ async function listProjects() {
218
+ const store = getVFSStore();
219
+ const files = await store.listFiles();
220
+ const projectIds = /* @__PURE__ */ new Set();
221
+ for (const file of files) {
222
+ const id = file.split("/")[0];
223
+ if (id) projectIds.add(id);
224
+ }
225
+ return Array.from(projectIds);
226
+ }
227
+ async function saveFile(path, content) {
228
+ const store = getVFSStore();
229
+ await store.writeFile(path, content);
230
+ }
231
+ async function loadFile(path, encoding) {
232
+ const store = getVFSStore();
233
+ return store.readFile(path, encoding);
234
+ }
235
+ function subscribeToChanges(callback) {
236
+ const store = getVFSStore();
237
+ return store.on("change", callback);
238
+ }
239
+ async function isVFSAvailable() {
240
+ try {
241
+ const res = await fetch(VFS_BASE_URL, { method: "HEAD" });
242
+ return res.ok || res.status === 404;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
183
247
 
184
248
  // src/components/edit/types.ts
185
249
  function getActiveContent(state) {
@@ -440,20 +504,12 @@ function cloneProject(project) {
440
504
  files: new Map(project.files)
441
505
  };
442
506
  }
443
- function useEditSession(options) {
507
+ function useProjectState(options) {
444
508
  const {
445
509
  originalCode,
446
510
  originalProject: providedProject,
447
- initialActiveFile,
448
- compile,
449
- apiEndpoint
511
+ initialActiveFile
450
512
  } = options;
451
- console.log(
452
- "[useEditSession] providedProject:",
453
- providedProject?.id,
454
- "files:",
455
- providedProject ? Array.from(providedProject.files.keys()) : "none"
456
- );
457
513
  const originalProject = useMemo(
458
514
  () => providedProject ?? createSingleFileProject(originalCode ?? ""),
459
515
  [providedProject, originalCode]
@@ -464,10 +520,6 @@ function useEditSession(options) {
464
520
  initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
465
521
  );
466
522
  const [history, setHistory] = useState([]);
467
- const [isApplying, setIsApplying] = useState(false);
468
- const [error, setError] = useState(null);
469
- const [streamingNotes, setStreamingNotes] = useState([]);
470
- const [pendingPrompt, setPendingPrompt] = useState(null);
471
523
  useEffect(() => {
472
524
  if (originalProject !== lastSyncedProjectRef.current) {
473
525
  lastSyncedProjectRef.current = originalProject;
@@ -476,15 +528,74 @@ function useEditSession(options) {
476
528
  initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
477
529
  );
478
530
  setHistory([]);
479
- setError(null);
480
- setStreamingNotes([]);
481
531
  }
482
532
  }, [originalProject, initialActiveFile]);
533
+ const currentCode = useMemo(
534
+ () => project.files.get(activeFile)?.content ?? "",
535
+ [project, activeFile]
536
+ );
537
+ const revert = useCallback(() => {
538
+ setProject(originalProject);
539
+ setActiveFile(originalProject.entry);
540
+ setHistory([]);
541
+ }, [originalProject]);
542
+ const updateActiveFile = useCallback(
543
+ (content) => {
544
+ setProject((prev) => {
545
+ const updated = cloneProject(prev);
546
+ const file = updated.files.get(activeFile);
547
+ if (file) {
548
+ updated.files.set(activeFile, { ...file, content });
549
+ }
550
+ return updated;
551
+ });
552
+ },
553
+ [activeFile]
554
+ );
555
+ const replaceFile = useCallback(
556
+ (path, content, encoding = "utf8") => {
557
+ setProject((prev) => {
558
+ const updated = cloneProject(prev);
559
+ const file = updated.files.get(path);
560
+ if (file) {
561
+ updated.files.set(path, { ...file, content, encoding });
562
+ } else {
563
+ updated.files.set(path, { path, content, encoding });
564
+ }
565
+ return updated;
566
+ });
567
+ },
568
+ []
569
+ );
570
+ return {
571
+ project,
572
+ originalProject,
573
+ activeFile,
574
+ setActiveFile,
575
+ history,
576
+ setHistory,
577
+ setProject,
578
+ revert,
579
+ updateActiveFile,
580
+ replaceFile,
581
+ currentCode
582
+ };
583
+ }
584
+ function useEditSession(options) {
585
+ const {
586
+ compile,
587
+ apiEndpoint
588
+ } = options;
589
+ const state = useProjectState(options);
590
+ const [isApplying, setIsApplying] = useState(false);
591
+ const [error, setError] = useState(null);
592
+ const [streamingNotes, setStreamingNotes] = useState([]);
593
+ const [pendingPrompt, setPendingPrompt] = useState(null);
483
594
  const performEdit = useCallback(
484
- async (currentCode2, prompt, isRetry = false) => {
595
+ async (currentCode, prompt, isRetry = false) => {
485
596
  const entries = [];
486
597
  const response = await sendEditRequest(
487
- { code: currentCode2, prompt },
598
+ { code: currentCode, prompt },
488
599
  {
489
600
  endpoint: apiEndpoint,
490
601
  onProgress: (note) => setStreamingNotes((prev) => [...prev, note])
@@ -517,10 +628,6 @@ Please fix this error.`;
517
628
  },
518
629
  [compile, apiEndpoint]
519
630
  );
520
- const currentCode = useMemo(
521
- () => project.files.get(activeFile)?.content ?? "",
522
- [project, activeFile]
523
- );
524
631
  const submitEdit = useCallback(
525
632
  async (prompt) => {
526
633
  if (!prompt.trim() || isApplying) return;
@@ -529,16 +636,16 @@ Please fix this error.`;
529
636
  setStreamingNotes([]);
530
637
  setPendingPrompt(prompt);
531
638
  try {
532
- const result = await performEdit(currentCode, prompt);
533
- setProject((prev) => {
534
- const updated = cloneProject(prev);
535
- const file = updated.files.get(activeFile);
639
+ const result = await performEdit(state.currentCode, prompt);
640
+ state.setProject((prev) => {
641
+ const updated = { ...prev, files: new Map(prev.files) };
642
+ const file = updated.files.get(state.activeFile);
536
643
  if (file) {
537
- updated.files.set(activeFile, { ...file, content: result.newCode });
644
+ updated.files.set(state.activeFile, { ...file, content: result.newCode });
538
645
  }
539
646
  return updated;
540
647
  });
541
- setHistory((prev) => [...prev, ...result.entries]);
648
+ state.setHistory((prev) => [...prev, ...result.entries]);
542
649
  } catch (err) {
543
650
  setError(err instanceof Error ? err.message : "Edit failed");
544
651
  } finally {
@@ -547,61 +654,26 @@ Please fix this error.`;
547
654
  setPendingPrompt(null);
548
655
  }
549
656
  },
550
- [currentCode, activeFile, isApplying, performEdit]
551
- );
552
- const revert = useCallback(() => {
553
- setProject(originalProject);
554
- setActiveFile(originalProject.entry);
555
- setHistory([]);
556
- setError(null);
557
- setStreamingNotes([]);
558
- }, [originalProject]);
559
- const updateActiveFile = useCallback(
560
- (content) => {
561
- setProject((prev) => {
562
- const updated = cloneProject(prev);
563
- const file = updated.files.get(activeFile);
564
- if (file) {
565
- updated.files.set(activeFile, { ...file, content });
566
- }
567
- return updated;
568
- });
569
- },
570
- [activeFile]
571
- );
572
- const replaceFile = useCallback(
573
- (path, content, encoding = "utf8") => {
574
- setProject((prev) => {
575
- const updated = cloneProject(prev);
576
- const file = updated.files.get(path);
577
- if (file) {
578
- updated.files.set(path, { ...file, content, encoding });
579
- } else {
580
- updated.files.set(path, { path, content, encoding });
581
- }
582
- return updated;
583
- });
584
- },
585
- []
657
+ [state.currentCode, state.activeFile, state.setProject, state.setHistory, isApplying, performEdit]
586
658
  );
587
659
  const clearError = useCallback(() => {
588
660
  setError(null);
589
661
  }, []);
590
662
  return {
591
- project,
592
- originalProject,
593
- activeFile,
594
- history,
663
+ project: state.project,
664
+ originalProject: state.originalProject,
665
+ activeFile: state.activeFile,
666
+ history: state.history,
595
667
  isApplying,
596
668
  error,
597
669
  streamingNotes,
598
670
  pendingPrompt,
599
671
  submitEdit,
600
- revert,
601
- updateActiveFile,
602
- setActiveFile,
672
+ revert: state.revert,
673
+ updateActiveFile: state.updateActiveFile,
674
+ setActiveFile: state.setActiveFile,
603
675
  clearError,
604
- replaceFile
676
+ replaceFile: state.replaceFile
605
677
  };
606
678
  }
607
679
  function ProgressNote({ text, isLatest }) {
@@ -1010,69 +1082,221 @@ function SaveStatusButton({
1010
1082
  }
1011
1083
  );
1012
1084
  }
1013
-
1014
- // src/components/edit/fileTypes.ts
1015
- var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
1016
- var MEDIA_EXTENSIONS = [
1017
- ".svg",
1018
- ".png",
1019
- ".jpg",
1020
- ".jpeg",
1021
- ".gif",
1022
- ".webp",
1023
- ".mp4",
1024
- ".mov",
1025
- ".webm"
1026
- ];
1027
- var TEXT_EXTENSIONS = [
1028
- ".json",
1029
- ".yaml",
1030
- ".yml",
1031
- ".md",
1032
- ".txt",
1033
- ".css",
1034
- ".html",
1035
- ".xml",
1036
- ".toml"
1085
+ var highlighterPromise = null;
1086
+ var COMMON_LANGUAGES = [
1087
+ "typescript",
1088
+ "javascript",
1089
+ "tsx",
1090
+ "jsx",
1091
+ "json",
1092
+ "html",
1093
+ "css",
1094
+ "markdown",
1095
+ "yaml",
1096
+ "python",
1097
+ "bash",
1098
+ "sql"
1037
1099
  ];
1038
- var EXTENSION_TO_LANGUAGE = {
1039
- ".tsx": "tsx",
1040
- ".jsx": "jsx",
1041
- ".ts": "typescript",
1042
- ".js": "javascript",
1043
- ".json": "json",
1044
- ".yaml": "yaml",
1045
- ".yml": "yaml",
1046
- ".md": "markdown",
1047
- ".txt": "text",
1048
- ".css": "css",
1049
- ".html": "html",
1050
- ".xml": "xml",
1051
- ".toml": "toml",
1052
- ".svg": "xml"
1053
- };
1054
- var EXTENSION_TO_MIME = {
1055
- ".tsx": "text/typescript-jsx",
1056
- ".jsx": "text/javascript-jsx",
1057
- ".ts": "text/typescript",
1058
- ".js": "text/javascript",
1059
- ".json": "application/json",
1060
- ".yaml": "text/yaml",
1061
- ".yml": "text/yaml",
1062
- ".md": "text/markdown",
1063
- ".txt": "text/plain",
1064
- ".css": "text/css",
1065
- ".html": "text/html",
1066
- ".xml": "application/xml",
1067
- ".toml": "text/toml",
1068
- ".svg": "image/svg+xml",
1069
- ".png": "image/png",
1070
- ".jpg": "image/jpeg",
1071
- ".jpeg": "image/jpeg",
1072
- ".gif": "image/gif",
1073
- ".webp": "image/webp",
1074
- ".mp4": "video/mp4",
1075
- ".mov": "video/quicktime",
1100
+ function getHighlighter() {
1101
+ if (!highlighterPromise) {
1102
+ highlighterPromise = createHighlighter({
1103
+ themes: ["github-light"],
1104
+ langs: COMMON_LANGUAGES
1105
+ });
1106
+ }
1107
+ return highlighterPromise;
1108
+ }
1109
+ function normalizeLanguage(lang) {
1110
+ if (!lang) return "typescript";
1111
+ const normalized = lang.toLowerCase();
1112
+ const mapping = {
1113
+ ts: "typescript",
1114
+ tsx: "tsx",
1115
+ js: "javascript",
1116
+ jsx: "jsx",
1117
+ json: "json",
1118
+ html: "html",
1119
+ css: "css",
1120
+ md: "markdown",
1121
+ markdown: "markdown",
1122
+ yml: "yaml",
1123
+ yaml: "yaml",
1124
+ py: "python",
1125
+ python: "python",
1126
+ sh: "bash",
1127
+ bash: "bash",
1128
+ sql: "sql",
1129
+ typescript: "typescript",
1130
+ javascript: "javascript"
1131
+ };
1132
+ return mapping[normalized] || "typescript";
1133
+ }
1134
+ function CodeBlockView({ content, language, editable = false, onChange }) {
1135
+ const textareaRef = useRef(null);
1136
+ const containerRef = useRef(null);
1137
+ const [highlighter, setHighlighter] = useState(null);
1138
+ useEffect(() => {
1139
+ let mounted = true;
1140
+ getHighlighter().then((h) => {
1141
+ if (mounted) setHighlighter(h);
1142
+ });
1143
+ return () => {
1144
+ mounted = false;
1145
+ };
1146
+ }, []);
1147
+ useEffect(() => {
1148
+ if (textareaRef.current) {
1149
+ textareaRef.current.style.height = "auto";
1150
+ textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
1151
+ }
1152
+ }, [content]);
1153
+ const handleChange = useCallback(
1154
+ (e) => {
1155
+ onChange?.(e.target.value);
1156
+ },
1157
+ [onChange]
1158
+ );
1159
+ const handleKeyDown = useCallback(
1160
+ (e) => {
1161
+ if (e.key === "Tab") {
1162
+ e.preventDefault();
1163
+ const target = e.target;
1164
+ const start = target.selectionStart;
1165
+ const end = target.selectionEnd;
1166
+ const value = target.value;
1167
+ const newValue = value.substring(0, start) + " " + value.substring(end);
1168
+ onChange?.(newValue);
1169
+ requestAnimationFrame(() => {
1170
+ target.selectionStart = target.selectionEnd = start + 2;
1171
+ });
1172
+ }
1173
+ },
1174
+ [onChange]
1175
+ );
1176
+ const langLabel = language || "text";
1177
+ const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
1178
+ const highlightedHtml = useMemo(() => {
1179
+ if (!highlighter) return null;
1180
+ try {
1181
+ const raw = highlighter.codeToHtml(content, {
1182
+ lang: shikiLang,
1183
+ theme: "github-light"
1184
+ });
1185
+ return DOMPurify.sanitize(raw, {
1186
+ ALLOWED_TAGS: ["pre", "code", "span", "div"],
1187
+ ALLOWED_ATTR: ["class", "style"]
1188
+ });
1189
+ } catch {
1190
+ return null;
1191
+ }
1192
+ }, [highlighter, content, shikiLang]);
1193
+ return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-[#ffffff]", children: [
1194
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-[#f6f8fa] border-b border-[#d0d7de] text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-[#57606a]", children: langLabel }) }),
1195
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: editable ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-full", children: [
1196
+ /* @__PURE__ */ jsx(
1197
+ "div",
1198
+ {
1199
+ ref: containerRef,
1200
+ className: "absolute top-0 left-0 right-0 pointer-events-none p-4",
1201
+ "aria-hidden": "true",
1202
+ children: highlightedHtml ? /* @__PURE__ */ jsx(
1203
+ "div",
1204
+ {
1205
+ className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1206
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1207
+ }
1208
+ ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words text-[#24292f] m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
1209
+ }
1210
+ ),
1211
+ /* @__PURE__ */ jsx(
1212
+ "textarea",
1213
+ {
1214
+ ref: textareaRef,
1215
+ value: content,
1216
+ onChange: handleChange,
1217
+ onKeyDown: handleKeyDown,
1218
+ className: "relative w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none p-4 text-transparent whitespace-pre-wrap break-words",
1219
+ spellCheck: false,
1220
+ style: {
1221
+ tabSize: 2,
1222
+ caretColor: "#24292f",
1223
+ wordBreak: "break-word",
1224
+ overflowWrap: "break-word"
1225
+ }
1226
+ }
1227
+ )
1228
+ ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: highlightedHtml ? /* @__PURE__ */ jsx(
1229
+ "div",
1230
+ {
1231
+ className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1232
+ dangerouslySetInnerHTML: { __html: highlightedHtml }
1233
+ }
1234
+ ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed text-[#24292f]", children: /* @__PURE__ */ jsx("code", { children: content }) }) }) })
1235
+ ] });
1236
+ }
1237
+
1238
+ // src/components/edit/fileTypes.ts
1239
+ var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
1240
+ var MEDIA_EXTENSIONS = [
1241
+ ".svg",
1242
+ ".png",
1243
+ ".jpg",
1244
+ ".jpeg",
1245
+ ".gif",
1246
+ ".webp",
1247
+ ".mp4",
1248
+ ".mov",
1249
+ ".webm"
1250
+ ];
1251
+ var TEXT_EXTENSIONS = [
1252
+ ".json",
1253
+ ".yaml",
1254
+ ".yml",
1255
+ ".md",
1256
+ ".txt",
1257
+ ".css",
1258
+ ".html",
1259
+ ".xml",
1260
+ ".toml"
1261
+ ];
1262
+ var EXTENSION_TO_LANGUAGE = {
1263
+ ".tsx": "tsx",
1264
+ ".jsx": "jsx",
1265
+ ".ts": "typescript",
1266
+ ".js": "javascript",
1267
+ ".json": "json",
1268
+ ".yaml": "yaml",
1269
+ ".yml": "yaml",
1270
+ ".md": "markdown",
1271
+ ".txt": "text",
1272
+ ".css": "css",
1273
+ ".html": "html",
1274
+ ".xml": "xml",
1275
+ ".toml": "toml",
1276
+ ".svg": "xml"
1277
+ };
1278
+ var EXTENSION_TO_MIME = {
1279
+ ".tsx": "text/typescript-jsx",
1280
+ ".jsx": "text/javascript-jsx",
1281
+ ".ts": "text/typescript",
1282
+ ".js": "text/javascript",
1283
+ ".json": "application/json",
1284
+ ".yaml": "text/yaml",
1285
+ ".yml": "text/yaml",
1286
+ ".md": "text/markdown",
1287
+ ".txt": "text/plain",
1288
+ ".css": "text/css",
1289
+ ".html": "text/html",
1290
+ ".xml": "application/xml",
1291
+ ".toml": "text/toml",
1292
+ ".svg": "image/svg+xml",
1293
+ ".png": "image/png",
1294
+ ".jpg": "image/jpeg",
1295
+ ".jpeg": "image/jpeg",
1296
+ ".gif": "image/gif",
1297
+ ".webp": "image/webp",
1298
+ ".mp4": "video/mp4",
1299
+ ".mov": "video/quicktime",
1076
1300
  ".webm": "video/webm"
1077
1301
  };
1078
1302
  function getExtension(path) {
@@ -1693,203 +1917,6 @@ function FileTree({
1693
1917
  ) })
1694
1918
  ] });
1695
1919
  }
1696
- function SaveConfirmDialog({
1697
- isOpen,
1698
- isSaving,
1699
- error,
1700
- onSave,
1701
- onDiscard,
1702
- onCancel
1703
- }) {
1704
- if (!isOpen) return null;
1705
- return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-lg shadow-lg w-full max-w-md", children: [
1706
- /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
1707
- /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: "Unsaved Changes" }),
1708
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: "You have unsaved changes. Would you like to save them before closing?" }),
1709
- error && /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-3", children: [
1710
- "Save failed: ",
1711
- error
1712
- ] })
1713
- ] }),
1714
- /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 p-6 pt-0", children: [
1715
- /* @__PURE__ */ jsx(
1716
- "button",
1717
- {
1718
- onClick: onCancel,
1719
- disabled: isSaving,
1720
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
1721
- children: "Cancel"
1722
- }
1723
- ),
1724
- /* @__PURE__ */ jsx(
1725
- "button",
1726
- {
1727
- onClick: onDiscard,
1728
- disabled: isSaving,
1729
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background text-destructive hover:bg-destructive/10 disabled:opacity-50",
1730
- children: "Discard"
1731
- }
1732
- ),
1733
- /* @__PURE__ */ jsx(
1734
- "button",
1735
- {
1736
- onClick: onSave,
1737
- disabled: isSaving,
1738
- className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
1739
- children: isSaving ? "Saving..." : "Save"
1740
- }
1741
- )
1742
- ] })
1743
- ] }) });
1744
- }
1745
- var highlighterPromise = null;
1746
- var COMMON_LANGUAGES = [
1747
- "typescript",
1748
- "javascript",
1749
- "tsx",
1750
- "jsx",
1751
- "json",
1752
- "html",
1753
- "css",
1754
- "markdown",
1755
- "yaml",
1756
- "python",
1757
- "bash",
1758
- "sql"
1759
- ];
1760
- function getHighlighter() {
1761
- if (!highlighterPromise) {
1762
- highlighterPromise = createHighlighter({
1763
- themes: ["github-light"],
1764
- langs: COMMON_LANGUAGES
1765
- });
1766
- }
1767
- return highlighterPromise;
1768
- }
1769
- function normalizeLanguage(lang) {
1770
- if (!lang) return "typescript";
1771
- const normalized = lang.toLowerCase();
1772
- const mapping = {
1773
- ts: "typescript",
1774
- tsx: "tsx",
1775
- js: "javascript",
1776
- jsx: "jsx",
1777
- json: "json",
1778
- html: "html",
1779
- css: "css",
1780
- md: "markdown",
1781
- markdown: "markdown",
1782
- yml: "yaml",
1783
- yaml: "yaml",
1784
- py: "python",
1785
- python: "python",
1786
- sh: "bash",
1787
- bash: "bash",
1788
- sql: "sql",
1789
- typescript: "typescript",
1790
- javascript: "javascript"
1791
- };
1792
- return mapping[normalized] || "typescript";
1793
- }
1794
- function CodeBlockView({ content, language, editable = false, onChange }) {
1795
- const textareaRef = useRef(null);
1796
- const containerRef = useRef(null);
1797
- const [highlighter, setHighlighter] = useState(null);
1798
- useEffect(() => {
1799
- let mounted = true;
1800
- getHighlighter().then((h) => {
1801
- if (mounted) setHighlighter(h);
1802
- });
1803
- return () => {
1804
- mounted = false;
1805
- };
1806
- }, []);
1807
- useEffect(() => {
1808
- if (textareaRef.current) {
1809
- textareaRef.current.style.height = "auto";
1810
- textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
1811
- }
1812
- }, [content]);
1813
- const handleChange = useCallback(
1814
- (e) => {
1815
- onChange?.(e.target.value);
1816
- },
1817
- [onChange]
1818
- );
1819
- const handleKeyDown = useCallback(
1820
- (e) => {
1821
- if (e.key === "Tab") {
1822
- e.preventDefault();
1823
- const target = e.target;
1824
- const start = target.selectionStart;
1825
- const end = target.selectionEnd;
1826
- const value = target.value;
1827
- const newValue = value.substring(0, start) + " " + value.substring(end);
1828
- onChange?.(newValue);
1829
- requestAnimationFrame(() => {
1830
- target.selectionStart = target.selectionEnd = start + 2;
1831
- });
1832
- }
1833
- },
1834
- [onChange]
1835
- );
1836
- const langLabel = language || "text";
1837
- const shikiLang = useMemo(() => normalizeLanguage(language), [language]);
1838
- const highlightedHtml = useMemo(() => {
1839
- if (!highlighter) return null;
1840
- try {
1841
- return highlighter.codeToHtml(content, {
1842
- lang: shikiLang,
1843
- theme: "github-light"
1844
- });
1845
- } catch {
1846
- return null;
1847
- }
1848
- }, [highlighter, content, shikiLang]);
1849
- return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-[#ffffff]", children: [
1850
- /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-[#f6f8fa] border-b border-[#d0d7de] text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-[#57606a]", children: langLabel }) }),
1851
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto overflow-x-hidden", children: editable ? /* @__PURE__ */ jsxs("div", { className: "relative min-h-full", children: [
1852
- /* @__PURE__ */ jsx(
1853
- "div",
1854
- {
1855
- ref: containerRef,
1856
- className: "absolute top-0 left-0 right-0 pointer-events-none p-4",
1857
- "aria-hidden": "true",
1858
- children: highlightedHtml ? /* @__PURE__ */ jsx(
1859
- "div",
1860
- {
1861
- className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1862
- dangerouslySetInnerHTML: { __html: highlightedHtml }
1863
- }
1864
- ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words text-[#24292f] m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
1865
- }
1866
- ),
1867
- /* @__PURE__ */ jsx(
1868
- "textarea",
1869
- {
1870
- ref: textareaRef,
1871
- value: content,
1872
- onChange: handleChange,
1873
- onKeyDown: handleKeyDown,
1874
- className: "relative w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none p-4 text-transparent whitespace-pre-wrap break-words",
1875
- spellCheck: false,
1876
- style: {
1877
- tabSize: 2,
1878
- caretColor: "#24292f",
1879
- wordBreak: "break-word",
1880
- overflowWrap: "break-word"
1881
- }
1882
- }
1883
- )
1884
- ] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: highlightedHtml ? /* @__PURE__ */ jsx(
1885
- "div",
1886
- {
1887
- className: "highlighted-code font-mono text-xs leading-relaxed whitespace-pre-wrap break-words [&_pre]:!bg-transparent [&_pre]:!m-0 [&_pre]:!p-0 [&_pre]:whitespace-pre-wrap [&_code]:!bg-transparent [&_code]:whitespace-pre-wrap [&_code]:break-words",
1888
- dangerouslySetInnerHTML: { __html: highlightedHtml }
1889
- }
1890
- ) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed text-[#24292f]", children: /* @__PURE__ */ jsx("code", { children: content }) }) }) })
1891
- ] });
1892
- }
1893
1920
  function formatFileSize(bytes) {
1894
1921
  if (bytes < 1024) return `${bytes} B`;
1895
1922
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -1980,6 +2007,55 @@ function MediaPreview({ content, mimeType, fileName }) {
1980
2007
  ] })
1981
2008
  ] });
1982
2009
  }
2010
+ function SaveConfirmDialog({
2011
+ isOpen,
2012
+ isSaving,
2013
+ error,
2014
+ onSave,
2015
+ onDiscard,
2016
+ onCancel
2017
+ }) {
2018
+ if (!isOpen) return null;
2019
+ return /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[60] flex items-center justify-center bg-black/80", children: /* @__PURE__ */ jsxs("div", { className: "bg-background border rounded-lg shadow-lg w-full max-w-md", children: [
2020
+ /* @__PURE__ */ jsxs("div", { className: "p-6", children: [
2021
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: "Unsaved Changes" }),
2022
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: "You have unsaved changes. Would you like to save them before closing?" }),
2023
+ error && /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-3", children: [
2024
+ "Save failed: ",
2025
+ error
2026
+ ] })
2027
+ ] }),
2028
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 p-6 pt-0", children: [
2029
+ /* @__PURE__ */ jsx(
2030
+ "button",
2031
+ {
2032
+ onClick: onCancel,
2033
+ disabled: isSaving,
2034
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background hover:bg-accent hover:text-accent-foreground disabled:opacity-50",
2035
+ children: "Cancel"
2036
+ }
2037
+ ),
2038
+ /* @__PURE__ */ jsx(
2039
+ "button",
2040
+ {
2041
+ onClick: onDiscard,
2042
+ disabled: isSaving,
2043
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 border border-input bg-background text-destructive hover:bg-destructive/10 disabled:opacity-50",
2044
+ children: "Discard"
2045
+ }
2046
+ ),
2047
+ /* @__PURE__ */ jsx(
2048
+ "button",
2049
+ {
2050
+ onClick: onSave,
2051
+ disabled: isSaving,
2052
+ className: "inline-flex items-center justify-center rounded-md text-sm font-medium h-9 px-4 py-2 bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50",
2053
+ children: isSaving ? "Saving..." : "Save"
2054
+ }
2055
+ )
2056
+ ] })
2057
+ ] }) });
2058
+ }
1983
2059
  function hashCode(str) {
1984
2060
  let hash = 0;
1985
2061
  for (let i = 0; i < str.length; i++) {
@@ -2416,69 +2492,6 @@ function WidgetPreview({
2416
2492
  /* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
2417
2493
  ] });
2418
2494
  }
2419
- var VFS_BASE_URL = "/vfs";
2420
- var vfsConfigCache = null;
2421
- async function getVFSConfig() {
2422
- if (vfsConfigCache) return vfsConfigCache;
2423
- try {
2424
- const res = await fetch(`${VFS_BASE_URL}/config`);
2425
- if (res.ok) {
2426
- vfsConfigCache = await res.json();
2427
- return vfsConfigCache;
2428
- }
2429
- } catch {
2430
- }
2431
- return { usePaths: false };
2432
- }
2433
- var storeInstance = null;
2434
- function getVFSStore() {
2435
- if (!storeInstance) {
2436
- const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
2437
- storeInstance = new VFSStore(provider, {
2438
- sync: true,
2439
- conflictStrategy: "local-wins"
2440
- });
2441
- }
2442
- return storeInstance;
2443
- }
2444
- async function saveProject(project) {
2445
- const store = getVFSStore();
2446
- await store.saveProject(project);
2447
- }
2448
- async function loadProject(id) {
2449
- const store = getVFSStore();
2450
- return store.loadProject(id);
2451
- }
2452
- async function listProjects() {
2453
- const store = getVFSStore();
2454
- const files = await store.listFiles();
2455
- const projectIds = /* @__PURE__ */ new Set();
2456
- for (const file of files) {
2457
- const id = file.split("/")[0];
2458
- if (id) projectIds.add(id);
2459
- }
2460
- return Array.from(projectIds);
2461
- }
2462
- async function saveFile(path, content) {
2463
- const store = getVFSStore();
2464
- await store.writeFile(path, content);
2465
- }
2466
- async function loadFile(path, encoding) {
2467
- const store = getVFSStore();
2468
- return store.readFile(path, encoding);
2469
- }
2470
- function subscribeToChanges(callback) {
2471
- const store = getVFSStore();
2472
- return store.on("change", callback);
2473
- }
2474
- async function isVFSAvailable() {
2475
- try {
2476
- const res = await fetch(VFS_BASE_URL, { method: "HEAD" });
2477
- return res.ok || res.status === 404;
2478
- } catch {
2479
- return false;
2480
- }
2481
- }
2482
2495
  function createManifest2(services) {
2483
2496
  return {
2484
2497
  name: "preview",
@@ -2591,8 +2604,7 @@ function CodePreview({
2591
2604
  await saveProject(project);
2592
2605
  setLastSavedCode(currentCode);
2593
2606
  setSaveStatus("saved");
2594
- } catch (err) {
2595
- console.warn("[VFS] Failed to save project:", err);
2607
+ } catch {
2596
2608
  setSaveStatus("error");
2597
2609
  }
2598
2610
  }, [currentCode, getProjectId, getEntryFile]);
@@ -2763,8 +2775,7 @@ ${errors.join("\n")}` : "";
2763
2775
  await saveProject(project);
2764
2776
  setLastSavedCode(finalCode);
2765
2777
  setSaveStatus("saved");
2766
- } catch (err) {
2767
- console.warn("[VFS] Failed to save project:", err);
2778
+ } catch {
2768
2779
  setSaveStatus("error");
2769
2780
  }
2770
2781
  })();
@@ -3012,4 +3023,4 @@ function cn(...inputs) {
3012
3023
  return twMerge(clsx(inputs));
3013
3024
  }
3014
3025
 
3015
- export { CodeBlockExtension, CodeBlockView, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, MarkdownPreview, MediaPreview, SaveConfirmDialog, ServicesInspector, WidgetPreview, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFileType, getFiles, getLanguageFromExt, getMimeType, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isCompilable, isImageFile, isMarkdownFile, isMediaFile, isPreviewable, isTextFile, isVFSAvailable, isVideoFile, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };
3026
+ export { CodeBlockExtension, CodeBlockView, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, MarkdownPreview, MediaPreview, SaveConfirmDialog, ServicesInspector, WidgetPreview, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFileType, getFiles, getLanguageFromExt, getMimeType, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isCompilable, isImageFile, isMarkdownFile, isMediaFile, isPreviewable, isTextFile, isVFSAvailable, isVideoFile, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession, useProjectState };