@aprovan/patchwork-editor 0.1.0 → 0.1.1-dev.6bd527d
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/.turbo/turbo-build.log +4 -7
- package/dist/components/CodeBlockExtension.d.ts +2 -0
- package/dist/components/CodePreview.d.ts +13 -0
- package/dist/components/MarkdownEditor.d.ts +10 -0
- package/dist/components/ServicesInspector.d.ts +49 -0
- package/dist/components/edit/CodeBlockView.d.ts +7 -0
- package/dist/components/edit/EditHistory.d.ts +10 -0
- package/dist/components/edit/EditModal.d.ts +20 -0
- package/dist/components/edit/FileTree.d.ts +8 -0
- package/dist/components/edit/MediaPreview.d.ts +6 -0
- package/dist/components/edit/SaveConfirmDialog.d.ts +9 -0
- package/dist/components/edit/api.d.ts +8 -0
- package/dist/components/edit/fileTypes.d.ts +14 -0
- package/dist/components/edit/index.d.ts +10 -0
- package/dist/components/edit/types.d.ts +41 -0
- package/dist/components/edit/useEditSession.d.ts +9 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/index.d.ts +3 -331
- package/dist/index.js +707 -142
- package/dist/lib/code-extractor.d.ts +44 -0
- package/dist/lib/diff.d.ts +90 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/vfs.d.ts +36 -0
- package/package.json +4 -4
- package/src/components/CodeBlockExtension.tsx +1 -1
- package/src/components/CodePreview.tsx +64 -4
- package/src/components/edit/CodeBlockView.tsx +72 -0
- package/src/components/edit/EditModal.tsx +169 -28
- package/src/components/edit/FileTree.tsx +67 -13
- package/src/components/edit/MediaPreview.tsx +106 -0
- package/src/components/edit/SaveConfirmDialog.tsx +60 -0
- package/src/components/edit/fileTypes.ts +125 -0
- package/src/components/edit/index.ts +4 -0
- package/src/components/edit/types.ts +3 -0
- package/src/components/edit/useEditSession.ts +56 -11
- package/src/index.ts +17 -0
- package/src/lib/diff.ts +2 -1
- package/src/lib/vfs.ts +28 -10
- package/tsup.config.ts +10 -5
package/dist/index.js
CHANGED
|
@@ -2,8 +2,8 @@ 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 { Pencil, Loader2, RotateCcw, FileCode, FolderTree, Eye, Code,
|
|
6
|
-
import { createSingleFileProject,
|
|
5
|
+
import { AlertCircle, FileImage, FileVideo, Pencil, Loader2, RotateCcw, FileCode, FolderTree, Eye, Code, Save, X, Send, MessageSquare, Cloud, Check, Server, ChevronDown, ChevronRight, Folder, File, Upload } 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
9
|
import StarterKit from '@tiptap/starter-kit';
|
|
@@ -119,7 +119,7 @@ var CodeBlockExtension = Node.create({
|
|
|
119
119
|
default: null,
|
|
120
120
|
parseHTML: (element) => {
|
|
121
121
|
const { languageClassPrefix } = this.options;
|
|
122
|
-
const classNames =
|
|
122
|
+
const classNames = Array.from(element.firstElementChild?.classList || []);
|
|
123
123
|
const languages = classNames.filter((className) => className.startsWith(languageClassPrefix)).map((className) => className.replace(languageClassPrefix, ""));
|
|
124
124
|
return languages[0] || null;
|
|
125
125
|
},
|
|
@@ -340,6 +340,7 @@ function applyDiffs(code, diffs, options = {}) {
|
|
|
340
340
|
return { code: result, applied, failed, warning };
|
|
341
341
|
}
|
|
342
342
|
function hasDiffBlocks(text) {
|
|
343
|
+
DIFF_BLOCK_REGEX.lastIndex = 0;
|
|
343
344
|
return DIFF_BLOCK_REGEX.test(text);
|
|
344
345
|
}
|
|
345
346
|
function extractTextWithoutDiffs(text) {
|
|
@@ -435,11 +436,23 @@ function cloneProject(project) {
|
|
|
435
436
|
};
|
|
436
437
|
}
|
|
437
438
|
function useEditSession(options) {
|
|
438
|
-
const {
|
|
439
|
+
const {
|
|
440
|
+
originalCode,
|
|
441
|
+
originalProject: providedProject,
|
|
442
|
+
compile,
|
|
443
|
+
apiEndpoint
|
|
444
|
+
} = options;
|
|
445
|
+
console.log(
|
|
446
|
+
"[useEditSession] providedProject:",
|
|
447
|
+
providedProject?.id,
|
|
448
|
+
"files:",
|
|
449
|
+
providedProject ? Array.from(providedProject.files.keys()) : "none"
|
|
450
|
+
);
|
|
439
451
|
const originalProject = useMemo(
|
|
440
|
-
() => createSingleFileProject(originalCode),
|
|
441
|
-
[originalCode]
|
|
452
|
+
() => providedProject ?? createSingleFileProject(originalCode ?? ""),
|
|
453
|
+
[providedProject, originalCode]
|
|
442
454
|
);
|
|
455
|
+
const lastSyncedProjectRef = useRef(originalProject);
|
|
443
456
|
const [project, setProject] = useState(originalProject);
|
|
444
457
|
const [activeFile, setActiveFile] = useState(originalProject.entry);
|
|
445
458
|
const [history, setHistory] = useState([]);
|
|
@@ -447,6 +460,16 @@ function useEditSession(options) {
|
|
|
447
460
|
const [error, setError] = useState(null);
|
|
448
461
|
const [streamingNotes, setStreamingNotes] = useState([]);
|
|
449
462
|
const [pendingPrompt, setPendingPrompt] = useState(null);
|
|
463
|
+
useEffect(() => {
|
|
464
|
+
if (originalProject !== lastSyncedProjectRef.current) {
|
|
465
|
+
lastSyncedProjectRef.current = originalProject;
|
|
466
|
+
setProject(originalProject);
|
|
467
|
+
setActiveFile(originalProject.entry);
|
|
468
|
+
setHistory([]);
|
|
469
|
+
setError(null);
|
|
470
|
+
setStreamingNotes([]);
|
|
471
|
+
}
|
|
472
|
+
}, [originalProject]);
|
|
450
473
|
const performEdit = useCallback(
|
|
451
474
|
async (currentCode2, prompt, isRetry = false) => {
|
|
452
475
|
const entries = [];
|
|
@@ -536,6 +559,21 @@ Please fix this error.`;
|
|
|
536
559
|
},
|
|
537
560
|
[activeFile]
|
|
538
561
|
);
|
|
562
|
+
const replaceFile = useCallback(
|
|
563
|
+
(path, content, encoding = "utf8") => {
|
|
564
|
+
setProject((prev) => {
|
|
565
|
+
const updated = cloneProject(prev);
|
|
566
|
+
const file = updated.files.get(path);
|
|
567
|
+
if (file) {
|
|
568
|
+
updated.files.set(path, { ...file, content, encoding });
|
|
569
|
+
} else {
|
|
570
|
+
updated.files.set(path, { path, content, encoding });
|
|
571
|
+
}
|
|
572
|
+
return updated;
|
|
573
|
+
});
|
|
574
|
+
},
|
|
575
|
+
[]
|
|
576
|
+
);
|
|
539
577
|
const clearError = useCallback(() => {
|
|
540
578
|
setError(null);
|
|
541
579
|
}, []);
|
|
@@ -552,7 +590,8 @@ Please fix this error.`;
|
|
|
552
590
|
revert,
|
|
553
591
|
updateActiveFile,
|
|
554
592
|
setActiveFile,
|
|
555
|
-
clearError
|
|
593
|
+
clearError,
|
|
594
|
+
replaceFile
|
|
556
595
|
};
|
|
557
596
|
}
|
|
558
597
|
function ProgressNote({ text, isLatest }) {
|
|
@@ -811,6 +850,110 @@ function MarkdownEditor({
|
|
|
811
850
|
}
|
|
812
851
|
);
|
|
813
852
|
}
|
|
853
|
+
|
|
854
|
+
// src/components/edit/fileTypes.ts
|
|
855
|
+
var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
856
|
+
var MEDIA_EXTENSIONS = [".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".mp4", ".mov", ".webm"];
|
|
857
|
+
var TEXT_EXTENSIONS = [".json", ".yaml", ".yml", ".md", ".txt", ".css", ".html", ".xml", ".toml"];
|
|
858
|
+
var EXTENSION_TO_LANGUAGE = {
|
|
859
|
+
".tsx": "tsx",
|
|
860
|
+
".jsx": "jsx",
|
|
861
|
+
".ts": "typescript",
|
|
862
|
+
".js": "javascript",
|
|
863
|
+
".json": "json",
|
|
864
|
+
".yaml": "yaml",
|
|
865
|
+
".yml": "yaml",
|
|
866
|
+
".md": "markdown",
|
|
867
|
+
".txt": "text",
|
|
868
|
+
".css": "css",
|
|
869
|
+
".html": "html",
|
|
870
|
+
".xml": "xml",
|
|
871
|
+
".toml": "toml",
|
|
872
|
+
".svg": "xml"
|
|
873
|
+
};
|
|
874
|
+
var EXTENSION_TO_MIME = {
|
|
875
|
+
".tsx": "text/typescript-jsx",
|
|
876
|
+
".jsx": "text/javascript-jsx",
|
|
877
|
+
".ts": "text/typescript",
|
|
878
|
+
".js": "text/javascript",
|
|
879
|
+
".json": "application/json",
|
|
880
|
+
".yaml": "text/yaml",
|
|
881
|
+
".yml": "text/yaml",
|
|
882
|
+
".md": "text/markdown",
|
|
883
|
+
".txt": "text/plain",
|
|
884
|
+
".css": "text/css",
|
|
885
|
+
".html": "text/html",
|
|
886
|
+
".xml": "application/xml",
|
|
887
|
+
".toml": "text/toml",
|
|
888
|
+
".svg": "image/svg+xml",
|
|
889
|
+
".png": "image/png",
|
|
890
|
+
".jpg": "image/jpeg",
|
|
891
|
+
".jpeg": "image/jpeg",
|
|
892
|
+
".gif": "image/gif",
|
|
893
|
+
".webp": "image/webp",
|
|
894
|
+
".mp4": "video/mp4",
|
|
895
|
+
".mov": "video/quicktime",
|
|
896
|
+
".webm": "video/webm"
|
|
897
|
+
};
|
|
898
|
+
function getExtension(path) {
|
|
899
|
+
const lastDot = path.lastIndexOf(".");
|
|
900
|
+
if (lastDot === -1) return "";
|
|
901
|
+
return path.slice(lastDot).toLowerCase();
|
|
902
|
+
}
|
|
903
|
+
function getFileType(path) {
|
|
904
|
+
const ext = getExtension(path);
|
|
905
|
+
if (COMPILABLE_EXTENSIONS.includes(ext)) {
|
|
906
|
+
return {
|
|
907
|
+
category: "compilable",
|
|
908
|
+
language: EXTENSION_TO_LANGUAGE[ext] ?? null,
|
|
909
|
+
mimeType: EXTENSION_TO_MIME[ext] ?? "text/plain"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (TEXT_EXTENSIONS.includes(ext)) {
|
|
913
|
+
return {
|
|
914
|
+
category: "text",
|
|
915
|
+
language: EXTENSION_TO_LANGUAGE[ext] ?? null,
|
|
916
|
+
mimeType: EXTENSION_TO_MIME[ext] ?? "text/plain"
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
if (MEDIA_EXTENSIONS.includes(ext)) {
|
|
920
|
+
return {
|
|
921
|
+
category: "media",
|
|
922
|
+
language: ext === ".svg" ? "xml" : null,
|
|
923
|
+
mimeType: EXTENSION_TO_MIME[ext] ?? "application/octet-stream"
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
category: "binary",
|
|
928
|
+
language: null,
|
|
929
|
+
mimeType: "application/octet-stream"
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function isCompilable(path) {
|
|
933
|
+
return COMPILABLE_EXTENSIONS.includes(getExtension(path));
|
|
934
|
+
}
|
|
935
|
+
function isMediaFile(path) {
|
|
936
|
+
return MEDIA_EXTENSIONS.includes(getExtension(path));
|
|
937
|
+
}
|
|
938
|
+
function isTextFile(path) {
|
|
939
|
+
return TEXT_EXTENSIONS.includes(getExtension(path));
|
|
940
|
+
}
|
|
941
|
+
function getLanguageFromExt(path) {
|
|
942
|
+
const ext = getExtension(path);
|
|
943
|
+
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
944
|
+
}
|
|
945
|
+
function getMimeType(path) {
|
|
946
|
+
const ext = getExtension(path);
|
|
947
|
+
return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
|
|
948
|
+
}
|
|
949
|
+
function isImageFile(path) {
|
|
950
|
+
const ext = getExtension(path);
|
|
951
|
+
return [".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(ext);
|
|
952
|
+
}
|
|
953
|
+
function isVideoFile(path) {
|
|
954
|
+
const ext = getExtension(path);
|
|
955
|
+
return [".mp4", ".mov", ".webm"].includes(ext);
|
|
956
|
+
}
|
|
814
957
|
function buildTree(files) {
|
|
815
958
|
const root = { name: "", path: "", isDir: true, children: [] };
|
|
816
959
|
for (const file of files) {
|
|
@@ -839,8 +982,26 @@ function buildTree(files) {
|
|
|
839
982
|
});
|
|
840
983
|
return root;
|
|
841
984
|
}
|
|
842
|
-
function TreeNodeComponent({ node, activeFile, onSelect, depth = 0 }) {
|
|
985
|
+
function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth = 0 }) {
|
|
843
986
|
const [expanded, setExpanded] = useState(true);
|
|
987
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
988
|
+
const fileInputRef = useRef(null);
|
|
989
|
+
const handleUploadClick = useCallback((e) => {
|
|
990
|
+
e.stopPropagation();
|
|
991
|
+
fileInputRef.current?.click();
|
|
992
|
+
}, []);
|
|
993
|
+
const handleFileChange = useCallback((e) => {
|
|
994
|
+
const file = e.target.files?.[0];
|
|
995
|
+
if (!file || !onReplaceFile) return;
|
|
996
|
+
const reader = new FileReader();
|
|
997
|
+
reader.onload = () => {
|
|
998
|
+
const result = reader.result;
|
|
999
|
+
const base64 = result.split(",")[1] ?? "";
|
|
1000
|
+
onReplaceFile(node.path, base64, "base64");
|
|
1001
|
+
};
|
|
1002
|
+
reader.readAsDataURL(file);
|
|
1003
|
+
e.target.value = "";
|
|
1004
|
+
}, [node.path, onReplaceFile]);
|
|
844
1005
|
if (!node.name) {
|
|
845
1006
|
return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
|
|
846
1007
|
TreeNodeComponent,
|
|
@@ -848,12 +1009,15 @@ function TreeNodeComponent({ node, activeFile, onSelect, depth = 0 }) {
|
|
|
848
1009
|
node: child,
|
|
849
1010
|
activeFile,
|
|
850
1011
|
onSelect,
|
|
1012
|
+
onReplaceFile,
|
|
851
1013
|
depth
|
|
852
1014
|
},
|
|
853
1015
|
child.path
|
|
854
1016
|
)) });
|
|
855
1017
|
}
|
|
856
1018
|
const isActive = node.path === activeFile;
|
|
1019
|
+
const isMedia = !node.isDir && isMediaFile(node.path);
|
|
1020
|
+
const showUpload = isMedia && isHovered && onReplaceFile;
|
|
857
1021
|
if (node.isDir) {
|
|
858
1022
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
859
1023
|
/* @__PURE__ */ jsxs(
|
|
@@ -875,6 +1039,7 @@ function TreeNodeComponent({ node, activeFile, onSelect, depth = 0 }) {
|
|
|
875
1039
|
node: child,
|
|
876
1040
|
activeFile,
|
|
877
1041
|
onSelect,
|
|
1042
|
+
onReplaceFile,
|
|
878
1043
|
depth: depth + 1
|
|
879
1044
|
},
|
|
880
1045
|
child.path
|
|
@@ -882,19 +1047,48 @@ function TreeNodeComponent({ node, activeFile, onSelect, depth = 0 }) {
|
|
|
882
1047
|
] });
|
|
883
1048
|
}
|
|
884
1049
|
return /* @__PURE__ */ jsxs(
|
|
885
|
-
"
|
|
1050
|
+
"div",
|
|
886
1051
|
{
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1052
|
+
className: "relative",
|
|
1053
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1054
|
+
onMouseLeave: () => setIsHovered(false),
|
|
890
1055
|
children: [
|
|
891
|
-
/* @__PURE__ */
|
|
892
|
-
|
|
1056
|
+
/* @__PURE__ */ jsxs(
|
|
1057
|
+
"button",
|
|
1058
|
+
{
|
|
1059
|
+
onClick: () => onSelect(node.path),
|
|
1060
|
+
className: `flex items-center gap-1 w-full px-2 py-1 text-left text-sm hover:bg-muted/50 rounded ${isActive ? "bg-primary/10 text-primary" : ""}`,
|
|
1061
|
+
style: { paddingLeft: `${depth * 12 + 20}px` },
|
|
1062
|
+
children: [
|
|
1063
|
+
/* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1064
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: node.name }),
|
|
1065
|
+
showUpload && /* @__PURE__ */ jsx(
|
|
1066
|
+
"span",
|
|
1067
|
+
{
|
|
1068
|
+
onClick: handleUploadClick,
|
|
1069
|
+
className: "p-0.5 hover:bg-primary/20 rounded cursor-pointer",
|
|
1070
|
+
title: "Replace file",
|
|
1071
|
+
children: /* @__PURE__ */ jsx(Upload, { className: "h-3 w-3 text-primary" })
|
|
1072
|
+
}
|
|
1073
|
+
)
|
|
1074
|
+
]
|
|
1075
|
+
}
|
|
1076
|
+
),
|
|
1077
|
+
isMedia && /* @__PURE__ */ jsx(
|
|
1078
|
+
"input",
|
|
1079
|
+
{
|
|
1080
|
+
ref: fileInputRef,
|
|
1081
|
+
type: "file",
|
|
1082
|
+
className: "hidden",
|
|
1083
|
+
accept: "image/*,video/*",
|
|
1084
|
+
onChange: handleFileChange
|
|
1085
|
+
}
|
|
1086
|
+
)
|
|
893
1087
|
]
|
|
894
1088
|
}
|
|
895
1089
|
);
|
|
896
1090
|
}
|
|
897
|
-
function FileTree({ files, activeFile, onSelectFile }) {
|
|
1091
|
+
function FileTree({ files, activeFile, onSelectFile, onReplaceFile }) {
|
|
898
1092
|
const tree = useMemo(() => buildTree(files), [files]);
|
|
899
1093
|
return /* @__PURE__ */ jsxs("div", { className: "w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
|
|
900
1094
|
/* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: "Files" }),
|
|
@@ -903,11 +1097,196 @@ function FileTree({ files, activeFile, onSelectFile }) {
|
|
|
903
1097
|
{
|
|
904
1098
|
node: tree,
|
|
905
1099
|
activeFile,
|
|
906
|
-
onSelect: onSelectFile
|
|
1100
|
+
onSelect: onSelectFile,
|
|
1101
|
+
onReplaceFile
|
|
907
1102
|
}
|
|
908
1103
|
) })
|
|
909
1104
|
] });
|
|
910
1105
|
}
|
|
1106
|
+
function SaveConfirmDialog({
|
|
1107
|
+
isOpen,
|
|
1108
|
+
isSaving,
|
|
1109
|
+
error,
|
|
1110
|
+
onSave,
|
|
1111
|
+
onDiscard,
|
|
1112
|
+
onCancel
|
|
1113
|
+
}) {
|
|
1114
|
+
if (!isOpen) return null;
|
|
1115
|
+
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: [
|
|
1116
|
+
/* @__PURE__ */ jsxs("div", { className: "p-6", children: [
|
|
1117
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold leading-none tracking-tight", children: "Unsaved Changes" }),
|
|
1118
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground mt-2", children: "You have unsaved changes. Would you like to save them before closing?" }),
|
|
1119
|
+
error && /* @__PURE__ */ jsxs("p", { className: "text-sm text-destructive mt-3", children: [
|
|
1120
|
+
"Save failed: ",
|
|
1121
|
+
error
|
|
1122
|
+
] })
|
|
1123
|
+
] }),
|
|
1124
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 p-6 pt-0", children: [
|
|
1125
|
+
/* @__PURE__ */ jsx(
|
|
1126
|
+
"button",
|
|
1127
|
+
{
|
|
1128
|
+
onClick: onCancel,
|
|
1129
|
+
disabled: isSaving,
|
|
1130
|
+
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",
|
|
1131
|
+
children: "Cancel"
|
|
1132
|
+
}
|
|
1133
|
+
),
|
|
1134
|
+
/* @__PURE__ */ jsx(
|
|
1135
|
+
"button",
|
|
1136
|
+
{
|
|
1137
|
+
onClick: onDiscard,
|
|
1138
|
+
disabled: isSaving,
|
|
1139
|
+
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",
|
|
1140
|
+
children: "Discard"
|
|
1141
|
+
}
|
|
1142
|
+
),
|
|
1143
|
+
/* @__PURE__ */ jsx(
|
|
1144
|
+
"button",
|
|
1145
|
+
{
|
|
1146
|
+
onClick: onSave,
|
|
1147
|
+
disabled: isSaving,
|
|
1148
|
+
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",
|
|
1149
|
+
children: isSaving ? "Saving..." : "Save"
|
|
1150
|
+
}
|
|
1151
|
+
)
|
|
1152
|
+
] })
|
|
1153
|
+
] }) });
|
|
1154
|
+
}
|
|
1155
|
+
function CodeBlockView({ content, language, editable = false, onChange }) {
|
|
1156
|
+
const textareaRef = useRef(null);
|
|
1157
|
+
useEffect(() => {
|
|
1158
|
+
if (textareaRef.current) {
|
|
1159
|
+
textareaRef.current.style.height = "auto";
|
|
1160
|
+
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
|
1161
|
+
}
|
|
1162
|
+
}, [content]);
|
|
1163
|
+
const handleChange = useCallback(
|
|
1164
|
+
(e) => {
|
|
1165
|
+
onChange?.(e.target.value);
|
|
1166
|
+
},
|
|
1167
|
+
[onChange]
|
|
1168
|
+
);
|
|
1169
|
+
const handleKeyDown = useCallback(
|
|
1170
|
+
(e) => {
|
|
1171
|
+
if (e.key === "Tab") {
|
|
1172
|
+
e.preventDefault();
|
|
1173
|
+
const target = e.target;
|
|
1174
|
+
const start = target.selectionStart;
|
|
1175
|
+
const end = target.selectionEnd;
|
|
1176
|
+
const value = target.value;
|
|
1177
|
+
const newValue = value.substring(0, start) + " " + value.substring(end);
|
|
1178
|
+
onChange?.(newValue);
|
|
1179
|
+
requestAnimationFrame(() => {
|
|
1180
|
+
target.selectionStart = target.selectionEnd = start + 2;
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
[onChange]
|
|
1185
|
+
);
|
|
1186
|
+
const langLabel = language || "text";
|
|
1187
|
+
return /* @__PURE__ */ jsxs("div", { className: "h-full flex flex-col bg-muted/10", children: [
|
|
1188
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center justify-between px-4 py-2 bg-muted/30 border-b text-xs", children: /* @__PURE__ */ jsx("span", { className: "font-mono text-muted-foreground", children: langLabel }) }),
|
|
1189
|
+
editable ? /* @__PURE__ */ jsx(
|
|
1190
|
+
"textarea",
|
|
1191
|
+
{
|
|
1192
|
+
ref: textareaRef,
|
|
1193
|
+
value: content,
|
|
1194
|
+
onChange: handleChange,
|
|
1195
|
+
onKeyDown: handleKeyDown,
|
|
1196
|
+
className: "w-full min-h-full font-mono text-xs leading-relaxed bg-transparent border-none outline-none resize-none",
|
|
1197
|
+
spellCheck: false,
|
|
1198
|
+
style: {
|
|
1199
|
+
tabSize: 2,
|
|
1200
|
+
WebkitTextFillColor: "inherit"
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
) : /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0 leading-relaxed", children: /* @__PURE__ */ jsx("code", { children: content }) })
|
|
1204
|
+
] });
|
|
1205
|
+
}
|
|
1206
|
+
function formatFileSize(bytes) {
|
|
1207
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1208
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1209
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1210
|
+
}
|
|
1211
|
+
function getDataUrl(content, mimeType) {
|
|
1212
|
+
if (content.startsWith("data:")) {
|
|
1213
|
+
return content;
|
|
1214
|
+
}
|
|
1215
|
+
return `data:${mimeType};base64,${content}`;
|
|
1216
|
+
}
|
|
1217
|
+
function MediaPreview({ content, mimeType, fileName }) {
|
|
1218
|
+
const [dimensions, setDimensions] = useState(null);
|
|
1219
|
+
const [error, setError] = useState(null);
|
|
1220
|
+
const dataUrl = getDataUrl(content, mimeType);
|
|
1221
|
+
const isImage = isImageFile(fileName);
|
|
1222
|
+
const isVideo = isVideoFile(fileName);
|
|
1223
|
+
content.length;
|
|
1224
|
+
const estimatedBytes = content.startsWith("data:") ? Math.floor((content.split(",")[1]?.length ?? 0) * 0.75) : Math.floor(content.length * 0.75);
|
|
1225
|
+
useEffect(() => {
|
|
1226
|
+
setDimensions(null);
|
|
1227
|
+
setError(null);
|
|
1228
|
+
if (isImage) {
|
|
1229
|
+
const img = new Image();
|
|
1230
|
+
img.onload = () => {
|
|
1231
|
+
setDimensions({ width: img.naturalWidth, height: img.naturalHeight });
|
|
1232
|
+
};
|
|
1233
|
+
img.onerror = () => {
|
|
1234
|
+
setError("Failed to load image");
|
|
1235
|
+
};
|
|
1236
|
+
img.src = dataUrl;
|
|
1237
|
+
}
|
|
1238
|
+
}, [dataUrl, isImage]);
|
|
1239
|
+
if (error) {
|
|
1240
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-full p-8 text-muted-foreground", children: [
|
|
1241
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-12 w-12 mb-4 text-destructive" }),
|
|
1242
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: error })
|
|
1243
|
+
] });
|
|
1244
|
+
}
|
|
1245
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center h-full p-8 bg-muted/20", children: [
|
|
1246
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 flex items-center justify-center w-full max-h-[60vh] overflow-hidden", children: [
|
|
1247
|
+
isImage && /* @__PURE__ */ jsx(
|
|
1248
|
+
"img",
|
|
1249
|
+
{
|
|
1250
|
+
src: dataUrl,
|
|
1251
|
+
alt: fileName,
|
|
1252
|
+
className: "max-w-full max-h-full object-contain rounded shadow-sm",
|
|
1253
|
+
style: { maxHeight: "calc(60vh - 2rem)" }
|
|
1254
|
+
}
|
|
1255
|
+
),
|
|
1256
|
+
isVideo && /* @__PURE__ */ jsx(
|
|
1257
|
+
"video",
|
|
1258
|
+
{
|
|
1259
|
+
src: dataUrl,
|
|
1260
|
+
controls: true,
|
|
1261
|
+
className: "max-w-full max-h-full rounded shadow-sm",
|
|
1262
|
+
style: { maxHeight: "calc(60vh - 2rem)" },
|
|
1263
|
+
children: "Your browser does not support video playback."
|
|
1264
|
+
}
|
|
1265
|
+
),
|
|
1266
|
+
!isImage && !isVideo && /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center text-muted-foreground", children: [
|
|
1267
|
+
/* @__PURE__ */ jsx(FileImage, { className: "h-16 w-16 mb-4" }),
|
|
1268
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm", children: "Preview not available for this file type" })
|
|
1269
|
+
] })
|
|
1270
|
+
] }),
|
|
1271
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-6 text-center text-sm text-muted-foreground space-y-1", children: [
|
|
1272
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2", children: [
|
|
1273
|
+
isImage && /* @__PURE__ */ jsx(FileImage, { className: "h-4 w-4" }),
|
|
1274
|
+
isVideo && /* @__PURE__ */ jsx(FileVideo, { className: "h-4 w-4" }),
|
|
1275
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: fileName })
|
|
1276
|
+
] }),
|
|
1277
|
+
/* @__PURE__ */ jsxs("div", { className: "text-xs space-x-3", children: [
|
|
1278
|
+
dimensions && /* @__PURE__ */ jsxs("span", { children: [
|
|
1279
|
+
dimensions.width,
|
|
1280
|
+
" \xD7 ",
|
|
1281
|
+
dimensions.height,
|
|
1282
|
+
" px"
|
|
1283
|
+
] }),
|
|
1284
|
+
/* @__PURE__ */ jsx("span", { children: formatFileSize(estimatedBytes) }),
|
|
1285
|
+
/* @__PURE__ */ jsx("span", { className: "text-muted-foreground/60", children: mimeType })
|
|
1286
|
+
] })
|
|
1287
|
+
] })
|
|
1288
|
+
] });
|
|
1289
|
+
}
|
|
911
1290
|
function hashCode(str) {
|
|
912
1291
|
let hash = 0;
|
|
913
1292
|
for (let i = 0; i < str.length; i++) {
|
|
@@ -918,22 +1297,37 @@ function hashCode(str) {
|
|
|
918
1297
|
function EditModal({
|
|
919
1298
|
isOpen,
|
|
920
1299
|
onClose,
|
|
1300
|
+
onSave,
|
|
1301
|
+
onSaveProject,
|
|
921
1302
|
renderPreview,
|
|
922
1303
|
renderLoading,
|
|
923
1304
|
renderError,
|
|
924
1305
|
previewError,
|
|
925
1306
|
previewLoading,
|
|
1307
|
+
initialState = {},
|
|
1308
|
+
hideFileTree = false,
|
|
926
1309
|
...sessionOptions
|
|
927
1310
|
}) {
|
|
928
|
-
const [showPreview, setShowPreview] = useState(true);
|
|
929
|
-
const [showTree, setShowTree] = useState(
|
|
1311
|
+
const [showPreview, setShowPreview] = useState(initialState?.showPreview ?? true);
|
|
1312
|
+
const [showTree, setShowTree] = useState(
|
|
1313
|
+
hideFileTree ? false : initialState?.showTree ?? false
|
|
1314
|
+
);
|
|
930
1315
|
const [editInput, setEditInput] = useState("");
|
|
931
1316
|
const [bobbinChanges, setBobbinChanges] = useState([]);
|
|
932
1317
|
const [previewContainer, setPreviewContainer] = useState(null);
|
|
1318
|
+
const [showConfirm, setShowConfirm] = useState(false);
|
|
1319
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
1320
|
+
const [saveError, setSaveError] = useState(null);
|
|
1321
|
+
const [pendingClose, setPendingClose] = useState(null);
|
|
1322
|
+
const currentCodeRef = useRef("");
|
|
933
1323
|
const session = useEditSession(sessionOptions);
|
|
934
1324
|
const code = getActiveContent(session);
|
|
1325
|
+
currentCodeRef.current = code;
|
|
935
1326
|
const files = useMemo(() => getFiles(session.project), [session.project]);
|
|
936
1327
|
const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
|
|
1328
|
+
const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
|
|
1329
|
+
const isCompilableFile = isCompilable(session.activeFile);
|
|
1330
|
+
const showPreviewToggle = isCompilableFile;
|
|
937
1331
|
const handleBobbinChanges = useCallback((changes) => {
|
|
938
1332
|
setBobbinChanges(changes);
|
|
939
1333
|
}, []);
|
|
@@ -954,146 +1348,253 @@ ${bobbinYaml}
|
|
|
954
1348
|
setEditInput("");
|
|
955
1349
|
setBobbinChanges([]);
|
|
956
1350
|
};
|
|
957
|
-
const
|
|
1351
|
+
const hasSaveHandler = onSave || onSaveProject;
|
|
1352
|
+
const handleClose = useCallback(() => {
|
|
958
1353
|
const editCount = session.history.length;
|
|
959
1354
|
const finalCode = code;
|
|
1355
|
+
const hasUnsavedChanges = editCount > 0 && finalCode !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
|
|
1356
|
+
if (hasUnsavedChanges && hasSaveHandler) {
|
|
1357
|
+
setPendingClose({ code: finalCode, count: editCount });
|
|
1358
|
+
setShowConfirm(true);
|
|
1359
|
+
} else {
|
|
1360
|
+
setEditInput("");
|
|
1361
|
+
session.clearError();
|
|
1362
|
+
onClose(finalCode, editCount);
|
|
1363
|
+
}
|
|
1364
|
+
}, [code, session, hasSaveHandler, onClose]);
|
|
1365
|
+
const handleSaveAndClose = useCallback(async () => {
|
|
1366
|
+
if (!pendingClose || !hasSaveHandler) return;
|
|
1367
|
+
setIsSaving(true);
|
|
1368
|
+
setSaveError(null);
|
|
1369
|
+
try {
|
|
1370
|
+
if (onSaveProject) {
|
|
1371
|
+
await onSaveProject(session.project);
|
|
1372
|
+
} else if (onSave) {
|
|
1373
|
+
await onSave(pendingClose.code);
|
|
1374
|
+
}
|
|
1375
|
+
setShowConfirm(false);
|
|
1376
|
+
setEditInput("");
|
|
1377
|
+
session.clearError();
|
|
1378
|
+
onClose(pendingClose.code, pendingClose.count);
|
|
1379
|
+
setPendingClose(null);
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
1382
|
+
} finally {
|
|
1383
|
+
setIsSaving(false);
|
|
1384
|
+
}
|
|
1385
|
+
}, [pendingClose, onSave, onSaveProject, session, onClose]);
|
|
1386
|
+
const handleDiscard = useCallback(() => {
|
|
1387
|
+
if (!pendingClose) return;
|
|
1388
|
+
setShowConfirm(false);
|
|
960
1389
|
setEditInput("");
|
|
961
1390
|
session.clearError();
|
|
962
|
-
onClose(
|
|
963
|
-
|
|
1391
|
+
onClose(pendingClose.code, pendingClose.count);
|
|
1392
|
+
setPendingClose(null);
|
|
1393
|
+
}, [pendingClose, session, onClose]);
|
|
1394
|
+
const handleCancelClose = useCallback(() => {
|
|
1395
|
+
setShowConfirm(false);
|
|
1396
|
+
setPendingClose(null);
|
|
1397
|
+
setSaveError(null);
|
|
1398
|
+
}, []);
|
|
1399
|
+
const handleDirectSave = useCallback(async () => {
|
|
1400
|
+
if (!hasSaveHandler) return;
|
|
1401
|
+
setIsSaving(true);
|
|
1402
|
+
setSaveError(null);
|
|
1403
|
+
try {
|
|
1404
|
+
if (onSaveProject) {
|
|
1405
|
+
await onSaveProject(session.project);
|
|
1406
|
+
} else if (onSave && currentCodeRef.current) {
|
|
1407
|
+
await onSave(currentCodeRef.current);
|
|
1408
|
+
}
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
1411
|
+
} finally {
|
|
1412
|
+
setIsSaving(false);
|
|
1413
|
+
}
|
|
1414
|
+
}, [onSave, onSaveProject, session.project]);
|
|
964
1415
|
if (!isOpen) return null;
|
|
965
|
-
return /* @__PURE__ */
|
|
966
|
-
/* @__PURE__ */
|
|
967
|
-
/* @__PURE__ */
|
|
968
|
-
|
|
969
|
-
/* @__PURE__ */
|
|
970
|
-
|
|
1416
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1417
|
+
/* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-8", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col bg-background rounded-lg shadow-xl w-full h-full max-w-6xl max-h-[90vh] overflow-hidden", children: [
|
|
1418
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 bg-background border-b-2", children: [
|
|
1419
|
+
/* @__PURE__ */ jsx(Pencil, { className: "h-4 w-4 text-primary" }),
|
|
1420
|
+
session.isApplying && /* @__PURE__ */ jsxs("span", { className: "text-xs font-medium text-primary flex items-center gap-1 ml-2", children: [
|
|
1421
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
|
|
1422
|
+
"Applying edits..."
|
|
1423
|
+
] }),
|
|
1424
|
+
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-2", children: [
|
|
1425
|
+
hasChanges && /* @__PURE__ */ jsx(
|
|
1426
|
+
"button",
|
|
1427
|
+
{
|
|
1428
|
+
onClick: session.revert,
|
|
1429
|
+
className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary",
|
|
1430
|
+
title: "Revert to original",
|
|
1431
|
+
children: /* @__PURE__ */ jsx(RotateCcw, { className: "h-3 w-3" })
|
|
1432
|
+
}
|
|
1433
|
+
),
|
|
1434
|
+
!hideFileTree && /* @__PURE__ */ jsx(
|
|
1435
|
+
"button",
|
|
1436
|
+
{
|
|
1437
|
+
onClick: () => setShowTree(!showTree),
|
|
1438
|
+
className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showTree ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
|
|
1439
|
+
title: showTree ? "Single file" : "File tree",
|
|
1440
|
+
children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
|
|
1441
|
+
}
|
|
1442
|
+
),
|
|
1443
|
+
showPreviewToggle && /* @__PURE__ */ jsxs(
|
|
1444
|
+
"button",
|
|
1445
|
+
{
|
|
1446
|
+
onClick: () => setShowPreview(!showPreview),
|
|
1447
|
+
className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
|
|
1448
|
+
children: [
|
|
1449
|
+
showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
|
|
1450
|
+
showPreview ? "Preview" : "Code"
|
|
1451
|
+
]
|
|
1452
|
+
}
|
|
1453
|
+
),
|
|
1454
|
+
hasSaveHandler && /* @__PURE__ */ jsxs(
|
|
1455
|
+
"button",
|
|
1456
|
+
{
|
|
1457
|
+
onClick: handleDirectSave,
|
|
1458
|
+
disabled: isSaving,
|
|
1459
|
+
className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary disabled:opacity-50",
|
|
1460
|
+
title: "Save changes",
|
|
1461
|
+
children: [
|
|
1462
|
+
isSaving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3 w-3" }),
|
|
1463
|
+
"Save"
|
|
1464
|
+
]
|
|
1465
|
+
}
|
|
1466
|
+
),
|
|
1467
|
+
/* @__PURE__ */ jsxs(
|
|
1468
|
+
"button",
|
|
1469
|
+
{
|
|
1470
|
+
onClick: handleClose,
|
|
1471
|
+
className: "px-2 py-1 text-xs rounded flex items-center gap-1 bg-primary text-primary-foreground hover:bg-primary/90",
|
|
1472
|
+
title: "Exit edit mode",
|
|
1473
|
+
children: [
|
|
1474
|
+
/* @__PURE__ */ jsx(X, { className: "h-3 w-3" }),
|
|
1475
|
+
"Done"
|
|
1476
|
+
]
|
|
1477
|
+
}
|
|
1478
|
+
)
|
|
1479
|
+
] })
|
|
971
1480
|
] }),
|
|
972
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
973
|
-
|
|
974
|
-
|
|
1481
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-h-0 border-b-2 overflow-hidden flex", children: [
|
|
1482
|
+
!hideFileTree && showTree && /* @__PURE__ */ jsx(
|
|
1483
|
+
FileTree,
|
|
975
1484
|
{
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1485
|
+
files,
|
|
1486
|
+
activeFile: session.activeFile,
|
|
1487
|
+
onSelectFile: session.setActiveFile,
|
|
1488
|
+
onReplaceFile: session.replaceFile
|
|
980
1489
|
}
|
|
981
1490
|
),
|
|
982
|
-
/* @__PURE__ */ jsx(
|
|
983
|
-
"
|
|
1491
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto", children: fileType.category === "compilable" && showPreview ? /* @__PURE__ */ jsxs("div", { className: "bg-white h-full relative", ref: setPreviewContainer, children: [
|
|
1492
|
+
previewError && renderError ? renderError(previewError) : previewError ? /* @__PURE__ */ jsxs("div", { className: "p-4 text-sm text-destructive flex items-center gap-2", children: [
|
|
1493
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
|
|
1494
|
+
/* @__PURE__ */ jsx("span", { children: previewError })
|
|
1495
|
+
] }) : previewLoading && renderLoading ? renderLoading() : previewLoading ? /* @__PURE__ */ jsxs("div", { className: "p-4 flex items-center gap-2 text-muted-foreground", children: [
|
|
1496
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
1497
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
|
|
1498
|
+
] }) : /* @__PURE__ */ jsx("div", { className: "p-4", children: renderPreview(code) }, hashCode(code)),
|
|
1499
|
+
!renderLoading && !renderError && !previewLoading && /* @__PURE__ */ jsx(
|
|
1500
|
+
Bobbin,
|
|
1501
|
+
{
|
|
1502
|
+
container: previewContainer,
|
|
1503
|
+
pillContainer: previewContainer,
|
|
1504
|
+
defaultActive: false,
|
|
1505
|
+
showInspector: true,
|
|
1506
|
+
onChanges: handleBobbinChanges,
|
|
1507
|
+
exclude: [".bobbin-pill", "[data-bobbin]"]
|
|
1508
|
+
}
|
|
1509
|
+
)
|
|
1510
|
+
] }) : fileType.category === "compilable" && !showPreview ? /* @__PURE__ */ jsx(
|
|
1511
|
+
CodeBlockView,
|
|
984
1512
|
{
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1513
|
+
content: code,
|
|
1514
|
+
language: fileType.language,
|
|
1515
|
+
editable: true,
|
|
1516
|
+
onChange: session.updateActiveFile
|
|
989
1517
|
}
|
|
990
|
-
)
|
|
991
|
-
|
|
992
|
-
"button",
|
|
1518
|
+
) : fileType.category === "text" ? /* @__PURE__ */ jsx(
|
|
1519
|
+
CodeBlockView,
|
|
993
1520
|
{
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
showPreview ? "Preview" : "Code"
|
|
999
|
-
]
|
|
1521
|
+
content: code,
|
|
1522
|
+
language: fileType.language,
|
|
1523
|
+
editable: true,
|
|
1524
|
+
onChange: session.updateActiveFile
|
|
1000
1525
|
}
|
|
1001
|
-
)
|
|
1002
|
-
|
|
1003
|
-
"button",
|
|
1526
|
+
) : fileType.category === "media" ? /* @__PURE__ */ jsx(
|
|
1527
|
+
MediaPreview,
|
|
1004
1528
|
{
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
children: [
|
|
1009
|
-
/* @__PURE__ */ jsx(X, { className: "h-3 w-3" }),
|
|
1010
|
-
"Done"
|
|
1011
|
-
]
|
|
1529
|
+
content: code,
|
|
1530
|
+
mimeType: getMimeType(session.activeFile),
|
|
1531
|
+
fileName: session.activeFile.split("/").pop() ?? session.activeFile
|
|
1012
1532
|
}
|
|
1013
|
-
)
|
|
1014
|
-
] })
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
showTree && /* @__PURE__ */ jsx(
|
|
1018
|
-
FileTree,
|
|
1533
|
+
) : /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center h-full text-muted-foreground", children: /* @__PURE__ */ jsx("p", { className: "text-sm", children: "Preview not available for this file type" }) }) })
|
|
1534
|
+
] }),
|
|
1535
|
+
/* @__PURE__ */ jsx(
|
|
1536
|
+
EditHistory,
|
|
1019
1537
|
{
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1538
|
+
entries: session.history,
|
|
1539
|
+
streamingNotes: session.streamingNotes,
|
|
1540
|
+
isStreaming: session.isApplying,
|
|
1541
|
+
pendingPrompt: session.pendingPrompt,
|
|
1542
|
+
className: "h-48"
|
|
1023
1543
|
}
|
|
1024
1544
|
),
|
|
1025
|
-
/* @__PURE__ */
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1545
|
+
(session.error || saveError) && /* @__PURE__ */ jsxs("div", { className: "px-4 py-2 bg-destructive/10 text-destructive text-sm flex items-center gap-2 border-t-2 border-destructive", children: [
|
|
1546
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
|
|
1547
|
+
session.error || saveError
|
|
1548
|
+
] }),
|
|
1549
|
+
bobbinChanges.length > 0 && /* @__PURE__ */ jsxs("div", { className: "px-4 py-2 bg-blue-50 text-blue-700 text-sm flex items-center gap-2 border-t", children: [
|
|
1550
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1551
|
+
bobbinChanges.length,
|
|
1552
|
+
" visual change",
|
|
1553
|
+
bobbinChanges.length !== 1 ? "s" : ""
|
|
1554
|
+
] }),
|
|
1555
|
+
/* @__PURE__ */ jsx(
|
|
1556
|
+
"button",
|
|
1035
1557
|
{
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
showInspector: true,
|
|
1040
|
-
onChanges: handleBobbinChanges,
|
|
1041
|
-
exclude: [".bobbin-pill", "[data-bobbin]"]
|
|
1558
|
+
onClick: () => setBobbinChanges([]),
|
|
1559
|
+
className: "text-xs underline hover:no-underline",
|
|
1560
|
+
children: "Clear"
|
|
1042
1561
|
}
|
|
1043
1562
|
)
|
|
1044
|
-
] })
|
|
1045
|
-
|
|
1563
|
+
] }),
|
|
1564
|
+
/* @__PURE__ */ jsxs("div", { className: "p-4 border-t-2 bg-primary/5 flex gap-2 items-end", children: [
|
|
1565
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
|
|
1566
|
+
MarkdownEditor,
|
|
1567
|
+
{
|
|
1568
|
+
value: editInput,
|
|
1569
|
+
onChange: setEditInput,
|
|
1570
|
+
onSubmit: handleSubmit,
|
|
1571
|
+
placeholder: "Describe changes...",
|
|
1572
|
+
disabled: session.isApplying
|
|
1573
|
+
}
|
|
1574
|
+
) }),
|
|
1575
|
+
/* @__PURE__ */ jsx(
|
|
1576
|
+
"button",
|
|
1577
|
+
{
|
|
1578
|
+
onClick: handleSubmit,
|
|
1579
|
+
disabled: !editInput.trim() && bobbinChanges.length === 0 || session.isApplying,
|
|
1580
|
+
className: "px-3 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 flex items-center gap-1 shrink-0",
|
|
1581
|
+
children: session.isApplying ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
|
|
1582
|
+
}
|
|
1583
|
+
)
|
|
1584
|
+
] })
|
|
1585
|
+
] }) }),
|
|
1046
1586
|
/* @__PURE__ */ jsx(
|
|
1047
|
-
|
|
1587
|
+
SaveConfirmDialog,
|
|
1048
1588
|
{
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1589
|
+
isOpen: showConfirm,
|
|
1590
|
+
isSaving,
|
|
1591
|
+
error: saveError,
|
|
1592
|
+
onSave: handleSaveAndClose,
|
|
1593
|
+
onDiscard: handleDiscard,
|
|
1594
|
+
onCancel: handleCancelClose
|
|
1054
1595
|
}
|
|
1055
|
-
)
|
|
1056
|
-
|
|
1057
|
-
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
|
|
1058
|
-
session.error
|
|
1059
|
-
] }),
|
|
1060
|
-
bobbinChanges.length > 0 && /* @__PURE__ */ jsxs("div", { className: "px-4 py-2 bg-blue-50 text-blue-700 text-sm flex items-center gap-2 border-t", children: [
|
|
1061
|
-
/* @__PURE__ */ jsxs("span", { children: [
|
|
1062
|
-
bobbinChanges.length,
|
|
1063
|
-
" visual change",
|
|
1064
|
-
bobbinChanges.length !== 1 ? "s" : ""
|
|
1065
|
-
] }),
|
|
1066
|
-
/* @__PURE__ */ jsx(
|
|
1067
|
-
"button",
|
|
1068
|
-
{
|
|
1069
|
-
onClick: () => setBobbinChanges([]),
|
|
1070
|
-
className: "text-xs underline hover:no-underline",
|
|
1071
|
-
children: "Clear"
|
|
1072
|
-
}
|
|
1073
|
-
)
|
|
1074
|
-
] }),
|
|
1075
|
-
/* @__PURE__ */ jsxs("div", { className: "p-4 border-t-2 bg-primary/5 flex gap-2 items-end", children: [
|
|
1076
|
-
/* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
|
|
1077
|
-
MarkdownEditor,
|
|
1078
|
-
{
|
|
1079
|
-
value: editInput,
|
|
1080
|
-
onChange: setEditInput,
|
|
1081
|
-
onSubmit: handleSubmit,
|
|
1082
|
-
placeholder: "Describe changes...",
|
|
1083
|
-
disabled: session.isApplying
|
|
1084
|
-
}
|
|
1085
|
-
) }),
|
|
1086
|
-
/* @__PURE__ */ jsx(
|
|
1087
|
-
"button",
|
|
1088
|
-
{
|
|
1089
|
-
onClick: handleSubmit,
|
|
1090
|
-
disabled: !editInput.trim() && bobbinChanges.length === 0 || session.isApplying,
|
|
1091
|
-
className: "px-3 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 flex items-center gap-1 shrink-0",
|
|
1092
|
-
children: session.isApplying ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
|
|
1093
|
-
}
|
|
1094
|
-
)
|
|
1095
|
-
] })
|
|
1096
|
-
] }) });
|
|
1596
|
+
)
|
|
1597
|
+
] });
|
|
1097
1598
|
}
|
|
1098
1599
|
var VFS_BASE_URL = "/vfs";
|
|
1099
1600
|
var vfsConfigCache = null;
|
|
@@ -1112,8 +1613,11 @@ async function getVFSConfig() {
|
|
|
1112
1613
|
var storeInstance = null;
|
|
1113
1614
|
function getVFSStore() {
|
|
1114
1615
|
if (!storeInstance) {
|
|
1115
|
-
const
|
|
1116
|
-
storeInstance = new VFSStore(
|
|
1616
|
+
const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
|
|
1617
|
+
storeInstance = new VFSStore(provider, {
|
|
1618
|
+
sync: true,
|
|
1619
|
+
conflictStrategy: "local-wins"
|
|
1620
|
+
});
|
|
1117
1621
|
}
|
|
1118
1622
|
return storeInstance;
|
|
1119
1623
|
}
|
|
@@ -1135,9 +1639,17 @@ async function listProjects() {
|
|
|
1135
1639
|
}
|
|
1136
1640
|
return Array.from(projectIds);
|
|
1137
1641
|
}
|
|
1138
|
-
async function saveFile(
|
|
1642
|
+
async function saveFile(path, content) {
|
|
1643
|
+
const store = getVFSStore();
|
|
1644
|
+
await store.writeFile(path, content);
|
|
1645
|
+
}
|
|
1646
|
+
async function loadFile(path, encoding) {
|
|
1139
1647
|
const store = getVFSStore();
|
|
1140
|
-
|
|
1648
|
+
return store.readFile(path, encoding);
|
|
1649
|
+
}
|
|
1650
|
+
function subscribeToChanges(callback) {
|
|
1651
|
+
const store = getVFSStore();
|
|
1652
|
+
return store.on("change", callback);
|
|
1141
1653
|
}
|
|
1142
1654
|
async function isVFSAvailable() {
|
|
1143
1655
|
try {
|
|
@@ -1207,12 +1719,17 @@ function useCodeCompiler(compiler, code, enabled, services) {
|
|
|
1207
1719
|
}, [code, compiler, enabled, services]);
|
|
1208
1720
|
return { containerRef, loading, error };
|
|
1209
1721
|
}
|
|
1210
|
-
function CodePreview({ code: originalCode, compiler, services, filePath }) {
|
|
1722
|
+
function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint = "index.ts" }) {
|
|
1211
1723
|
const [isEditing, setIsEditing] = useState(false);
|
|
1212
1724
|
const [showPreview, setShowPreview] = useState(true);
|
|
1213
1725
|
const [currentCode, setCurrentCode] = useState(originalCode);
|
|
1214
1726
|
const [editCount, setEditCount] = useState(0);
|
|
1215
1727
|
const [saveStatus, setSaveStatus] = useState("unsaved");
|
|
1728
|
+
const [lastSavedCode, setLastSavedCode] = useState(originalCode);
|
|
1729
|
+
const [vfsPath, setVfsPath] = useState(null);
|
|
1730
|
+
const currentCodeRef = useRef(currentCode);
|
|
1731
|
+
const lastSavedRef = useRef(lastSavedCode);
|
|
1732
|
+
const isEditingRef = useRef(isEditing);
|
|
1216
1733
|
const fallbackId = useMemo(() => crypto.randomUUID(), []);
|
|
1217
1734
|
const getProjectId = useCallback(async () => {
|
|
1218
1735
|
if (filePath) {
|
|
@@ -1230,10 +1747,57 @@ function CodePreview({ code: originalCode, compiler, services, filePath }) {
|
|
|
1230
1747
|
const getEntryFile = useCallback(() => {
|
|
1231
1748
|
if (filePath) {
|
|
1232
1749
|
const parts = filePath.split("/");
|
|
1233
|
-
return parts[parts.length - 1] ||
|
|
1750
|
+
return parts[parts.length - 1] || entrypoint;
|
|
1234
1751
|
}
|
|
1235
|
-
return
|
|
1752
|
+
return entrypoint;
|
|
1236
1753
|
}, [filePath]);
|
|
1754
|
+
useEffect(() => {
|
|
1755
|
+
currentCodeRef.current = currentCode;
|
|
1756
|
+
}, [currentCode]);
|
|
1757
|
+
useEffect(() => {
|
|
1758
|
+
lastSavedRef.current = lastSavedCode;
|
|
1759
|
+
}, [lastSavedCode]);
|
|
1760
|
+
useEffect(() => {
|
|
1761
|
+
isEditingRef.current = isEditing;
|
|
1762
|
+
}, [isEditing]);
|
|
1763
|
+
useEffect(() => {
|
|
1764
|
+
let active = true;
|
|
1765
|
+
void (async () => {
|
|
1766
|
+
const projectId = await getProjectId();
|
|
1767
|
+
const entryFile = getEntryFile();
|
|
1768
|
+
if (!active) return;
|
|
1769
|
+
setVfsPath(`${projectId}/${entryFile}`);
|
|
1770
|
+
})();
|
|
1771
|
+
return () => {
|
|
1772
|
+
active = false;
|
|
1773
|
+
};
|
|
1774
|
+
}, [getProjectId, getEntryFile]);
|
|
1775
|
+
useEffect(() => {
|
|
1776
|
+
if (!vfsPath) return;
|
|
1777
|
+
const unsubscribe = subscribeToChanges(async (record) => {
|
|
1778
|
+
if (record.path !== vfsPath) return;
|
|
1779
|
+
if (record.type === "delete") {
|
|
1780
|
+
setSaveStatus("unsaved");
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
if (isEditingRef.current) return;
|
|
1784
|
+
try {
|
|
1785
|
+
const remote = await loadFile(vfsPath);
|
|
1786
|
+
if (currentCodeRef.current !== lastSavedRef.current) {
|
|
1787
|
+
setSaveStatus("unsaved");
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (remote !== currentCodeRef.current) {
|
|
1791
|
+
setCurrentCode(remote);
|
|
1792
|
+
setLastSavedCode(remote);
|
|
1793
|
+
setSaveStatus("saved");
|
|
1794
|
+
}
|
|
1795
|
+
} catch {
|
|
1796
|
+
setSaveStatus("error");
|
|
1797
|
+
}
|
|
1798
|
+
});
|
|
1799
|
+
return () => unsubscribe();
|
|
1800
|
+
}, [vfsPath]);
|
|
1237
1801
|
const handleSave = useCallback(async () => {
|
|
1238
1802
|
setSaveStatus("saving");
|
|
1239
1803
|
try {
|
|
@@ -1241,6 +1805,7 @@ function CodePreview({ code: originalCode, compiler, services, filePath }) {
|
|
|
1241
1805
|
const entryFile = getEntryFile();
|
|
1242
1806
|
const project = createSingleFileProject(currentCode, entryFile, projectId);
|
|
1243
1807
|
await saveProject(project);
|
|
1808
|
+
setLastSavedCode(currentCode);
|
|
1244
1809
|
setSaveStatus("saved");
|
|
1245
1810
|
} catch (err) {
|
|
1246
1811
|
console.warn("[VFS] Failed to save project:", err);
|
|
@@ -1594,4 +2159,4 @@ function cn(...inputs) {
|
|
|
1594
2159
|
return twMerge(clsx(inputs));
|
|
1595
2160
|
}
|
|
1596
2161
|
|
|
1597
|
-
export { CodeBlockExtension, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, ServicesInspector, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFiles, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isVFSAvailable, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };
|
|
2162
|
+
export { CodeBlockExtension, CodeBlockView, CodePreview, EditHistory, EditModal, FileTree, MarkdownEditor, MediaPreview, SaveConfirmDialog, ServicesInspector, applyDiffs, cn, extractCodeBlocks, extractProject, extractSummary, extractTextWithoutDiffs, findDiffMarkers, findFirstCodeBlock, getActiveContent, getCodeBlockLanguages, getFileType, getFiles, getLanguageFromExt, getMimeType, getVFSConfig, getVFSStore, hasCodeBlock, hasDiffBlocks, isCompilable, isImageFile, isMediaFile, isTextFile, isVFSAvailable, isVideoFile, listProjects, loadProject, parseCodeBlockAttributes, parseCodeBlocks, parseDiffs, parseEditResponse, sanitizeDiffMarkers, saveFile, saveProject, sendEditRequest, useEditSession };
|