@aprovan/patchwork-editor 0.1.2-dev.03aaf5b → 0.1.2-dev.ba8f277
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 +2 -2
- package/dist/components/CodePreview.d.ts +10 -1
- package/dist/components/MarkdownPreview.d.ts +8 -0
- package/dist/components/SaveStatusButton.d.ts +9 -0
- package/dist/components/ServicesInspector.d.ts +3 -4
- package/dist/components/WidgetPreview.d.ts +8 -0
- package/dist/components/edit/EditModal.d.ts +2 -1
- package/dist/components/edit/FileTree.d.ts +22 -3
- package/dist/components/edit/fileTypes.d.ts +2 -0
- package/dist/components/edit/useEditSession.d.ts +1 -0
- package/dist/components/index.d.ts +7 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.js +904 -176
- package/package.json +3 -3
- package/src/components/CodePreview.tsx +118 -160
- package/src/components/MarkdownPreview.tsx +147 -0
- package/src/components/SaveStatusButton.tsx +55 -0
- package/src/components/ServicesInspector.tsx +101 -37
- package/src/components/WidgetPreview.tsx +102 -0
- package/src/components/edit/EditModal.tsx +83 -26
- package/src/components/edit/FileTree.tsx +523 -28
- package/src/components/edit/api.ts +6 -1
- package/src/components/edit/fileTypes.ts +8 -0
- package/src/components/edit/useEditSession.ts +13 -3
- package/src/components/index.ts +7 -5
- package/src/index.ts +10 -6
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ 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 { AlertCircle, FileImage, FileVideo, Pencil,
|
|
5
|
+
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
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';
|
|
@@ -371,7 +371,11 @@ async function sendEditRequest(request, options = {}) {
|
|
|
371
371
|
}
|
|
372
372
|
const text = await streamResponse(response, onProgress);
|
|
373
373
|
if (!hasDiffBlocks(text)) {
|
|
374
|
-
|
|
374
|
+
return {
|
|
375
|
+
newCode: request.code,
|
|
376
|
+
summary: text.trim(),
|
|
377
|
+
progressNotes: []
|
|
378
|
+
};
|
|
375
379
|
}
|
|
376
380
|
const parsed = parseEditResponse(text);
|
|
377
381
|
const result = applyDiffs(request.code, parsed.diffs, { sanitize });
|
|
@@ -440,6 +444,7 @@ function useEditSession(options) {
|
|
|
440
444
|
const {
|
|
441
445
|
originalCode,
|
|
442
446
|
originalProject: providedProject,
|
|
447
|
+
initialActiveFile,
|
|
443
448
|
compile,
|
|
444
449
|
apiEndpoint
|
|
445
450
|
} = options;
|
|
@@ -455,7 +460,9 @@ function useEditSession(options) {
|
|
|
455
460
|
);
|
|
456
461
|
const lastSyncedProjectRef = useRef(originalProject);
|
|
457
462
|
const [project, setProject] = useState(originalProject);
|
|
458
|
-
const [activeFile, setActiveFile] = useState(
|
|
463
|
+
const [activeFile, setActiveFile] = useState(
|
|
464
|
+
initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
|
|
465
|
+
);
|
|
459
466
|
const [history, setHistory] = useState([]);
|
|
460
467
|
const [isApplying, setIsApplying] = useState(false);
|
|
461
468
|
const [error, setError] = useState(null);
|
|
@@ -465,12 +472,14 @@ function useEditSession(options) {
|
|
|
465
472
|
if (originalProject !== lastSyncedProjectRef.current) {
|
|
466
473
|
lastSyncedProjectRef.current = originalProject;
|
|
467
474
|
setProject(originalProject);
|
|
468
|
-
setActiveFile(
|
|
475
|
+
setActiveFile(
|
|
476
|
+
initialActiveFile && originalProject.files.has(initialActiveFile) ? initialActiveFile : originalProject.entry
|
|
477
|
+
);
|
|
469
478
|
setHistory([]);
|
|
470
479
|
setError(null);
|
|
471
480
|
setStreamingNotes([]);
|
|
472
481
|
}
|
|
473
|
-
}, [originalProject]);
|
|
482
|
+
}, [originalProject, initialActiveFile]);
|
|
474
483
|
const performEdit = useCallback(
|
|
475
484
|
async (currentCode2, prompt, isRetry = false) => {
|
|
476
485
|
const entries = [];
|
|
@@ -851,6 +860,156 @@ function MarkdownEditor({
|
|
|
851
860
|
}
|
|
852
861
|
);
|
|
853
862
|
}
|
|
863
|
+
function parseFrontmatter(content) {
|
|
864
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
865
|
+
if (!match) return { frontmatter: "", body: content };
|
|
866
|
+
return { frontmatter: match[1], body: match[2] };
|
|
867
|
+
}
|
|
868
|
+
function assembleFrontmatter(frontmatter, body) {
|
|
869
|
+
if (!frontmatter.trim()) return body;
|
|
870
|
+
return `---
|
|
871
|
+
${frontmatter}
|
|
872
|
+
---
|
|
873
|
+
${body}`;
|
|
874
|
+
}
|
|
875
|
+
function MarkdownPreview({
|
|
876
|
+
value,
|
|
877
|
+
onChange,
|
|
878
|
+
editable = false,
|
|
879
|
+
className = ""
|
|
880
|
+
}) {
|
|
881
|
+
const { frontmatter, body } = parseFrontmatter(value);
|
|
882
|
+
const [fm, setFm] = useState(frontmatter);
|
|
883
|
+
const fmRef = useRef(frontmatter);
|
|
884
|
+
const bodyRef = useRef(body);
|
|
885
|
+
const textareaRef = useRef(null);
|
|
886
|
+
useEffect(() => {
|
|
887
|
+
const parsed = parseFrontmatter(value);
|
|
888
|
+
fmRef.current = parsed.frontmatter;
|
|
889
|
+
bodyRef.current = parsed.body;
|
|
890
|
+
setFm(parsed.frontmatter);
|
|
891
|
+
}, [value]);
|
|
892
|
+
const emitChange = useCallback(
|
|
893
|
+
(newFm, newBody) => {
|
|
894
|
+
onChange?.(assembleFrontmatter(newFm, newBody));
|
|
895
|
+
},
|
|
896
|
+
[onChange]
|
|
897
|
+
);
|
|
898
|
+
const handleFmChange = useCallback(
|
|
899
|
+
(e) => {
|
|
900
|
+
const newFm = e.target.value;
|
|
901
|
+
setFm(newFm);
|
|
902
|
+
fmRef.current = newFm;
|
|
903
|
+
emitChange(newFm, bodyRef.current);
|
|
904
|
+
},
|
|
905
|
+
[emitChange]
|
|
906
|
+
);
|
|
907
|
+
useEffect(() => {
|
|
908
|
+
if (textareaRef.current) {
|
|
909
|
+
textareaRef.current.style.height = "auto";
|
|
910
|
+
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
|
911
|
+
}
|
|
912
|
+
}, [fm]);
|
|
913
|
+
const editor = useEditor({
|
|
914
|
+
extensions: [
|
|
915
|
+
StarterKit.configure({
|
|
916
|
+
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
|
917
|
+
bulletList: { keepMarks: true, keepAttributes: false },
|
|
918
|
+
orderedList: { keepMarks: true, keepAttributes: false },
|
|
919
|
+
codeBlock: false,
|
|
920
|
+
code: {
|
|
921
|
+
HTMLAttributes: {
|
|
922
|
+
class: "bg-muted rounded px-1 py-0.5 font-mono text-sm"
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
blockquote: {
|
|
926
|
+
HTMLAttributes: {
|
|
927
|
+
class: "border-l-4 border-muted-foreground/30 pl-4 italic"
|
|
928
|
+
}
|
|
929
|
+
},
|
|
930
|
+
hardBreak: { keepMarks: false }
|
|
931
|
+
}),
|
|
932
|
+
CodeBlockExtension,
|
|
933
|
+
Typography,
|
|
934
|
+
Markdown$1.configure({
|
|
935
|
+
html: false,
|
|
936
|
+
transformPastedText: true,
|
|
937
|
+
transformCopiedText: true
|
|
938
|
+
})
|
|
939
|
+
],
|
|
940
|
+
content: body,
|
|
941
|
+
editable,
|
|
942
|
+
editorProps: {
|
|
943
|
+
attributes: {
|
|
944
|
+
class: `outline-none ${className}`
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
onUpdate: ({ editor: editor2 }) => {
|
|
948
|
+
const markdownStorage = editor2.storage.markdown;
|
|
949
|
+
const newBody = markdownStorage?.getMarkdown?.() ?? editor2.getText();
|
|
950
|
+
bodyRef.current = newBody;
|
|
951
|
+
emitChange(fmRef.current, newBody);
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
useEffect(() => {
|
|
955
|
+
editor?.setEditable(editable);
|
|
956
|
+
}, [editor, editable]);
|
|
957
|
+
useEffect(() => {
|
|
958
|
+
if (!editor) return;
|
|
959
|
+
const parsed = parseFrontmatter(value);
|
|
960
|
+
const markdownStorage = editor.storage.markdown;
|
|
961
|
+
const current = markdownStorage?.getMarkdown?.() ?? editor.getText();
|
|
962
|
+
if (parsed.body !== current) {
|
|
963
|
+
editor.commands.setContent(parsed.body);
|
|
964
|
+
}
|
|
965
|
+
}, [editor, value]);
|
|
966
|
+
return /* @__PURE__ */ jsxs("div", { className: "markdown-preview", children: [
|
|
967
|
+
frontmatter && /* @__PURE__ */ jsxs("div", { className: "mb-4 rounded-md border border-border bg-muted/40 overflow-hidden", children: [
|
|
968
|
+
/* @__PURE__ */ jsx("div", { className: "px-3 py-1.5 text-xs font-mono text-muted-foreground border-b border-border bg-muted/60 select-none", children: "yml" }),
|
|
969
|
+
/* @__PURE__ */ jsx(
|
|
970
|
+
"textarea",
|
|
971
|
+
{
|
|
972
|
+
ref: textareaRef,
|
|
973
|
+
value: fm,
|
|
974
|
+
onChange: handleFmChange,
|
|
975
|
+
readOnly: !editable,
|
|
976
|
+
className: "w-full bg-transparent px-3 py-2 font-mono text-sm outline-none resize-none",
|
|
977
|
+
spellCheck: false
|
|
978
|
+
}
|
|
979
|
+
)
|
|
980
|
+
] }),
|
|
981
|
+
/* @__PURE__ */ jsx(EditorContent, { editor, className: "markdown-editor" })
|
|
982
|
+
] });
|
|
983
|
+
}
|
|
984
|
+
function getToneClass(tone, status) {
|
|
985
|
+
if (status === "error") {
|
|
986
|
+
return tone === "muted" ? "text-destructive hover:bg-muted" : "text-destructive hover:bg-destructive/10";
|
|
987
|
+
}
|
|
988
|
+
if (status === "saved") {
|
|
989
|
+
return tone === "muted" ? "text-muted-foreground/50 hover:bg-muted" : "text-primary/50 hover:bg-primary/10";
|
|
990
|
+
}
|
|
991
|
+
return tone === "muted" ? "text-muted-foreground hover:bg-muted" : "text-primary hover:bg-primary/20";
|
|
992
|
+
}
|
|
993
|
+
function SaveStatusButton({
|
|
994
|
+
status,
|
|
995
|
+
onClick,
|
|
996
|
+
disabled = false,
|
|
997
|
+
tone
|
|
998
|
+
}) {
|
|
999
|
+
return /* @__PURE__ */ jsxs(
|
|
1000
|
+
"button",
|
|
1001
|
+
{
|
|
1002
|
+
onClick,
|
|
1003
|
+
disabled,
|
|
1004
|
+
className: `px-2 py-1 text-xs rounded flex items-center gap-1 disabled:opacity-50 ${getToneClass(tone, status)}`,
|
|
1005
|
+
title: "Save",
|
|
1006
|
+
children: [
|
|
1007
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex h-3 w-3 items-center justify-center shrink-0", children: status === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : status === "error" ? /* @__PURE__ */ jsx(AlertTriangle, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Save, { className: `h-3 w-3 ${status === "saved" ? "opacity-60" : "opacity-100"}` }) }),
|
|
1008
|
+
"Save"
|
|
1009
|
+
]
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
854
1013
|
|
|
855
1014
|
// src/components/edit/fileTypes.ts
|
|
856
1015
|
var COMPILABLE_EXTENSIONS = [".tsx", ".jsx", ".ts", ".js"];
|
|
@@ -939,6 +1098,12 @@ function isMediaFile(path) {
|
|
|
939
1098
|
function isTextFile(path) {
|
|
940
1099
|
return TEXT_EXTENSIONS.includes(getExtension(path));
|
|
941
1100
|
}
|
|
1101
|
+
function isMarkdownFile(path) {
|
|
1102
|
+
return getExtension(path) === ".md";
|
|
1103
|
+
}
|
|
1104
|
+
function isPreviewable(path) {
|
|
1105
|
+
return isCompilable(path) || isMarkdownFile(path);
|
|
1106
|
+
}
|
|
942
1107
|
function getLanguageFromExt(path) {
|
|
943
1108
|
const ext = getExtension(path);
|
|
944
1109
|
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
@@ -955,6 +1120,17 @@ function isVideoFile(path) {
|
|
|
955
1120
|
const ext = getExtension(path);
|
|
956
1121
|
return [".mp4", ".mov", ".webm"].includes(ext);
|
|
957
1122
|
}
|
|
1123
|
+
function sortNodes(nodes) {
|
|
1124
|
+
nodes.sort((a, b) => {
|
|
1125
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
1126
|
+
return a.name.localeCompare(b.name);
|
|
1127
|
+
});
|
|
1128
|
+
for (const node of nodes) {
|
|
1129
|
+
if (node.children.length > 0) {
|
|
1130
|
+
sortNodes(node.children);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
958
1134
|
function buildTree(files) {
|
|
959
1135
|
const root = { name: "", path: "", isDir: true, children: [] };
|
|
960
1136
|
for (const file of files) {
|
|
@@ -977,14 +1153,26 @@ function buildTree(files) {
|
|
|
977
1153
|
current = child;
|
|
978
1154
|
}
|
|
979
1155
|
}
|
|
980
|
-
root.children
|
|
981
|
-
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
982
|
-
return a.name.localeCompare(b.name);
|
|
983
|
-
});
|
|
1156
|
+
sortNodes(root.children);
|
|
984
1157
|
return root;
|
|
985
1158
|
}
|
|
986
|
-
function TreeNodeComponent({
|
|
987
|
-
|
|
1159
|
+
function TreeNodeComponent({
|
|
1160
|
+
node,
|
|
1161
|
+
activePath,
|
|
1162
|
+
onSelect,
|
|
1163
|
+
onSelectDirectory,
|
|
1164
|
+
onReplaceFile,
|
|
1165
|
+
onOpenInEditor,
|
|
1166
|
+
openInEditorMode = "files",
|
|
1167
|
+
openInEditorIcon,
|
|
1168
|
+
openInEditorTitle = "Open in editor",
|
|
1169
|
+
pinnedPaths,
|
|
1170
|
+
onTogglePin,
|
|
1171
|
+
pageSize = 10,
|
|
1172
|
+
depth = 0
|
|
1173
|
+
}) {
|
|
1174
|
+
const [expanded, setExpanded] = useState(false);
|
|
1175
|
+
const [visibleCount, setVisibleCount] = useState(pageSize);
|
|
988
1176
|
const [isHovered, setIsHovered] = useState(false);
|
|
989
1177
|
const fileInputRef = useRef(null);
|
|
990
1178
|
const handleUploadClick = useCallback((e) => {
|
|
@@ -1003,48 +1191,119 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1003
1191
|
reader.readAsDataURL(file);
|
|
1004
1192
|
e.target.value = "";
|
|
1005
1193
|
}, [node.path, onReplaceFile]);
|
|
1194
|
+
const isPinned = pinnedPaths?.has(node.path) ?? false;
|
|
1195
|
+
const showPin = onTogglePin && isHovered;
|
|
1196
|
+
const handleTogglePin = useCallback((e) => {
|
|
1197
|
+
e.stopPropagation();
|
|
1198
|
+
onTogglePin?.(node.path, node.isDir);
|
|
1199
|
+
}, [node.path, node.isDir, onTogglePin]);
|
|
1006
1200
|
if (!node.name) {
|
|
1007
1201
|
return /* @__PURE__ */ jsx(Fragment, { children: node.children.map((child) => /* @__PURE__ */ jsx(
|
|
1008
1202
|
TreeNodeComponent,
|
|
1009
1203
|
{
|
|
1010
1204
|
node: child,
|
|
1011
|
-
|
|
1205
|
+
activePath,
|
|
1012
1206
|
onSelect,
|
|
1207
|
+
onSelectDirectory,
|
|
1013
1208
|
onReplaceFile,
|
|
1209
|
+
onOpenInEditor,
|
|
1210
|
+
openInEditorMode,
|
|
1211
|
+
openInEditorIcon,
|
|
1212
|
+
openInEditorTitle,
|
|
1213
|
+
pinnedPaths,
|
|
1214
|
+
onTogglePin,
|
|
1215
|
+
pageSize,
|
|
1014
1216
|
depth
|
|
1015
1217
|
},
|
|
1016
1218
|
child.path
|
|
1017
1219
|
)) });
|
|
1018
1220
|
}
|
|
1019
|
-
const isActive = node.path ===
|
|
1221
|
+
const isActive = node.path === activePath;
|
|
1020
1222
|
const isMedia = !node.isDir && isMediaFile(node.path);
|
|
1021
1223
|
const showUpload = isMedia && isHovered && onReplaceFile;
|
|
1224
|
+
const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? node.isDir : !node.isDir));
|
|
1225
|
+
const handleOpenInEditor = useCallback(
|
|
1226
|
+
(e) => {
|
|
1227
|
+
e.stopPropagation();
|
|
1228
|
+
onOpenInEditor?.(node.path, node.isDir);
|
|
1229
|
+
},
|
|
1230
|
+
[node.path, node.isDir, onOpenInEditor]
|
|
1231
|
+
);
|
|
1022
1232
|
if (node.isDir) {
|
|
1023
1233
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1024
1234
|
/* @__PURE__ */ jsxs(
|
|
1025
1235
|
"button",
|
|
1026
1236
|
{
|
|
1027
|
-
onClick: () =>
|
|
1028
|
-
|
|
1237
|
+
onClick: () => {
|
|
1238
|
+
onSelectDirectory?.(node.path);
|
|
1239
|
+
setExpanded(!expanded);
|
|
1240
|
+
},
|
|
1241
|
+
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" : ""}`,
|
|
1242
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1243
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1029
1244
|
style: { paddingLeft: `${depth * 12 + 8}px` },
|
|
1030
1245
|
children: [
|
|
1031
1246
|
expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
|
|
1032
1247
|
/* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1033
|
-
/* @__PURE__ */ jsx("span", { className: "truncate", children: node.name })
|
|
1248
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
|
|
1249
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1250
|
+
"span",
|
|
1251
|
+
{
|
|
1252
|
+
onClick: handleTogglePin,
|
|
1253
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1254
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1255
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1256
|
+
}
|
|
1257
|
+
),
|
|
1258
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1259
|
+
"span",
|
|
1260
|
+
{
|
|
1261
|
+
onClick: handleOpenInEditor,
|
|
1262
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1263
|
+
title: openInEditorTitle,
|
|
1264
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1265
|
+
}
|
|
1266
|
+
)
|
|
1034
1267
|
]
|
|
1035
1268
|
}
|
|
1036
1269
|
),
|
|
1037
|
-
expanded && /* @__PURE__ */
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1270
|
+
expanded && /* @__PURE__ */ jsxs("div", { children: [
|
|
1271
|
+
node.children.slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
|
|
1272
|
+
TreeNodeComponent,
|
|
1273
|
+
{
|
|
1274
|
+
node: child,
|
|
1275
|
+
activePath,
|
|
1276
|
+
onSelect,
|
|
1277
|
+
onSelectDirectory,
|
|
1278
|
+
onReplaceFile,
|
|
1279
|
+
onOpenInEditor,
|
|
1280
|
+
openInEditorMode,
|
|
1281
|
+
openInEditorIcon,
|
|
1282
|
+
openInEditorTitle,
|
|
1283
|
+
pinnedPaths,
|
|
1284
|
+
onTogglePin,
|
|
1285
|
+
pageSize,
|
|
1286
|
+
depth: depth + 1
|
|
1287
|
+
},
|
|
1288
|
+
child.path
|
|
1289
|
+
)),
|
|
1290
|
+
node.children.length > visibleCount && /* @__PURE__ */ jsxs(
|
|
1291
|
+
"button",
|
|
1292
|
+
{
|
|
1293
|
+
onClick: () => setVisibleCount((prev) => prev + pageSize),
|
|
1294
|
+
className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
|
|
1295
|
+
style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
|
|
1296
|
+
children: [
|
|
1297
|
+
/* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
|
|
1298
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1299
|
+
"Show ",
|
|
1300
|
+
Math.min(pageSize, node.children.length - visibleCount),
|
|
1301
|
+
" more"
|
|
1302
|
+
] })
|
|
1303
|
+
]
|
|
1304
|
+
}
|
|
1305
|
+
)
|
|
1306
|
+
] })
|
|
1048
1307
|
] });
|
|
1049
1308
|
}
|
|
1050
1309
|
return /* @__PURE__ */ jsxs(
|
|
@@ -1062,7 +1321,25 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1062
1321
|
style: { paddingLeft: `${depth * 12 + 20}px` },
|
|
1063
1322
|
children: [
|
|
1064
1323
|
/* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1065
|
-
/* @__PURE__ */ jsx("span", { className: "truncate flex-1", children: node.name }),
|
|
1324
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: node.name }),
|
|
1325
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1326
|
+
"span",
|
|
1327
|
+
{
|
|
1328
|
+
onClick: handleTogglePin,
|
|
1329
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1330
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1331
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1332
|
+
}
|
|
1333
|
+
),
|
|
1334
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1335
|
+
"span",
|
|
1336
|
+
{
|
|
1337
|
+
onClick: handleOpenInEditor,
|
|
1338
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1339
|
+
title: openInEditorTitle,
|
|
1340
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1341
|
+
}
|
|
1342
|
+
),
|
|
1066
1343
|
showUpload && /* @__PURE__ */ jsx(
|
|
1067
1344
|
"span",
|
|
1068
1345
|
{
|
|
@@ -1089,17 +1366,309 @@ function TreeNodeComponent({ node, activeFile, onSelect, onReplaceFile, depth =
|
|
|
1089
1366
|
}
|
|
1090
1367
|
);
|
|
1091
1368
|
}
|
|
1092
|
-
function
|
|
1369
|
+
function LazyTreeNode({
|
|
1370
|
+
entry,
|
|
1371
|
+
activePath,
|
|
1372
|
+
onSelectFile,
|
|
1373
|
+
onSelectDirectory,
|
|
1374
|
+
onOpenInEditor,
|
|
1375
|
+
openInEditorMode = "files",
|
|
1376
|
+
openInEditorIcon,
|
|
1377
|
+
openInEditorTitle = "Open in editor",
|
|
1378
|
+
pinnedPaths,
|
|
1379
|
+
onTogglePin,
|
|
1380
|
+
directoryLoader,
|
|
1381
|
+
pageSize,
|
|
1382
|
+
depth = 0,
|
|
1383
|
+
reloadToken
|
|
1384
|
+
}) {
|
|
1385
|
+
const [expanded, setExpanded] = useState(false);
|
|
1386
|
+
const [loading, setLoading] = useState(false);
|
|
1387
|
+
const [loadError, setLoadError] = useState(null);
|
|
1388
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
1389
|
+
const [children, setChildren] = useState(null);
|
|
1390
|
+
const [visibleCount, setVisibleCount] = useState(pageSize);
|
|
1391
|
+
useEffect(() => {
|
|
1392
|
+
setChildren(null);
|
|
1393
|
+
setVisibleCount(pageSize);
|
|
1394
|
+
if (expanded) {
|
|
1395
|
+
setLoading(true);
|
|
1396
|
+
setLoadError(null);
|
|
1397
|
+
directoryLoader(entry.path).then((loaded) => setChildren(loaded)).catch((err) => setLoadError(err instanceof Error ? err.message : "Failed to load directory")).finally(() => setLoading(false));
|
|
1398
|
+
}
|
|
1399
|
+
}, [reloadToken]);
|
|
1400
|
+
const isActive = entry.path === activePath;
|
|
1401
|
+
const isPinned = pinnedPaths?.has(entry.path) ?? false;
|
|
1402
|
+
const showPin = onTogglePin && isHovered;
|
|
1403
|
+
const showOpenInEditor = !!onOpenInEditor && isHovered && (openInEditorMode === "all" || (openInEditorMode === "directories" ? entry.isDir : !entry.isDir));
|
|
1404
|
+
const handleOpenInEditor = useCallback(
|
|
1405
|
+
(e) => {
|
|
1406
|
+
e.stopPropagation();
|
|
1407
|
+
onOpenInEditor?.(entry.path, entry.isDir);
|
|
1408
|
+
},
|
|
1409
|
+
[entry.path, entry.isDir, onOpenInEditor]
|
|
1410
|
+
);
|
|
1411
|
+
const handleTogglePin = useCallback(
|
|
1412
|
+
(e) => {
|
|
1413
|
+
e.stopPropagation();
|
|
1414
|
+
onTogglePin?.(entry.path, entry.isDir);
|
|
1415
|
+
},
|
|
1416
|
+
[entry.path, entry.isDir, onTogglePin]
|
|
1417
|
+
);
|
|
1418
|
+
const toggleDirectory = useCallback(async () => {
|
|
1419
|
+
if (!entry.isDir) return;
|
|
1420
|
+
onSelectDirectory?.(entry.path);
|
|
1421
|
+
if (!expanded && children === null) {
|
|
1422
|
+
setLoading(true);
|
|
1423
|
+
setLoadError(null);
|
|
1424
|
+
try {
|
|
1425
|
+
const loaded = await directoryLoader(entry.path);
|
|
1426
|
+
setChildren(loaded);
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
setLoadError(err instanceof Error ? err.message : "Failed to load directory");
|
|
1429
|
+
} finally {
|
|
1430
|
+
setLoading(false);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
setExpanded((prev) => !prev);
|
|
1434
|
+
}, [entry.isDir, entry.path, onSelectDirectory, expanded, children, directoryLoader]);
|
|
1435
|
+
if (!entry.isDir) {
|
|
1436
|
+
return /* @__PURE__ */ jsx(
|
|
1437
|
+
"div",
|
|
1438
|
+
{
|
|
1439
|
+
className: "relative",
|
|
1440
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1441
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1442
|
+
children: /* @__PURE__ */ jsxs(
|
|
1443
|
+
"button",
|
|
1444
|
+
{
|
|
1445
|
+
onClick: () => onSelectFile(entry.path),
|
|
1446
|
+
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" : ""}`,
|
|
1447
|
+
style: { paddingLeft: `${depth * 12 + 20}px` },
|
|
1448
|
+
children: [
|
|
1449
|
+
/* @__PURE__ */ jsx(File, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1450
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
|
|
1451
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1452
|
+
"span",
|
|
1453
|
+
{
|
|
1454
|
+
onClick: handleTogglePin,
|
|
1455
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1456
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1457
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1458
|
+
}
|
|
1459
|
+
),
|
|
1460
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1461
|
+
"span",
|
|
1462
|
+
{
|
|
1463
|
+
onClick: handleOpenInEditor,
|
|
1464
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1465
|
+
title: openInEditorTitle,
|
|
1466
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1467
|
+
}
|
|
1468
|
+
)
|
|
1469
|
+
]
|
|
1470
|
+
}
|
|
1471
|
+
)
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1476
|
+
/* @__PURE__ */ jsxs(
|
|
1477
|
+
"button",
|
|
1478
|
+
{
|
|
1479
|
+
onClick: () => void toggleDirectory(),
|
|
1480
|
+
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" : ""}`,
|
|
1481
|
+
onMouseEnter: () => setIsHovered(true),
|
|
1482
|
+
onMouseLeave: () => setIsHovered(false),
|
|
1483
|
+
style: { paddingLeft: `${depth * 12 + 8}px` },
|
|
1484
|
+
children: [
|
|
1485
|
+
expanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3 shrink-0" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-3 w-3 shrink-0" }),
|
|
1486
|
+
/* @__PURE__ */ jsx(Folder, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
|
|
1487
|
+
/* @__PURE__ */ jsx("span", { className: "truncate flex-1 flex pl-2", children: entry.name }),
|
|
1488
|
+
loading && /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin text-muted-foreground" }),
|
|
1489
|
+
(showPin || isPinned) && /* @__PURE__ */ jsx(
|
|
1490
|
+
"span",
|
|
1491
|
+
{
|
|
1492
|
+
onClick: handleTogglePin,
|
|
1493
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1494
|
+
title: isPinned ? "Unpin" : "Pin",
|
|
1495
|
+
children: isPinned ? /* @__PURE__ */ jsx(PinOff, { className: "h-3 w-3 text-primary" }) : /* @__PURE__ */ jsx(Pin, { className: "h-3 w-3 text-muted-foreground" })
|
|
1496
|
+
}
|
|
1497
|
+
),
|
|
1498
|
+
showOpenInEditor && /* @__PURE__ */ jsx(
|
|
1499
|
+
"span",
|
|
1500
|
+
{
|
|
1501
|
+
onClick: handleOpenInEditor,
|
|
1502
|
+
className: "p-1 hover:bg-primary/20 rounded cursor-pointer",
|
|
1503
|
+
title: openInEditorTitle,
|
|
1504
|
+
children: openInEditorIcon ?? /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3 text-primary" })
|
|
1505
|
+
}
|
|
1506
|
+
)
|
|
1507
|
+
]
|
|
1508
|
+
}
|
|
1509
|
+
),
|
|
1510
|
+
expanded && /* @__PURE__ */ jsxs("div", { children: [
|
|
1511
|
+
loadError && /* @__PURE__ */ jsx(
|
|
1512
|
+
"div",
|
|
1513
|
+
{
|
|
1514
|
+
className: "px-2 py-1 text-xs text-destructive",
|
|
1515
|
+
style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
|
|
1516
|
+
children: loadError
|
|
1517
|
+
}
|
|
1518
|
+
),
|
|
1519
|
+
(children ?? []).slice(0, visibleCount).map((child) => /* @__PURE__ */ jsx(
|
|
1520
|
+
LazyTreeNode,
|
|
1521
|
+
{
|
|
1522
|
+
entry: child,
|
|
1523
|
+
activePath,
|
|
1524
|
+
onSelectFile,
|
|
1525
|
+
onSelectDirectory,
|
|
1526
|
+
onOpenInEditor,
|
|
1527
|
+
openInEditorMode,
|
|
1528
|
+
openInEditorIcon,
|
|
1529
|
+
openInEditorTitle,
|
|
1530
|
+
pinnedPaths,
|
|
1531
|
+
onTogglePin,
|
|
1532
|
+
directoryLoader,
|
|
1533
|
+
pageSize,
|
|
1534
|
+
depth: depth + 1,
|
|
1535
|
+
reloadToken
|
|
1536
|
+
},
|
|
1537
|
+
child.path
|
|
1538
|
+
)),
|
|
1539
|
+
(children?.length ?? 0) > visibleCount && /* @__PURE__ */ jsxs(
|
|
1540
|
+
"button",
|
|
1541
|
+
{
|
|
1542
|
+
onClick: () => setVisibleCount((prev) => prev + pageSize),
|
|
1543
|
+
className: "flex items-center gap-1 w-full px-2 py-1 text-left text-xs hover:bg-muted/50 rounded text-muted-foreground",
|
|
1544
|
+
style: { paddingLeft: `${(depth + 1) * 12 + 20}px` },
|
|
1545
|
+
children: [
|
|
1546
|
+
/* @__PURE__ */ jsx(ChevronsDown, { className: "h-3 w-3 shrink-0" }),
|
|
1547
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1548
|
+
"Show ",
|
|
1549
|
+
Math.min(pageSize, (children?.length ?? 0) - visibleCount),
|
|
1550
|
+
" more"
|
|
1551
|
+
] })
|
|
1552
|
+
]
|
|
1553
|
+
}
|
|
1554
|
+
)
|
|
1555
|
+
] })
|
|
1556
|
+
] });
|
|
1557
|
+
}
|
|
1558
|
+
function FileTree({
|
|
1559
|
+
files = [],
|
|
1560
|
+
activeFile,
|
|
1561
|
+
activePath,
|
|
1562
|
+
title = "Files",
|
|
1563
|
+
onSelectFile,
|
|
1564
|
+
onSelectDirectory,
|
|
1565
|
+
onReplaceFile,
|
|
1566
|
+
onOpenInEditor,
|
|
1567
|
+
openInEditorMode,
|
|
1568
|
+
openInEditorIcon,
|
|
1569
|
+
openInEditorTitle,
|
|
1570
|
+
pinnedPaths,
|
|
1571
|
+
onTogglePin,
|
|
1572
|
+
directoryLoader,
|
|
1573
|
+
pageSize = 10,
|
|
1574
|
+
reloadToken
|
|
1575
|
+
}) {
|
|
1093
1576
|
const tree = useMemo(() => buildTree(files), [files]);
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1577
|
+
const selectedPath = activePath ?? activeFile ?? "";
|
|
1578
|
+
const [rootEntries, setRootEntries] = useState([]);
|
|
1579
|
+
const [rootLoading, setRootLoading] = useState(false);
|
|
1580
|
+
const [rootError, setRootError] = useState(null);
|
|
1581
|
+
useEffect(() => {
|
|
1582
|
+
if (!directoryLoader) return;
|
|
1583
|
+
let cancelled = false;
|
|
1584
|
+
const loadRoot = async () => {
|
|
1585
|
+
setRootLoading(true);
|
|
1586
|
+
setRootError(null);
|
|
1587
|
+
try {
|
|
1588
|
+
const entries = await directoryLoader("");
|
|
1589
|
+
if (!cancelled) {
|
|
1590
|
+
setRootEntries(entries);
|
|
1591
|
+
}
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
if (!cancelled) {
|
|
1594
|
+
setRootError(err instanceof Error ? err.message : "Failed to load files");
|
|
1595
|
+
}
|
|
1596
|
+
} finally {
|
|
1597
|
+
if (!cancelled) {
|
|
1598
|
+
setRootLoading(false);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
};
|
|
1602
|
+
void loadRoot();
|
|
1603
|
+
return () => {
|
|
1604
|
+
cancelled = true;
|
|
1605
|
+
};
|
|
1606
|
+
}, [directoryLoader, reloadToken]);
|
|
1607
|
+
return /* @__PURE__ */ jsxs("div", { className: "min-w-48 border-r bg-muted/30 overflow-auto text-foreground", children: [
|
|
1608
|
+
/* @__PURE__ */ jsx("div", { className: "p-2 border-b text-xs font-medium text-muted-foreground uppercase tracking-wide", children: title }),
|
|
1609
|
+
pinnedPaths && pinnedPaths.size > 0 && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 border-b flex flex-wrap gap-1", children: Array.from(pinnedPaths).map(([p, isDir]) => /* @__PURE__ */ jsxs(
|
|
1610
|
+
"button",
|
|
1611
|
+
{
|
|
1612
|
+
onClick: () => isDir ? onSelectDirectory?.(p) : onSelectFile(p),
|
|
1613
|
+
className: `inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs hover:bg-muted/50 ${(activePath ?? activeFile ?? "") === p ? "bg-primary/10 text-primary" : "text-muted-foreground"}`,
|
|
1614
|
+
children: [
|
|
1615
|
+
isDir ? /* @__PURE__ */ jsx(Folder, { className: "h-2.5 w-2.5 shrink-0" }) : /* @__PURE__ */ jsx(Pin, { className: "h-2.5 w-2.5 shrink-0" }),
|
|
1616
|
+
/* @__PURE__ */ jsx("span", { className: "truncate max-w-[120px]", children: p.split("/").pop() }),
|
|
1617
|
+
onTogglePin && /* @__PURE__ */ jsx(
|
|
1618
|
+
"span",
|
|
1619
|
+
{
|
|
1620
|
+
onClick: (e) => {
|
|
1621
|
+
e.stopPropagation();
|
|
1622
|
+
onTogglePin(p, isDir);
|
|
1623
|
+
},
|
|
1624
|
+
className: "hover:text-destructive",
|
|
1625
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-2.5 w-2.5" })
|
|
1626
|
+
}
|
|
1627
|
+
)
|
|
1628
|
+
]
|
|
1629
|
+
},
|
|
1630
|
+
p
|
|
1631
|
+
)) }),
|
|
1632
|
+
/* @__PURE__ */ jsx("div", { className: "p-1", children: directoryLoader ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1633
|
+
rootLoading && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-2 py-1 text-xs text-muted-foreground", children: [
|
|
1634
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }),
|
|
1635
|
+
/* @__PURE__ */ jsx("span", { children: "Loading..." })
|
|
1636
|
+
] }),
|
|
1637
|
+
rootError && /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-xs text-destructive", children: rootError }),
|
|
1638
|
+
rootEntries.map((entry) => /* @__PURE__ */ jsx(
|
|
1639
|
+
LazyTreeNode,
|
|
1640
|
+
{
|
|
1641
|
+
entry,
|
|
1642
|
+
activePath: selectedPath,
|
|
1643
|
+
onSelectFile,
|
|
1644
|
+
onSelectDirectory,
|
|
1645
|
+
onOpenInEditor,
|
|
1646
|
+
openInEditorMode,
|
|
1647
|
+
openInEditorIcon,
|
|
1648
|
+
openInEditorTitle,
|
|
1649
|
+
pinnedPaths,
|
|
1650
|
+
onTogglePin,
|
|
1651
|
+
directoryLoader,
|
|
1652
|
+
pageSize,
|
|
1653
|
+
reloadToken
|
|
1654
|
+
},
|
|
1655
|
+
entry.path
|
|
1656
|
+
))
|
|
1657
|
+
] }) : /* @__PURE__ */ jsx(
|
|
1097
1658
|
TreeNodeComponent,
|
|
1098
1659
|
{
|
|
1099
1660
|
node: tree,
|
|
1100
|
-
|
|
1661
|
+
activePath: selectedPath,
|
|
1101
1662
|
onSelect: onSelectFile,
|
|
1102
|
-
|
|
1663
|
+
onSelectDirectory,
|
|
1664
|
+
onReplaceFile,
|
|
1665
|
+
onOpenInEditor,
|
|
1666
|
+
openInEditorMode,
|
|
1667
|
+
openInEditorIcon,
|
|
1668
|
+
openInEditorTitle,
|
|
1669
|
+
pinnedPaths,
|
|
1670
|
+
onTogglePin,
|
|
1671
|
+
pageSize
|
|
1103
1672
|
}
|
|
1104
1673
|
) })
|
|
1105
1674
|
] });
|
|
@@ -1408,6 +1977,7 @@ function EditModal({
|
|
|
1408
1977
|
renderError,
|
|
1409
1978
|
previewError,
|
|
1410
1979
|
previewLoading,
|
|
1980
|
+
initialTreePath,
|
|
1411
1981
|
initialState = {},
|
|
1412
1982
|
hideFileTree = false,
|
|
1413
1983
|
...sessionOptions
|
|
@@ -1422,17 +1992,27 @@ function EditModal({
|
|
|
1422
1992
|
const [pillContainer, setPillContainer] = useState(null);
|
|
1423
1993
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
1424
1994
|
const [isSaving, setIsSaving] = useState(false);
|
|
1995
|
+
const [saveStatus, setSaveStatus] = useState("saved");
|
|
1996
|
+
const [lastSavedSnapshot, setLastSavedSnapshot] = useState("");
|
|
1425
1997
|
const [saveError, setSaveError] = useState(null);
|
|
1426
1998
|
const [pendingClose, setPendingClose] = useState(null);
|
|
1999
|
+
const [treePath, setTreePath] = useState(initialTreePath ?? "");
|
|
2000
|
+
const wasOpenRef = useRef(false);
|
|
1427
2001
|
const currentCodeRef = useRef("");
|
|
1428
2002
|
const session = useEditSession(sessionOptions);
|
|
1429
2003
|
const code = getActiveContent(session);
|
|
2004
|
+
const effectiveTreePath = treePath || session.activeFile;
|
|
1430
2005
|
currentCodeRef.current = code;
|
|
1431
2006
|
const files = useMemo(() => getFiles(session.project), [session.project]);
|
|
2007
|
+
const projectSnapshot = useMemo(
|
|
2008
|
+
() => Array.from(session.project.files.entries()).sort(([a], [b]) => a.localeCompare(b)).map(([path, file]) => `${path}\0${file.content}`).join(""),
|
|
2009
|
+
[session.project]
|
|
2010
|
+
);
|
|
1432
2011
|
const hasChanges = code !== (session.originalProject.files.get(session.activeFile)?.content ?? "");
|
|
1433
2012
|
const fileType = useMemo(() => getFileType(session.activeFile), [session.activeFile]);
|
|
1434
2013
|
const isCompilableFile = isCompilable(session.activeFile);
|
|
1435
|
-
const
|
|
2014
|
+
const isMarkdown = isMarkdownFile(session.activeFile);
|
|
2015
|
+
const showPreviewToggle = isCompilableFile || isMarkdown;
|
|
1436
2016
|
const handleBobbinChanges = useCallback((changes) => {
|
|
1437
2017
|
setBobbinChanges(changes);
|
|
1438
2018
|
}, []);
|
|
@@ -1454,6 +2034,26 @@ ${bobbinYaml}
|
|
|
1454
2034
|
setBobbinChanges([]);
|
|
1455
2035
|
};
|
|
1456
2036
|
const hasSaveHandler = onSave || onSaveProject;
|
|
2037
|
+
useEffect(() => {
|
|
2038
|
+
if (isOpen && !wasOpenRef.current) {
|
|
2039
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2040
|
+
setSaveStatus("saved");
|
|
2041
|
+
setSaveError(null);
|
|
2042
|
+
}
|
|
2043
|
+
wasOpenRef.current = isOpen;
|
|
2044
|
+
}, [isOpen, projectSnapshot]);
|
|
2045
|
+
useEffect(() => {
|
|
2046
|
+
if (!hasSaveHandler) return;
|
|
2047
|
+
if (projectSnapshot === lastSavedSnapshot) {
|
|
2048
|
+
if (saveStatus !== "saving" && saveStatus !== "saved") {
|
|
2049
|
+
setSaveStatus("saved");
|
|
2050
|
+
}
|
|
2051
|
+
return;
|
|
2052
|
+
}
|
|
2053
|
+
if (saveStatus === "saved" || saveStatus === "error") {
|
|
2054
|
+
setSaveStatus("unsaved");
|
|
2055
|
+
}
|
|
2056
|
+
}, [projectSnapshot, lastSavedSnapshot, saveStatus, hasSaveHandler]);
|
|
1457
2057
|
const handleClose = useCallback(() => {
|
|
1458
2058
|
const editCount = session.history.length;
|
|
1459
2059
|
const finalCode = code;
|
|
@@ -1470,6 +2070,7 @@ ${bobbinYaml}
|
|
|
1470
2070
|
const handleSaveAndClose = useCallback(async () => {
|
|
1471
2071
|
if (!pendingClose || !hasSaveHandler) return;
|
|
1472
2072
|
setIsSaving(true);
|
|
2073
|
+
setSaveStatus("saving");
|
|
1473
2074
|
setSaveError(null);
|
|
1474
2075
|
try {
|
|
1475
2076
|
if (onSaveProject) {
|
|
@@ -1477,6 +2078,8 @@ ${bobbinYaml}
|
|
|
1477
2078
|
} else if (onSave) {
|
|
1478
2079
|
await onSave(pendingClose.code);
|
|
1479
2080
|
}
|
|
2081
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2082
|
+
setSaveStatus("saved");
|
|
1480
2083
|
setShowConfirm(false);
|
|
1481
2084
|
setEditInput("");
|
|
1482
2085
|
session.clearError();
|
|
@@ -1484,10 +2087,11 @@ ${bobbinYaml}
|
|
|
1484
2087
|
setPendingClose(null);
|
|
1485
2088
|
} catch (e) {
|
|
1486
2089
|
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
2090
|
+
setSaveStatus("error");
|
|
1487
2091
|
} finally {
|
|
1488
2092
|
setIsSaving(false);
|
|
1489
2093
|
}
|
|
1490
|
-
}, [pendingClose, onSave, onSaveProject, session, onClose]);
|
|
2094
|
+
}, [pendingClose, onSave, onSaveProject, session, onClose, projectSnapshot, hasSaveHandler]);
|
|
1491
2095
|
const handleDiscard = useCallback(() => {
|
|
1492
2096
|
if (!pendingClose) return;
|
|
1493
2097
|
setShowConfirm(false);
|
|
@@ -1504,6 +2108,7 @@ ${bobbinYaml}
|
|
|
1504
2108
|
const handleDirectSave = useCallback(async () => {
|
|
1505
2109
|
if (!hasSaveHandler) return;
|
|
1506
2110
|
setIsSaving(true);
|
|
2111
|
+
setSaveStatus("saving");
|
|
1507
2112
|
setSaveError(null);
|
|
1508
2113
|
try {
|
|
1509
2114
|
if (onSaveProject) {
|
|
@@ -1511,12 +2116,15 @@ ${bobbinYaml}
|
|
|
1511
2116
|
} else if (onSave && currentCodeRef.current) {
|
|
1512
2117
|
await onSave(currentCodeRef.current);
|
|
1513
2118
|
}
|
|
2119
|
+
setLastSavedSnapshot(projectSnapshot);
|
|
2120
|
+
setSaveStatus("saved");
|
|
1514
2121
|
} catch (e) {
|
|
1515
2122
|
setSaveError(e instanceof Error ? e.message : "Save failed");
|
|
2123
|
+
setSaveStatus("error");
|
|
1516
2124
|
} finally {
|
|
1517
2125
|
setIsSaving(false);
|
|
1518
2126
|
}
|
|
1519
|
-
}, [onSave, onSaveProject, session.project]);
|
|
2127
|
+
}, [onSave, onSaveProject, session.project, hasSaveHandler, projectSnapshot]);
|
|
1520
2128
|
if (!isOpen) return null;
|
|
1521
2129
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1522
2130
|
/* @__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: [
|
|
@@ -1545,30 +2153,26 @@ ${bobbinYaml}
|
|
|
1545
2153
|
children: showTree ? /* @__PURE__ */ jsx(FileCode, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(FolderTree, { className: "h-3 w-3" })
|
|
1546
2154
|
}
|
|
1547
2155
|
),
|
|
2156
|
+
hasSaveHandler && /* @__PURE__ */ jsx(
|
|
2157
|
+
SaveStatusButton,
|
|
2158
|
+
{
|
|
2159
|
+
status: saveStatus,
|
|
2160
|
+
onClick: handleDirectSave,
|
|
2161
|
+
disabled: isSaving,
|
|
2162
|
+
tone: "primary"
|
|
2163
|
+
}
|
|
2164
|
+
),
|
|
1548
2165
|
showPreviewToggle && /* @__PURE__ */ jsxs(
|
|
1549
2166
|
"button",
|
|
1550
2167
|
{
|
|
1551
2168
|
onClick: () => setShowPreview(!showPreview),
|
|
1552
|
-
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"}`,
|
|
2169
|
+
className: `w-[5rem] px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
|
|
1553
2170
|
children: [
|
|
1554
2171
|
showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
|
|
1555
2172
|
showPreview ? "Preview" : "Code"
|
|
1556
2173
|
]
|
|
1557
2174
|
}
|
|
1558
2175
|
),
|
|
1559
|
-
hasSaveHandler && /* @__PURE__ */ jsxs(
|
|
1560
|
-
"button",
|
|
1561
|
-
{
|
|
1562
|
-
onClick: handleDirectSave,
|
|
1563
|
-
disabled: isSaving,
|
|
1564
|
-
className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-primary/20 text-primary disabled:opacity-50",
|
|
1565
|
-
title: "Save changes",
|
|
1566
|
-
children: [
|
|
1567
|
-
isSaving ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsx(Save, { className: "h-3 w-3" }),
|
|
1568
|
-
"Save"
|
|
1569
|
-
]
|
|
1570
|
-
}
|
|
1571
|
-
),
|
|
1572
2176
|
/* @__PURE__ */ jsxs(
|
|
1573
2177
|
"button",
|
|
1574
2178
|
{
|
|
@@ -1589,7 +2193,12 @@ ${bobbinYaml}
|
|
|
1589
2193
|
{
|
|
1590
2194
|
files,
|
|
1591
2195
|
activeFile: session.activeFile,
|
|
1592
|
-
|
|
2196
|
+
activePath: effectiveTreePath,
|
|
2197
|
+
onSelectFile: (path) => {
|
|
2198
|
+
setTreePath(path);
|
|
2199
|
+
session.setActiveFile(path);
|
|
2200
|
+
},
|
|
2201
|
+
onSelectDirectory: (path) => setTreePath(path),
|
|
1593
2202
|
onReplaceFile: session.replaceFile
|
|
1594
2203
|
}
|
|
1595
2204
|
),
|
|
@@ -1620,7 +2229,14 @@ ${bobbinYaml}
|
|
|
1620
2229
|
editable: true,
|
|
1621
2230
|
onChange: session.updateActiveFile
|
|
1622
2231
|
}
|
|
1623
|
-
) :
|
|
2232
|
+
) : isMarkdown && showPreview ? /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none h-full overflow-auto", children: /* @__PURE__ */ jsx(
|
|
2233
|
+
MarkdownPreview,
|
|
2234
|
+
{
|
|
2235
|
+
value: code,
|
|
2236
|
+
editable: true,
|
|
2237
|
+
onChange: session.updateActiveFile
|
|
2238
|
+
}
|
|
2239
|
+
) }) : fileType.category === "text" ? /* @__PURE__ */ jsx(
|
|
1624
2240
|
CodeBlockView,
|
|
1625
2241
|
{
|
|
1626
2242
|
content: code,
|
|
@@ -1635,7 +2251,15 @@ ${bobbinYaml}
|
|
|
1635
2251
|
mimeType: getMimeType(session.activeFile),
|
|
1636
2252
|
fileName: session.activeFile.split("/").pop() ?? session.activeFile
|
|
1637
2253
|
}
|
|
1638
|
-
) : /* @__PURE__ */ jsx(
|
|
2254
|
+
) : /* @__PURE__ */ jsx(
|
|
2255
|
+
CodeBlockView,
|
|
2256
|
+
{
|
|
2257
|
+
content: code,
|
|
2258
|
+
language: fileType.language,
|
|
2259
|
+
editable: true,
|
|
2260
|
+
onChange: session.updateActiveFile
|
|
2261
|
+
}
|
|
2262
|
+
) })
|
|
1639
2263
|
] }),
|
|
1640
2264
|
/* @__PURE__ */ jsx(
|
|
1641
2265
|
EditHistory,
|
|
@@ -1701,6 +2325,77 @@ ${bobbinYaml}
|
|
|
1701
2325
|
)
|
|
1702
2326
|
] });
|
|
1703
2327
|
}
|
|
2328
|
+
function createManifest(services) {
|
|
2329
|
+
return {
|
|
2330
|
+
name: "preview",
|
|
2331
|
+
version: "1.0.0",
|
|
2332
|
+
platform: "browser",
|
|
2333
|
+
image: "@aprovan/patchwork-image-shadcn",
|
|
2334
|
+
services
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function WidgetPreview({
|
|
2338
|
+
code,
|
|
2339
|
+
compiler,
|
|
2340
|
+
services,
|
|
2341
|
+
enabled = true
|
|
2342
|
+
}) {
|
|
2343
|
+
const [loading, setLoading] = useState(false);
|
|
2344
|
+
const [error, setError] = useState(null);
|
|
2345
|
+
const containerRef = useRef(null);
|
|
2346
|
+
const mountedRef = useRef(null);
|
|
2347
|
+
useEffect(() => {
|
|
2348
|
+
if (!enabled || !compiler || !containerRef.current) return;
|
|
2349
|
+
let cancelled = false;
|
|
2350
|
+
const compileAndMount = async () => {
|
|
2351
|
+
setLoading(true);
|
|
2352
|
+
setError(null);
|
|
2353
|
+
try {
|
|
2354
|
+
if (mountedRef.current) {
|
|
2355
|
+
compiler.unmount(mountedRef.current);
|
|
2356
|
+
mountedRef.current = null;
|
|
2357
|
+
}
|
|
2358
|
+
const widget = await compiler.compile(code, createManifest(services), {
|
|
2359
|
+
typescript: true
|
|
2360
|
+
});
|
|
2361
|
+
if (cancelled || !containerRef.current) return;
|
|
2362
|
+
const mounted = await compiler.mount(widget, {
|
|
2363
|
+
target: containerRef.current,
|
|
2364
|
+
mode: "embedded"
|
|
2365
|
+
});
|
|
2366
|
+
mountedRef.current = mounted;
|
|
2367
|
+
} catch (err) {
|
|
2368
|
+
if (!cancelled) {
|
|
2369
|
+
setError(err instanceof Error ? err.message : "Failed to render preview");
|
|
2370
|
+
}
|
|
2371
|
+
} finally {
|
|
2372
|
+
if (!cancelled) {
|
|
2373
|
+
setLoading(false);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
void compileAndMount();
|
|
2378
|
+
return () => {
|
|
2379
|
+
cancelled = true;
|
|
2380
|
+
if (mountedRef.current && compiler) {
|
|
2381
|
+
compiler.unmount(mountedRef.current);
|
|
2382
|
+
mountedRef.current = null;
|
|
2383
|
+
}
|
|
2384
|
+
};
|
|
2385
|
+
}, [code, compiler, enabled, services]);
|
|
2386
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2387
|
+
error && /* @__PURE__ */ jsxs("div", { className: "text-sm text-destructive flex items-center gap-2 p-3", children: [
|
|
2388
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "h-4 w-4 shrink-0" }),
|
|
2389
|
+
/* @__PURE__ */ jsx("span", { children: error })
|
|
2390
|
+
] }),
|
|
2391
|
+
loading && /* @__PURE__ */ jsxs("div", { className: "p-3 flex items-center gap-2 text-muted-foreground", children: [
|
|
2392
|
+
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
2393
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
|
|
2394
|
+
] }),
|
|
2395
|
+
!compiler && enabled && !loading && !error && /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }),
|
|
2396
|
+
/* @__PURE__ */ jsx("div", { ref: containerRef, className: "w-full" })
|
|
2397
|
+
] });
|
|
2398
|
+
}
|
|
1704
2399
|
var VFS_BASE_URL = "/vfs";
|
|
1705
2400
|
var vfsConfigCache = null;
|
|
1706
2401
|
async function getVFSConfig() {
|
|
@@ -1764,7 +2459,7 @@ async function isVFSAvailable() {
|
|
|
1764
2459
|
return false;
|
|
1765
2460
|
}
|
|
1766
2461
|
}
|
|
1767
|
-
function
|
|
2462
|
+
function createManifest2(services) {
|
|
1768
2463
|
return {
|
|
1769
2464
|
name: "preview",
|
|
1770
2465
|
version: "1.0.0",
|
|
@@ -1773,63 +2468,19 @@ function createManifest(services) {
|
|
|
1773
2468
|
services
|
|
1774
2469
|
};
|
|
1775
2470
|
}
|
|
1776
|
-
function
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
async function compileAndMount() {
|
|
1785
|
-
if (!containerRef.current || !compiler) return;
|
|
1786
|
-
setLoading(true);
|
|
1787
|
-
setError(null);
|
|
1788
|
-
try {
|
|
1789
|
-
if (mountedRef.current) {
|
|
1790
|
-
compiler.unmount(mountedRef.current);
|
|
1791
|
-
mountedRef.current = null;
|
|
1792
|
-
}
|
|
1793
|
-
const widget = await compiler.compile(
|
|
1794
|
-
code,
|
|
1795
|
-
createManifest(services),
|
|
1796
|
-
{ typescript: true }
|
|
1797
|
-
);
|
|
1798
|
-
if (cancelled) {
|
|
1799
|
-
return;
|
|
1800
|
-
}
|
|
1801
|
-
const mounted = await compiler.mount(widget, {
|
|
1802
|
-
target: containerRef.current,
|
|
1803
|
-
mode: "embedded"
|
|
1804
|
-
});
|
|
1805
|
-
mountedRef.current = mounted;
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
if (!cancelled) {
|
|
1808
|
-
setError(err instanceof Error ? err.message : "Failed to render JSX");
|
|
1809
|
-
}
|
|
1810
|
-
} finally {
|
|
1811
|
-
if (!cancelled) {
|
|
1812
|
-
setLoading(false);
|
|
1813
|
-
}
|
|
1814
|
-
}
|
|
1815
|
-
}
|
|
1816
|
-
compileAndMount();
|
|
1817
|
-
return () => {
|
|
1818
|
-
cancelled = true;
|
|
1819
|
-
if (mountedRef.current && compiler) {
|
|
1820
|
-
compiler.unmount(mountedRef.current);
|
|
1821
|
-
mountedRef.current = null;
|
|
1822
|
-
}
|
|
1823
|
-
};
|
|
1824
|
-
}, [code, compiler, enabled, services]);
|
|
1825
|
-
return { containerRef, loading, error };
|
|
1826
|
-
}
|
|
1827
|
-
function CodePreview({ code: originalCode, compiler, services, filePath, entrypoint = "index.ts" }) {
|
|
2471
|
+
function CodePreview({
|
|
2472
|
+
code: originalCode,
|
|
2473
|
+
compiler,
|
|
2474
|
+
services,
|
|
2475
|
+
filePath,
|
|
2476
|
+
entrypoint = "index.ts",
|
|
2477
|
+
onOpenEditSession
|
|
2478
|
+
}) {
|
|
1828
2479
|
const [isEditing, setIsEditing] = useState(false);
|
|
1829
2480
|
const [showPreview, setShowPreview] = useState(true);
|
|
1830
2481
|
const [currentCode, setCurrentCode] = useState(originalCode);
|
|
1831
2482
|
const [editCount, setEditCount] = useState(0);
|
|
1832
|
-
const [saveStatus, setSaveStatus] = useState("
|
|
2483
|
+
const [saveStatus, setSaveStatus] = useState("saved");
|
|
1833
2484
|
const [lastSavedCode, setLastSavedCode] = useState(originalCode);
|
|
1834
2485
|
const [vfsPath, setVfsPath] = useState(null);
|
|
1835
2486
|
const currentCodeRef = useRef(currentCode);
|
|
@@ -1865,6 +2516,14 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1865
2516
|
useEffect(() => {
|
|
1866
2517
|
isEditingRef.current = isEditing;
|
|
1867
2518
|
}, [isEditing]);
|
|
2519
|
+
useEffect(() => {
|
|
2520
|
+
if (saveStatus === "saving") return;
|
|
2521
|
+
if (currentCode === lastSavedCode) {
|
|
2522
|
+
if (saveStatus !== "saved") setSaveStatus("saved");
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
if (saveStatus === "saved") setSaveStatus("unsaved");
|
|
2526
|
+
}, [currentCode, lastSavedCode, saveStatus]);
|
|
1868
2527
|
useEffect(() => {
|
|
1869
2528
|
let active = true;
|
|
1870
2529
|
void (async () => {
|
|
@@ -1917,14 +2576,12 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1917
2576
|
setSaveStatus("error");
|
|
1918
2577
|
}
|
|
1919
2578
|
}, [currentCode, getProjectId, getEntryFile]);
|
|
1920
|
-
const
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
showPreview && !isEditing,
|
|
1924
|
-
services
|
|
1925
|
-
);
|
|
2579
|
+
const previewPath = filePath ?? entrypoint;
|
|
2580
|
+
const fileType = useMemo(() => getFileType(previewPath), [previewPath]);
|
|
2581
|
+
const canRenderWidget = fileType.category === "compilable";
|
|
1926
2582
|
const compile = useCallback(
|
|
1927
2583
|
async (code) => {
|
|
2584
|
+
if (!canRenderWidget) return { success: true };
|
|
1928
2585
|
if (!compiler) return { success: true };
|
|
1929
2586
|
const errors = [];
|
|
1930
2587
|
const originalError = console.error;
|
|
@@ -1935,7 +2592,7 @@ function CodePreview({ code: originalCode, compiler, services, filePath, entrypo
|
|
|
1935
2592
|
try {
|
|
1936
2593
|
await compiler.compile(
|
|
1937
2594
|
code,
|
|
1938
|
-
|
|
2595
|
+
createManifest2(services),
|
|
1939
2596
|
{ typescript: true }
|
|
1940
2597
|
);
|
|
1941
2598
|
return { success: true };
|
|
@@ -1953,15 +2610,64 @@ ${errors.join("\n")}` : "";
|
|
|
1953
2610
|
console.error = originalError;
|
|
1954
2611
|
}
|
|
1955
2612
|
},
|
|
1956
|
-
[compiler, services]
|
|
2613
|
+
[canRenderWidget, compiler, services]
|
|
1957
2614
|
);
|
|
1958
2615
|
const handleRevert = () => {
|
|
1959
2616
|
setCurrentCode(originalCode);
|
|
1960
2617
|
setEditCount(0);
|
|
1961
2618
|
};
|
|
1962
2619
|
const hasChanges = currentCode !== originalCode;
|
|
2620
|
+
const previewBody = useMemo(() => {
|
|
2621
|
+
if (canRenderWidget) {
|
|
2622
|
+
return /* @__PURE__ */ jsx(
|
|
2623
|
+
WidgetPreview,
|
|
2624
|
+
{
|
|
2625
|
+
code: currentCode,
|
|
2626
|
+
compiler,
|
|
2627
|
+
services,
|
|
2628
|
+
enabled: showPreview && !isEditing
|
|
2629
|
+
}
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
if (fileType.category === "media") {
|
|
2633
|
+
return /* @__PURE__ */ jsx(
|
|
2634
|
+
MediaPreview,
|
|
2635
|
+
{
|
|
2636
|
+
content: currentCode,
|
|
2637
|
+
mimeType: fileType.mimeType,
|
|
2638
|
+
fileName: previewPath
|
|
2639
|
+
}
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
if (fileType.language === "markdown") {
|
|
2643
|
+
return /* @__PURE__ */ jsx("div", { className: "p-4 prose prose-sm dark:prose-invert max-w-none", children: /* @__PURE__ */ jsx(MarkdownPreview, { value: currentCode }) });
|
|
2644
|
+
}
|
|
2645
|
+
return /* @__PURE__ */ jsx(
|
|
2646
|
+
CodeBlockView,
|
|
2647
|
+
{
|
|
2648
|
+
content: currentCode,
|
|
2649
|
+
language: fileType.language
|
|
2650
|
+
}
|
|
2651
|
+
);
|
|
2652
|
+
}, [canRenderWidget, compiler, currentCode, fileType, isEditing, previewPath, services, showPreview]);
|
|
2653
|
+
const handleOpenEditor = useCallback(async () => {
|
|
2654
|
+
if (!onOpenEditSession) {
|
|
2655
|
+
setIsEditing(true);
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
const projectId = await getProjectId();
|
|
2659
|
+
const entryFile = getEntryFile();
|
|
2660
|
+
const initialProject = createSingleFileProject(currentCode, entryFile, projectId);
|
|
2661
|
+
onOpenEditSession({
|
|
2662
|
+
projectId,
|
|
2663
|
+
entryFile,
|
|
2664
|
+
filePath,
|
|
2665
|
+
initialCode: currentCode,
|
|
2666
|
+
initialProject
|
|
2667
|
+
});
|
|
2668
|
+
}, [onOpenEditSession, getProjectId, getEntryFile, currentCode, filePath]);
|
|
1963
2669
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1964
|
-
/* @__PURE__ */ jsxs("div", { className: "
|
|
2670
|
+
/* @__PURE__ */ jsxs("div", { className: "border rounded-lg overflow-hidden min-w-0", children: [
|
|
1965
2671
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-3 py-2 bg-muted/50 border-b rounded-t-lg", children: [
|
|
1966
2672
|
/* @__PURE__ */ jsx(Code, { className: "h-4 w-4 text-muted-foreground" }),
|
|
1967
2673
|
editCount > 0 && /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground flex items-center gap-1", children: [
|
|
@@ -1970,19 +2676,6 @@ ${errors.join("\n")}` : "";
|
|
|
1970
2676
|
" edit",
|
|
1971
2677
|
editCount !== 1 ? "s" : ""
|
|
1972
2678
|
] }),
|
|
1973
|
-
/* @__PURE__ */ jsx(
|
|
1974
|
-
"button",
|
|
1975
|
-
{
|
|
1976
|
-
onClick: handleSave,
|
|
1977
|
-
disabled: saveStatus === "saving",
|
|
1978
|
-
className: `px-2 py-1 text-xs rounded flex items-center gap-1 ${saveStatus === "saved" ? "text-green-600" : saveStatus === "error" ? "text-destructive hover:bg-muted" : "text-muted-foreground hover:bg-muted"}`,
|
|
1979
|
-
title: saveStatus === "saved" ? "Saved to disk" : saveStatus === "saving" ? "Saving..." : saveStatus === "error" ? "Save failed - click to retry" : "Click to save",
|
|
1980
|
-
children: saveStatus === "saving" ? /* @__PURE__ */ jsx(Loader2, { className: "h-3 w-3 animate-spin" }) : /* @__PURE__ */ jsxs("span", { className: "relative", children: [
|
|
1981
|
-
/* @__PURE__ */ jsx(Cloud, { className: "h-3 w-3" }),
|
|
1982
|
-
saveStatus === "saved" && /* @__PURE__ */ jsx(Check, { className: "h-2 w-2 absolute -bottom-0.5 -right-0.5 stroke-[3]" })
|
|
1983
|
-
] })
|
|
1984
|
-
}
|
|
1985
|
-
),
|
|
1986
2679
|
/* @__PURE__ */ jsxs("div", { className: "ml-auto flex gap-1", children: [
|
|
1987
2680
|
hasChanges && /* @__PURE__ */ jsx(
|
|
1988
2681
|
"button",
|
|
@@ -1996,17 +2689,26 @@ ${errors.join("\n")}` : "";
|
|
|
1996
2689
|
/* @__PURE__ */ jsx(
|
|
1997
2690
|
"button",
|
|
1998
2691
|
{
|
|
1999
|
-
onClick: () =>
|
|
2692
|
+
onClick: () => void handleOpenEditor(),
|
|
2000
2693
|
className: "px-2 py-1 text-xs rounded flex items-center gap-1 hover:bg-muted",
|
|
2001
2694
|
title: "Edit component",
|
|
2002
2695
|
children: /* @__PURE__ */ jsx(Pencil, { className: "h-3 w-3" })
|
|
2003
2696
|
}
|
|
2004
2697
|
),
|
|
2698
|
+
/* @__PURE__ */ jsx(
|
|
2699
|
+
SaveStatusButton,
|
|
2700
|
+
{
|
|
2701
|
+
status: saveStatus,
|
|
2702
|
+
onClick: handleSave,
|
|
2703
|
+
disabled: saveStatus === "saving",
|
|
2704
|
+
tone: "muted"
|
|
2705
|
+
}
|
|
2706
|
+
),
|
|
2005
2707
|
/* @__PURE__ */ jsxs(
|
|
2006
2708
|
"button",
|
|
2007
2709
|
{
|
|
2008
2710
|
onClick: () => setShowPreview(!showPreview),
|
|
2009
|
-
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"}`,
|
|
2711
|
+
className: `w-[5rem] px-2 py-1 text-xs rounded flex items-center gap-1 ${showPreview ? "bg-primary text-primary-foreground" : "hover:bg-primary/20 text-primary"}`,
|
|
2010
2712
|
children: [
|
|
2011
2713
|
showPreview ? /* @__PURE__ */ jsx(Eye, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(Code, { className: "h-3 w-3" }),
|
|
2012
2714
|
showPreview ? "Preview" : "Code"
|
|
@@ -2015,16 +2717,13 @@ ${errors.join("\n")}` : "";
|
|
|
2015
2717
|
)
|
|
2016
2718
|
] })
|
|
2017
2719
|
] }),
|
|
2018
|
-
showPreview ? /* @__PURE__ */
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
] }) : !compiler ? /* @__PURE__ */ jsx("div", { className: "p-3 text-sm text-muted-foreground", children: "Compiler not initialized" }) : null,
|
|
2026
|
-
/* @__PURE__ */ jsx("div", { ref: containerRef })
|
|
2027
|
-
] }) : /* @__PURE__ */ jsx("div", { className: "p-3 bg-muted/30 overflow-auto max-h-96", children: /* @__PURE__ */ jsx("pre", { className: "text-xs whitespace-pre-wrap break-words m-0", children: /* @__PURE__ */ jsx("code", { children: currentCode }) }) })
|
|
2720
|
+
showPreview ? /* @__PURE__ */ jsx("div", { className: "bg-white overflow-y-auto overflow-x-hidden max-h-[60vh]", children: previewBody }) : /* @__PURE__ */ jsx("div", { className: "bg-muted/30 overflow-auto max-h-[60vh]", children: /* @__PURE__ */ jsx(
|
|
2721
|
+
CodeBlockView,
|
|
2722
|
+
{
|
|
2723
|
+
content: currentCode,
|
|
2724
|
+
language: fileType.language
|
|
2725
|
+
}
|
|
2726
|
+
) })
|
|
2028
2727
|
] }),
|
|
2029
2728
|
/* @__PURE__ */ jsx(
|
|
2030
2729
|
EditModal,
|
|
@@ -2042,6 +2741,7 @@ ${errors.join("\n")}` : "";
|
|
|
2042
2741
|
const entryFile = getEntryFile();
|
|
2043
2742
|
const project = createSingleFileProject(finalCode, entryFile, projectId);
|
|
2044
2743
|
await saveProject(project);
|
|
2744
|
+
setLastSavedCode(finalCode);
|
|
2045
2745
|
setSaveStatus("saved");
|
|
2046
2746
|
} catch (err) {
|
|
2047
2747
|
console.warn("[VFS] Failed to save project:", err);
|
|
@@ -2052,49 +2752,77 @@ ${errors.join("\n")}` : "";
|
|
|
2052
2752
|
},
|
|
2053
2753
|
originalCode: currentCode,
|
|
2054
2754
|
compile,
|
|
2055
|
-
renderPreview: (code) => /* @__PURE__ */ jsx(
|
|
2755
|
+
renderPreview: (code) => /* @__PURE__ */ jsx(
|
|
2756
|
+
WidgetPreview,
|
|
2757
|
+
{
|
|
2758
|
+
code,
|
|
2759
|
+
compiler,
|
|
2760
|
+
services
|
|
2761
|
+
}
|
|
2762
|
+
)
|
|
2056
2763
|
}
|
|
2057
2764
|
)
|
|
2058
2765
|
] });
|
|
2059
2766
|
}
|
|
2060
|
-
function
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
services
|
|
2767
|
+
function DefaultBadge({
|
|
2768
|
+
children,
|
|
2769
|
+
className = ""
|
|
2064
2770
|
}) {
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
/* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }),
|
|
2073
|
-
/* @__PURE__ */ jsx("span", { className: "text-sm", children: "Rendering preview..." })
|
|
2074
|
-
] }),
|
|
2075
|
-
!compiler && !loading && !error && /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Compiler not initialized" }),
|
|
2076
|
-
/* @__PURE__ */ jsx("div", { ref: containerRef })
|
|
2077
|
-
] });
|
|
2078
|
-
}
|
|
2079
|
-
function DefaultBadge({ children, className = "" }) {
|
|
2080
|
-
return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`, children });
|
|
2771
|
+
return /* @__PURE__ */ jsx(
|
|
2772
|
+
"span",
|
|
2773
|
+
{
|
|
2774
|
+
className: `inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${className}`,
|
|
2775
|
+
children
|
|
2776
|
+
}
|
|
2777
|
+
);
|
|
2081
2778
|
}
|
|
2082
|
-
function DefaultDialog({
|
|
2779
|
+
function DefaultDialog({
|
|
2780
|
+
children,
|
|
2781
|
+
open,
|
|
2782
|
+
onOpenChange
|
|
2783
|
+
}) {
|
|
2083
2784
|
if (!open) return null;
|
|
2084
|
-
return /* @__PURE__ */ jsx(
|
|
2785
|
+
return /* @__PURE__ */ jsx(
|
|
2786
|
+
"div",
|
|
2787
|
+
{
|
|
2788
|
+
className: "fixed inset-0 z-50 bg-black/50",
|
|
2789
|
+
onClick: () => onOpenChange?.(false),
|
|
2790
|
+
children: /* @__PURE__ */ jsx(
|
|
2791
|
+
"div",
|
|
2792
|
+
{
|
|
2793
|
+
className: "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 bg-background p-6 shadow-lg rounded-lg",
|
|
2794
|
+
onClick: (e) => e.stopPropagation(),
|
|
2795
|
+
children
|
|
2796
|
+
}
|
|
2797
|
+
)
|
|
2798
|
+
}
|
|
2799
|
+
);
|
|
2085
2800
|
}
|
|
2086
2801
|
function ServicesInspector({
|
|
2087
2802
|
namespaces,
|
|
2088
2803
|
services = [],
|
|
2089
2804
|
BadgeComponent = DefaultBadge,
|
|
2090
|
-
DialogComponent = DefaultDialog
|
|
2805
|
+
DialogComponent = DefaultDialog,
|
|
2806
|
+
DialogHeaderComponent = ({ children }) => /* @__PURE__ */ jsx("div", { className: "flex justify-between items-center mb-4", children }),
|
|
2807
|
+
DialogContentComponent = ({ children, className = "" }) => /* @__PURE__ */ jsx("div", { className, children }),
|
|
2808
|
+
DialogCloseComponent = ({ onClose }) => /* @__PURE__ */ jsx(
|
|
2809
|
+
"button",
|
|
2810
|
+
{
|
|
2811
|
+
onClick: () => onClose?.(),
|
|
2812
|
+
className: "text-muted-foreground hover:text-foreground",
|
|
2813
|
+
children: "\xD7"
|
|
2814
|
+
}
|
|
2815
|
+
)
|
|
2091
2816
|
}) {
|
|
2092
2817
|
const [open, setOpen] = useState(false);
|
|
2093
2818
|
if (namespaces.length === 0) return null;
|
|
2094
|
-
const groupedServices = services.reduce(
|
|
2095
|
-
(acc
|
|
2096
|
-
|
|
2097
|
-
|
|
2819
|
+
const groupedServices = services.reduce(
|
|
2820
|
+
(acc, svc) => {
|
|
2821
|
+
(acc[svc.namespace] ??= []).push(svc);
|
|
2822
|
+
return acc;
|
|
2823
|
+
},
|
|
2824
|
+
{}
|
|
2825
|
+
);
|
|
2098
2826
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2099
2827
|
/* @__PURE__ */ jsxs(
|
|
2100
2828
|
"button",
|
|
@@ -2112,11 +2840,11 @@ function ServicesInspector({
|
|
|
2112
2840
|
}
|
|
2113
2841
|
),
|
|
2114
2842
|
/* @__PURE__ */ jsxs(DialogComponent, { open, onOpenChange: setOpen, children: [
|
|
2115
|
-
/* @__PURE__ */ jsxs(
|
|
2843
|
+
/* @__PURE__ */ jsxs(DialogHeaderComponent, { children: [
|
|
2116
2844
|
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Available Services" }),
|
|
2117
|
-
/* @__PURE__ */ jsx(
|
|
2845
|
+
/* @__PURE__ */ jsx(DialogCloseComponent, { onClose: () => setOpen(false) })
|
|
2118
2846
|
] }),
|
|
2119
|
-
/* @__PURE__ */ jsx(
|
|
2847
|
+
/* @__PURE__ */ jsx(DialogContentComponent, { className: "space-y-3 max-h-96 overflow-auto", children: namespaces.map((ns) => /* @__PURE__ */ jsxs("details", { open: namespaces.length === 1, children: [
|
|
2120
2848
|
/* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full p-2 rounded bg-muted/50 hover:bg-muted transition-colors cursor-pointer", children: [
|
|
2121
2849
|
/* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2122
2850
|
/* @__PURE__ */ jsx("span", { className: "font-medium text-sm", children: ns }),
|
|
@@ -2128,11 +2856,11 @@ function ServicesInspector({
|
|
|
2128
2856
|
] }),
|
|
2129
2857
|
/* @__PURE__ */ jsx("div", { className: "ml-6 mt-2 space-y-2", children: groupedServices[ns]?.map((svc) => /* @__PURE__ */ jsxs("details", { children: [
|
|
2130
2858
|
/* @__PURE__ */ jsxs("summary", { className: "flex items-center gap-2 w-full text-left text-sm hover:text-foreground text-muted-foreground transition-colors cursor-pointer", children: [
|
|
2131
|
-
/* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
|
|
2859
|
+
svc.parameters && /* @__PURE__ */ jsx(ChevronDown, { className: "h-3 w-3" }),
|
|
2132
2860
|
/* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: svc.procedure }),
|
|
2133
2861
|
/* @__PURE__ */ jsx("span", { className: "truncate text-xs opacity-70", children: svc.description })
|
|
2134
2862
|
] }),
|
|
2135
|
-
/* @__PURE__ */ jsx("div", { className: "ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48", children: /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0", children: JSON.stringify(svc.parameters
|
|
2863
|
+
svc.parameters && /* @__PURE__ */ jsx("div", { className: "ml-5 mt-1 p-2 rounded border bg-muted/30 overflow-auto max-h-48", children: /* @__PURE__ */ jsx("pre", { className: "text-xs font-mono whitespace-pre-wrap break-words m-0", children: JSON.stringify(svc.parameters, null, 2) }) })
|
|
2136
2864
|
] }, svc.name)) ?? /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No tool details available" }) })
|
|
2137
2865
|
] }, ns)) })
|
|
2138
2866
|
] })
|
|
@@ -2264,4 +2992,4 @@ function cn(...inputs) {
|
|
|
2264
2992
|
return twMerge(clsx(inputs));
|
|
2265
2993
|
}
|
|
2266
2994
|
|
|
2267
|
-
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 };
|
|
2995
|
+
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 };
|