@aprovan/patchwork-editor 0.1.2-dev.4a481e4 → 0.1.2-dev.4d82df8
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 +3 -3
- package/dist/components/CodePreview.d.ts +2 -3
- package/dist/components/MarkdownEditor.d.ts +1 -1
- package/dist/components/MarkdownPreview.d.ts +1 -1
- package/dist/components/SaveStatusButton.d.ts +1 -1
- package/dist/components/ServicesInspector.d.ts +1 -1
- package/dist/components/WidgetPreview.d.ts +1 -1
- package/dist/components/edit/CodeBlockView.d.ts +1 -1
- package/dist/components/edit/EditHistory.d.ts +1 -1
- package/dist/components/edit/EditModal.d.ts +1 -1
- package/dist/components/edit/FileTree.d.ts +1 -1
- package/dist/components/edit/MediaPreview.d.ts +1 -1
- package/dist/components/edit/SaveConfirmDialog.d.ts +1 -1
- package/dist/components/edit/index.d.ts +1 -0
- package/dist/components/edit/useEditSession.d.ts +1 -1
- package/dist/components/edit/useProjectState.d.ts +17 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +414 -403
- package/dist/lib/vfs.d.ts +2 -3
- package/package.json +6 -4
- package/src/components/CodePreview.tsx +7 -10
- package/src/components/MarkdownEditor.tsx +3 -3
- package/src/components/MarkdownPreview.tsx +2 -2
- package/src/components/ServicesInspector.tsx +1 -1
- package/src/components/WidgetPreview.tsx +1 -1
- package/src/components/edit/CodeBlockView.tsx +6 -2
- package/src/components/edit/EditHistory.tsx +1 -1
- package/src/components/edit/EditModal.tsx +6 -6
- package/src/components/edit/FileTree.tsx +2 -3
- package/src/components/edit/MediaPreview.tsx +1 -1
- package/src/components/edit/index.ts +4 -0
- package/src/components/edit/useEditSession.ts +19 -109
- package/src/components/edit/useProjectState.ts +122 -0
- package/src/index.ts +15 -18
- package/src/lib/code-extractor.ts +1 -1
- package/src/lib/vfs.ts +4 -5
- package/tsconfig.json +2 -1
- package/dist/components/index.d.ts +0 -7
- package/dist/lib/index.d.ts +0 -4
- package/src/components/index.ts +0 -7
- package/src/lib/index.ts +0 -4
package/dist/lib/vfs.d.ts
CHANGED
|
@@ -8,11 +8,10 @@ export declare function getVFSConfig(): Promise<{
|
|
|
8
8
|
}>;
|
|
9
9
|
/**
|
|
10
10
|
* Get the VFS store instance (creates one if needed).
|
|
11
|
-
* Store uses HttpBackend to persist to the stitchery server.
|
|
12
11
|
*/
|
|
13
12
|
export declare function getVFSStore(): VFSStore;
|
|
14
13
|
/**
|
|
15
|
-
* Save a virtual project to disk via the
|
|
14
|
+
* Save a virtual project to disk via the VFS server.
|
|
16
15
|
* Projects are saved under their ID in the VFS directory.
|
|
17
16
|
*/
|
|
18
17
|
export declare function saveProject(project: VirtualProject): Promise<void>;
|
|
@@ -31,6 +30,6 @@ export declare function saveFile(path: string, content: string): Promise<void>;
|
|
|
31
30
|
export declare function loadFile(path: string, encoding?: 'utf8' | 'base64'): Promise<string>;
|
|
32
31
|
export declare function subscribeToChanges(callback: (record: ChangeRecord) => void): () => void;
|
|
33
32
|
/**
|
|
34
|
-
* Check if VFS is available (
|
|
33
|
+
* Check if VFS is available (backend server has /vfs routes enabled).
|
|
35
34
|
*/
|
|
36
35
|
export declare function isVFSAvailable(): Promise<boolean>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aprovan/patchwork-editor",
|
|
3
|
-
"version": "0.1.2-dev.
|
|
3
|
+
"version": "0.1.2-dev.4d82df8",
|
|
4
4
|
"description": "Components for facilitating widget generation and editing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,15 +18,17 @@
|
|
|
18
18
|
"@tiptap/pm": "^3.19.0",
|
|
19
19
|
"@tiptap/react": "^3.19.0",
|
|
20
20
|
"@tiptap/starter-kit": "^3.19.0",
|
|
21
|
+
"@types/dompurify": "^3.2.0",
|
|
21
22
|
"clsx": "^2.1.1",
|
|
23
|
+
"dompurify": "^3.4.8",
|
|
22
24
|
"lucide-react": "^0.511.0",
|
|
23
25
|
"react-markdown": "^10.1.0",
|
|
24
26
|
"remark-gfm": "^4.0.1",
|
|
25
27
|
"shiki": "^3.22.0",
|
|
26
28
|
"tailwind-merge": "^3.4.0",
|
|
27
29
|
"tiptap-markdown": "^0.9.0",
|
|
28
|
-
"@aprovan/bobbin": "0.1.0-dev.
|
|
29
|
-
"@aprovan/patchwork-compiler": "0.1.2-dev.
|
|
30
|
+
"@aprovan/bobbin": "0.1.0-dev.4d82df8",
|
|
31
|
+
"@aprovan/patchwork-compiler": "0.1.2-dev.4d82df8"
|
|
30
32
|
},
|
|
31
33
|
"peerDependencies": {
|
|
32
34
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
"typescript": "^5.9.3"
|
|
40
42
|
},
|
|
41
43
|
"scripts": {
|
|
42
|
-
"build": "tsup && tsc
|
|
44
|
+
"build": "tsup && tsc -p tsconfig.json --emitDeclarationOnly",
|
|
43
45
|
"dev": "tsup --watch",
|
|
44
46
|
"typecheck": "tsc --noEmit"
|
|
45
47
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
2
|
-
import { Code, Eye, Pencil, RotateCcw, MessageSquare } from 'lucide-react';
|
|
3
|
-
import type { Compiler, Manifest } from '@aprovan/patchwork-compiler';
|
|
4
1
|
import { createSingleFileProject } from '@aprovan/patchwork-compiler';
|
|
2
|
+
import { Code, Eye, Pencil, RotateCcw, MessageSquare } from 'lucide-react';
|
|
3
|
+
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
|
4
|
+
import { saveProject, getVFSConfig, loadFile, subscribeToChanges } from '../lib/vfs';
|
|
5
5
|
import { EditModal, type CompileFn, CodeBlockView, MediaPreview, getFileType } from './edit';
|
|
6
|
+
import { MarkdownPreview } from './MarkdownPreview';
|
|
6
7
|
import { SaveStatusButton, type SaveStatus } from './SaveStatusButton';
|
|
7
8
|
import { WidgetPreview } from './WidgetPreview';
|
|
8
|
-
import {
|
|
9
|
-
import { saveProject, getVFSConfig, loadFile, subscribeToChanges } from '../lib/vfs';
|
|
10
|
-
import type { VirtualProject } from '@aprovan/patchwork-compiler';
|
|
9
|
+
import type { Compiler, Manifest , VirtualProject } from '@aprovan/patchwork-compiler';
|
|
11
10
|
|
|
12
11
|
interface CodePreviewProps {
|
|
13
12
|
code: string;
|
|
@@ -157,8 +156,7 @@ export function CodePreview({
|
|
|
157
156
|
await saveProject(project);
|
|
158
157
|
setLastSavedCode(currentCode);
|
|
159
158
|
setSaveStatus('saved');
|
|
160
|
-
} catch
|
|
161
|
-
console.warn('[VFS] Failed to save project:', err);
|
|
159
|
+
} catch {
|
|
162
160
|
setSaveStatus('error');
|
|
163
161
|
}
|
|
164
162
|
}, [currentCode, getProjectId, getEntryFile]);
|
|
@@ -340,8 +338,7 @@ export function CodePreview({
|
|
|
340
338
|
await saveProject(project);
|
|
341
339
|
setLastSavedCode(finalCode);
|
|
342
340
|
setSaveStatus('saved');
|
|
343
|
-
} catch
|
|
344
|
-
console.warn('[VFS] Failed to save project:', err);
|
|
341
|
+
} catch {
|
|
345
342
|
setSaveStatus('error');
|
|
346
343
|
}
|
|
347
344
|
})();
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { useEditor, EditorContent } from '@tiptap/react';
|
|
2
|
-
import StarterKit from '@tiptap/starter-kit';
|
|
3
1
|
import Placeholder from '@tiptap/extension-placeholder';
|
|
4
2
|
import Typography from '@tiptap/extension-typography';
|
|
5
|
-
import { Markdown } from 'tiptap-markdown';
|
|
6
3
|
import { TextSelection } from '@tiptap/pm/state';
|
|
4
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
5
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
7
6
|
import { useEffect, useCallback, useRef } from 'react';
|
|
7
|
+
import { Markdown } from 'tiptap-markdown';
|
|
8
8
|
import { CodeBlockExtension } from './CodeBlockExtension';
|
|
9
9
|
|
|
10
10
|
interface MarkdownEditorProps {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import Typography from '@tiptap/extension-typography';
|
|
1
2
|
import { useEditor, EditorContent } from '@tiptap/react';
|
|
2
3
|
import StarterKit from '@tiptap/starter-kit';
|
|
3
|
-
import Typography from '@tiptap/extension-typography';
|
|
4
|
-
import { Markdown } from 'tiptap-markdown';
|
|
5
4
|
import { useEffect, useCallback, useRef, useState } from 'react';
|
|
5
|
+
import { Markdown } from 'tiptap-markdown';
|
|
6
6
|
import { CodeBlockExtension } from './CodeBlockExtension';
|
|
7
7
|
|
|
8
8
|
function parseFrontmatter(content: string): { frontmatter: string; body: string } {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react';
|
|
2
1
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import type { Compiler, Manifest, MountedWidget } from '@aprovan/patchwork-compiler';
|
|
4
4
|
|
|
5
5
|
export interface WidgetPreviewProps {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
1
2
|
import { useCallback, useRef, useEffect, useState, useMemo } from 'react';
|
|
2
3
|
import { createHighlighter, type Highlighter, type BundledLanguage } from 'shiki';
|
|
3
4
|
|
|
@@ -117,12 +118,15 @@ export function CodeBlockView({ content, language, editable = false, onChange }:
|
|
|
117
118
|
const highlightedHtml = useMemo(() => {
|
|
118
119
|
if (!highlighter) return null;
|
|
119
120
|
try {
|
|
120
|
-
|
|
121
|
+
const raw = highlighter.codeToHtml(content, {
|
|
121
122
|
lang: shikiLang,
|
|
122
123
|
theme: 'github-light',
|
|
123
124
|
});
|
|
125
|
+
return DOMPurify.sanitize(raw, {
|
|
126
|
+
ALLOWED_TAGS: ['pre', 'code', 'span', 'div'],
|
|
127
|
+
ALLOWED_ATTR: ['class', 'style'],
|
|
128
|
+
});
|
|
124
129
|
} catch {
|
|
125
|
-
// Fallback if language is not supported
|
|
126
130
|
return null;
|
|
127
131
|
}
|
|
128
132
|
}, [highlighter, content, shikiLang]);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
|
|
2
2
|
import {
|
|
3
3
|
Code,
|
|
4
4
|
Eye,
|
|
@@ -11,18 +11,18 @@ import {
|
|
|
11
11
|
FolderTree,
|
|
12
12
|
FileCode,
|
|
13
13
|
} from 'lucide-react';
|
|
14
|
+
import { useState, useCallback, useMemo, useRef, useEffect, type ReactNode } from 'react';
|
|
14
15
|
import { MarkdownEditor } from '../MarkdownEditor';
|
|
15
16
|
import { MarkdownPreview } from '../MarkdownPreview';
|
|
16
17
|
import { SaveStatusButton, type SaveStatus } from '../SaveStatusButton';
|
|
18
|
+
import { CodeBlockView } from './CodeBlockView';
|
|
17
19
|
import { EditHistory } from './EditHistory';
|
|
18
20
|
import { FileTree } from './FileTree';
|
|
19
|
-
import {
|
|
20
|
-
import { CodeBlockView } from './CodeBlockView';
|
|
21
|
+
import { getFileType, isCompilable, isMarkdownFile, getMimeType } from './fileTypes';
|
|
21
22
|
import { MediaPreview } from './MediaPreview';
|
|
22
|
-
import {
|
|
23
|
+
import { SaveConfirmDialog } from './SaveConfirmDialog';
|
|
23
24
|
import { getActiveContent, getFiles } from './types';
|
|
24
|
-
import {
|
|
25
|
-
import { Bobbin, serializeChangesToYAML, type Change } from '@aprovan/bobbin';
|
|
25
|
+
import { useEditSession, type UseEditSessionOptions } from './useEditSession';
|
|
26
26
|
import type { VirtualProject } from '@aprovan/patchwork-compiler';
|
|
27
27
|
|
|
28
28
|
// Simple hash for React key to force re-render on code changes
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { useMemo, useState, useRef, useCallback, useEffect, type ReactNode } from 'react';
|
|
2
1
|
import {
|
|
3
2
|
ChevronRight,
|
|
4
3
|
ChevronDown,
|
|
@@ -12,8 +11,9 @@ import {
|
|
|
12
11
|
PinOff,
|
|
13
12
|
X,
|
|
14
13
|
} from 'lucide-react';
|
|
15
|
-
import type
|
|
14
|
+
import { useMemo, useState, useRef, useCallback, useEffect, type ReactNode } from 'react';
|
|
16
15
|
import { isMediaFile } from './fileTypes';
|
|
16
|
+
import type { VirtualFile } from '@aprovan/patchwork-compiler';
|
|
17
17
|
|
|
18
18
|
interface TreeNode {
|
|
19
19
|
name: string;
|
|
@@ -384,7 +384,6 @@ function LazyTreeNode({
|
|
|
384
384
|
.catch((err) => setLoadError(err instanceof Error ? err.message : 'Failed to load directory'))
|
|
385
385
|
.finally(() => setLoading(false));
|
|
386
386
|
}
|
|
387
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
388
387
|
}, [reloadToken]);
|
|
389
388
|
|
|
390
389
|
const isActive = entry.path === activePath;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
// Intentional public API boundary for the edit sub-module.
|
|
2
|
+
// Consumers: src/index.ts, CodePreview.tsx, and internal edit components.
|
|
3
|
+
|
|
1
4
|
export * from './types';
|
|
2
5
|
export * from './api';
|
|
6
|
+
export * from './useProjectState';
|
|
3
7
|
export * from './useEditSession';
|
|
4
8
|
export * from './EditHistory';
|
|
5
9
|
export * from './EditModal';
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { useState, useCallback
|
|
2
|
-
import type { VirtualProject } from "@aprovan/patchwork-compiler";
|
|
3
|
-
import { createSingleFileProject } from "@aprovan/patchwork-compiler";
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
4
2
|
import { sendEditRequest } from "./api";
|
|
3
|
+
import { useProjectState } from "./useProjectState";
|
|
5
4
|
import type {
|
|
6
5
|
EditHistoryEntry,
|
|
7
6
|
EditSessionState,
|
|
8
7
|
EditSessionActions,
|
|
9
8
|
CompileFn,
|
|
10
9
|
} from "./types";
|
|
10
|
+
import type { VirtualProject } from "@aprovan/patchwork-compiler";
|
|
11
11
|
|
|
12
12
|
export interface UseEditSessionOptions {
|
|
13
13
|
originalCode?: string;
|
|
@@ -17,67 +17,20 @@ export interface UseEditSessionOptions {
|
|
|
17
17
|
apiEndpoint?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function cloneProject(project: VirtualProject): VirtualProject {
|
|
21
|
-
return {
|
|
22
|
-
...project,
|
|
23
|
-
files: new Map(project.files),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
20
|
export function useEditSession(
|
|
28
21
|
options: UseEditSessionOptions,
|
|
29
22
|
): EditSessionState & EditSessionActions {
|
|
30
23
|
const {
|
|
31
|
-
originalCode,
|
|
32
|
-
originalProject: providedProject,
|
|
33
|
-
initialActiveFile,
|
|
34
24
|
compile,
|
|
35
25
|
apiEndpoint,
|
|
36
26
|
} = options;
|
|
37
27
|
|
|
38
|
-
|
|
39
|
-
"[useEditSession] providedProject:",
|
|
40
|
-
providedProject?.id,
|
|
41
|
-
"files:",
|
|
42
|
-
providedProject ? Array.from(providedProject.files.keys()) : "none",
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
const originalProject = useMemo(
|
|
46
|
-
() => providedProject ?? createSingleFileProject(originalCode ?? ""),
|
|
47
|
-
[providedProject, originalCode],
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
// Track the last project we synced from to detect reference changes
|
|
51
|
-
const lastSyncedProjectRef = useRef<VirtualProject>(originalProject);
|
|
52
|
-
|
|
53
|
-
const [project, setProject] = useState<VirtualProject>(originalProject);
|
|
54
|
-
const [activeFile, setActiveFile] = useState(
|
|
55
|
-
initialActiveFile && originalProject.files.has(initialActiveFile)
|
|
56
|
-
? initialActiveFile
|
|
57
|
-
: originalProject.entry,
|
|
58
|
-
);
|
|
59
|
-
const [history, setHistory] = useState<EditHistoryEntry[]>([]);
|
|
28
|
+
const state = useProjectState(options);
|
|
60
29
|
const [isApplying, setIsApplying] = useState(false);
|
|
61
30
|
const [error, setError] = useState<string | null>(null);
|
|
62
31
|
const [streamingNotes, setStreamingNotes] = useState<string[]>([]);
|
|
63
32
|
const [pendingPrompt, setPendingPrompt] = useState<string | null>(null);
|
|
64
33
|
|
|
65
|
-
// Sync state when the original project reference changes (new project or files loaded)
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
if (originalProject !== lastSyncedProjectRef.current) {
|
|
68
|
-
lastSyncedProjectRef.current = originalProject;
|
|
69
|
-
setProject(originalProject);
|
|
70
|
-
setActiveFile(
|
|
71
|
-
initialActiveFile && originalProject.files.has(initialActiveFile)
|
|
72
|
-
? initialActiveFile
|
|
73
|
-
: originalProject.entry,
|
|
74
|
-
);
|
|
75
|
-
setHistory([]);
|
|
76
|
-
setError(null);
|
|
77
|
-
setStreamingNotes([]);
|
|
78
|
-
}
|
|
79
|
-
}, [originalProject, initialActiveFile]);
|
|
80
|
-
|
|
81
34
|
const performEdit = useCallback(
|
|
82
35
|
async (
|
|
83
36
|
currentCode: string,
|
|
@@ -122,11 +75,6 @@ export function useEditSession(
|
|
|
122
75
|
[compile, apiEndpoint],
|
|
123
76
|
);
|
|
124
77
|
|
|
125
|
-
const currentCode = useMemo(
|
|
126
|
-
() => project.files.get(activeFile)?.content ?? "",
|
|
127
|
-
[project, activeFile],
|
|
128
|
-
);
|
|
129
|
-
|
|
130
78
|
const submitEdit = useCallback(
|
|
131
79
|
async (prompt: string) => {
|
|
132
80
|
if (!prompt.trim() || isApplying) return;
|
|
@@ -137,16 +85,16 @@ export function useEditSession(
|
|
|
137
85
|
setPendingPrompt(prompt);
|
|
138
86
|
|
|
139
87
|
try {
|
|
140
|
-
const result = await performEdit(currentCode, prompt);
|
|
141
|
-
setProject((prev) => {
|
|
142
|
-
const updated =
|
|
143
|
-
const file = updated.files.get(activeFile);
|
|
88
|
+
const result = await performEdit(state.currentCode, prompt);
|
|
89
|
+
state.setProject((prev: VirtualProject) => {
|
|
90
|
+
const updated = { ...prev, files: new Map(prev.files) };
|
|
91
|
+
const file = updated.files.get(state.activeFile);
|
|
144
92
|
if (file) {
|
|
145
|
-
updated.files.set(activeFile, { ...file, content: result.newCode });
|
|
93
|
+
updated.files.set(state.activeFile, { ...file, content: result.newCode });
|
|
146
94
|
}
|
|
147
95
|
return updated;
|
|
148
96
|
});
|
|
149
|
-
setHistory((prev) => [...prev, ...result.entries]);
|
|
97
|
+
state.setHistory((prev: EditHistoryEntry[]) => [...prev, ...result.entries]);
|
|
150
98
|
} catch (err) {
|
|
151
99
|
setError(err instanceof Error ? err.message : "Edit failed");
|
|
152
100
|
} finally {
|
|
@@ -155,45 +103,7 @@ export function useEditSession(
|
|
|
155
103
|
setPendingPrompt(null);
|
|
156
104
|
}
|
|
157
105
|
},
|
|
158
|
-
[currentCode, activeFile, isApplying, performEdit],
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const revert = useCallback(() => {
|
|
162
|
-
setProject(originalProject);
|
|
163
|
-
setActiveFile(originalProject.entry);
|
|
164
|
-
setHistory([]);
|
|
165
|
-
setError(null);
|
|
166
|
-
setStreamingNotes([]);
|
|
167
|
-
}, [originalProject]);
|
|
168
|
-
|
|
169
|
-
const updateActiveFile = useCallback(
|
|
170
|
-
(content: string) => {
|
|
171
|
-
setProject((prev) => {
|
|
172
|
-
const updated = cloneProject(prev);
|
|
173
|
-
const file = updated.files.get(activeFile);
|
|
174
|
-
if (file) {
|
|
175
|
-
updated.files.set(activeFile, { ...file, content });
|
|
176
|
-
}
|
|
177
|
-
return updated;
|
|
178
|
-
});
|
|
179
|
-
},
|
|
180
|
-
[activeFile],
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
const replaceFile = useCallback(
|
|
184
|
-
(path: string, content: string, encoding: "utf8" | "base64" = "utf8") => {
|
|
185
|
-
setProject((prev) => {
|
|
186
|
-
const updated = cloneProject(prev);
|
|
187
|
-
const file = updated.files.get(path);
|
|
188
|
-
if (file) {
|
|
189
|
-
updated.files.set(path, { ...file, content, encoding });
|
|
190
|
-
} else {
|
|
191
|
-
updated.files.set(path, { path, content, encoding });
|
|
192
|
-
}
|
|
193
|
-
return updated;
|
|
194
|
-
});
|
|
195
|
-
},
|
|
196
|
-
[],
|
|
106
|
+
[state.currentCode, state.activeFile, state.setProject, state.setHistory, isApplying, performEdit],
|
|
197
107
|
);
|
|
198
108
|
|
|
199
109
|
const clearError = useCallback(() => {
|
|
@@ -201,19 +111,19 @@ export function useEditSession(
|
|
|
201
111
|
}, []);
|
|
202
112
|
|
|
203
113
|
return {
|
|
204
|
-
project,
|
|
205
|
-
originalProject,
|
|
206
|
-
activeFile,
|
|
207
|
-
history,
|
|
114
|
+
project: state.project,
|
|
115
|
+
originalProject: state.originalProject,
|
|
116
|
+
activeFile: state.activeFile,
|
|
117
|
+
history: state.history,
|
|
208
118
|
isApplying,
|
|
209
119
|
error,
|
|
210
120
|
streamingNotes,
|
|
211
121
|
pendingPrompt,
|
|
212
122
|
submitEdit,
|
|
213
|
-
revert,
|
|
214
|
-
updateActiveFile,
|
|
215
|
-
setActiveFile,
|
|
123
|
+
revert: state.revert,
|
|
124
|
+
updateActiveFile: state.updateActiveFile,
|
|
125
|
+
setActiveFile: state.setActiveFile,
|
|
216
126
|
clearError,
|
|
217
|
-
replaceFile,
|
|
127
|
+
replaceFile: state.replaceFile,
|
|
218
128
|
};
|
|
219
129
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createSingleFileProject } from "@aprovan/patchwork-compiler";
|
|
2
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
|
3
|
+
import type {
|
|
4
|
+
EditHistoryEntry,
|
|
5
|
+
EditSessionState,
|
|
6
|
+
} from "./types";
|
|
7
|
+
import type { VirtualProject } from "@aprovan/patchwork-compiler";
|
|
8
|
+
|
|
9
|
+
export interface UseProjectStateOptions {
|
|
10
|
+
originalCode?: string;
|
|
11
|
+
originalProject?: VirtualProject;
|
|
12
|
+
initialActiveFile?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function cloneProject(project: VirtualProject): VirtualProject {
|
|
16
|
+
return {
|
|
17
|
+
...project,
|
|
18
|
+
files: new Map(project.files),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useProjectState(
|
|
23
|
+
options: UseProjectStateOptions,
|
|
24
|
+
): Omit<EditSessionState, "isApplying" | "error" | "streamingNotes" | "pendingPrompt"> & {
|
|
25
|
+
setProject: React.Dispatch<React.SetStateAction<VirtualProject>>;
|
|
26
|
+
setActiveFile: React.Dispatch<React.SetStateAction<string>>;
|
|
27
|
+
history: EditHistoryEntry[];
|
|
28
|
+
setHistory: React.Dispatch<React.SetStateAction<EditHistoryEntry[]>>;
|
|
29
|
+
revert: () => void;
|
|
30
|
+
updateActiveFile: (content: string) => void;
|
|
31
|
+
replaceFile: (path: string, content: string, encoding?: "utf8" | "base64") => void;
|
|
32
|
+
currentCode: string;
|
|
33
|
+
} {
|
|
34
|
+
const {
|
|
35
|
+
originalCode,
|
|
36
|
+
originalProject: providedProject,
|
|
37
|
+
initialActiveFile,
|
|
38
|
+
} = options;
|
|
39
|
+
|
|
40
|
+
const originalProject = useMemo(
|
|
41
|
+
() => providedProject ?? createSingleFileProject(originalCode ?? ""),
|
|
42
|
+
[providedProject, originalCode],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const lastSyncedProjectRef = useRef<VirtualProject>(originalProject);
|
|
46
|
+
|
|
47
|
+
const [project, setProject] = useState<VirtualProject>(originalProject);
|
|
48
|
+
const [activeFile, setActiveFile] = useState(
|
|
49
|
+
initialActiveFile && originalProject.files.has(initialActiveFile)
|
|
50
|
+
? initialActiveFile
|
|
51
|
+
: originalProject.entry,
|
|
52
|
+
);
|
|
53
|
+
const [history, setHistory] = useState<EditHistoryEntry[]>([]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (originalProject !== lastSyncedProjectRef.current) {
|
|
57
|
+
lastSyncedProjectRef.current = originalProject;
|
|
58
|
+
setProject(originalProject);
|
|
59
|
+
setActiveFile(
|
|
60
|
+
initialActiveFile && originalProject.files.has(initialActiveFile)
|
|
61
|
+
? initialActiveFile
|
|
62
|
+
: originalProject.entry,
|
|
63
|
+
);
|
|
64
|
+
setHistory([]);
|
|
65
|
+
}
|
|
66
|
+
}, [originalProject, initialActiveFile]);
|
|
67
|
+
|
|
68
|
+
const currentCode = useMemo(
|
|
69
|
+
() => project.files.get(activeFile)?.content ?? "",
|
|
70
|
+
[project, activeFile],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const revert = useCallback(() => {
|
|
74
|
+
setProject(originalProject);
|
|
75
|
+
setActiveFile(originalProject.entry);
|
|
76
|
+
setHistory([]);
|
|
77
|
+
}, [originalProject]);
|
|
78
|
+
|
|
79
|
+
const updateActiveFile = useCallback(
|
|
80
|
+
(content: string) => {
|
|
81
|
+
setProject((prev: VirtualProject) => {
|
|
82
|
+
const updated = cloneProject(prev);
|
|
83
|
+
const file = updated.files.get(activeFile);
|
|
84
|
+
if (file) {
|
|
85
|
+
updated.files.set(activeFile, { ...file, content });
|
|
86
|
+
}
|
|
87
|
+
return updated;
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
[activeFile],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const replaceFile = useCallback(
|
|
94
|
+
(path: string, content: string, encoding: "utf8" | "base64" = "utf8") => {
|
|
95
|
+
setProject((prev: VirtualProject) => {
|
|
96
|
+
const updated = cloneProject(prev);
|
|
97
|
+
const file = updated.files.get(path);
|
|
98
|
+
if (file) {
|
|
99
|
+
updated.files.set(path, { ...file, content, encoding });
|
|
100
|
+
} else {
|
|
101
|
+
updated.files.set(path, { path, content, encoding });
|
|
102
|
+
}
|
|
103
|
+
return updated;
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
[],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
project,
|
|
111
|
+
originalProject,
|
|
112
|
+
activeFile,
|
|
113
|
+
setActiveFile,
|
|
114
|
+
history,
|
|
115
|
+
setHistory,
|
|
116
|
+
setProject,
|
|
117
|
+
revert,
|
|
118
|
+
updateActiveFile,
|
|
119
|
+
replaceFile,
|
|
120
|
+
currentCode,
|
|
121
|
+
};
|
|
122
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
MarkdownPreview,
|
|
8
|
-
ServicesInspector,
|
|
9
|
-
type ServiceInfo,
|
|
10
|
-
} from "./components";
|
|
1
|
+
export { CodeBlockExtension } from "./components/CodeBlockExtension";
|
|
2
|
+
export { CodePreview } from "./components/CodePreview";
|
|
3
|
+
export { WidgetPreview } from "./components/WidgetPreview";
|
|
4
|
+
export { MarkdownEditor } from "./components/MarkdownEditor";
|
|
5
|
+
export { MarkdownPreview } from "./components/MarkdownPreview";
|
|
6
|
+
export { ServicesInspector, type ServiceInfo } from "./components/ServicesInspector";
|
|
11
7
|
|
|
12
|
-
// Edit components
|
|
8
|
+
// Edit components — intentional public API boundary (see components/edit/index.ts)
|
|
13
9
|
export {
|
|
14
10
|
EditModal,
|
|
15
11
|
EditHistory,
|
|
@@ -18,9 +14,11 @@ export {
|
|
|
18
14
|
CodeBlockView,
|
|
19
15
|
MediaPreview,
|
|
20
16
|
useEditSession,
|
|
17
|
+
useProjectState,
|
|
21
18
|
sendEditRequest,
|
|
22
19
|
type EditModalProps,
|
|
23
20
|
type UseEditSessionOptions,
|
|
21
|
+
type UseProjectStateOptions,
|
|
24
22
|
type EditHistoryEntry,
|
|
25
23
|
type EditSessionState,
|
|
26
24
|
type EditSessionActions,
|
|
@@ -50,9 +48,7 @@ export {
|
|
|
50
48
|
getMimeType,
|
|
51
49
|
} from "./components/edit";
|
|
52
50
|
|
|
53
|
-
// Lib utilities
|
|
54
51
|
export {
|
|
55
|
-
// Code extractor
|
|
56
52
|
extractCodeBlocks,
|
|
57
53
|
findFirstCodeBlock,
|
|
58
54
|
hasCodeBlock,
|
|
@@ -62,8 +58,9 @@ export {
|
|
|
62
58
|
type CodePart,
|
|
63
59
|
type ParsedPart,
|
|
64
60
|
type ExtractOptions,
|
|
61
|
+
} from "./lib/code-extractor";
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
export {
|
|
67
64
|
parseCodeBlockAttributes,
|
|
68
65
|
parseCodeBlocks,
|
|
69
66
|
findDiffMarkers,
|
|
@@ -78,8 +75,9 @@ export {
|
|
|
78
75
|
type CodeBlock,
|
|
79
76
|
type DiffBlock,
|
|
80
77
|
type ParsedEditResponse,
|
|
78
|
+
} from "./lib/diff";
|
|
81
79
|
|
|
82
|
-
|
|
80
|
+
export {
|
|
83
81
|
getVFSConfig,
|
|
84
82
|
getVFSStore,
|
|
85
83
|
saveProject,
|
|
@@ -87,7 +85,6 @@ export {
|
|
|
87
85
|
listProjects,
|
|
88
86
|
saveFile,
|
|
89
87
|
isVFSAvailable,
|
|
88
|
+
} from "./lib/vfs";
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
cn,
|
|
93
|
-
} from "./lib";
|
|
90
|
+
export { cn } from "./lib/utils";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { VirtualFile, VirtualProject } from '@aprovan/patchwork-compiler';
|
|
2
1
|
import { createProjectFromFiles, detectMainFile } from '@aprovan/patchwork-compiler';
|
|
2
|
+
import type { VirtualFile, VirtualProject } from '@aprovan/patchwork-compiler';
|
|
3
3
|
|
|
4
4
|
// Matches fenced code blocks with optional attributes: ```language attr="value"\n...content...```
|
|
5
5
|
// Captures: [1] = language (optional), [2] = attributes (optional), [3] = content
|