@aprovan/patchwork-editor 0.1.0 → 0.1.2-dev.03aaf5b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -7
- package/dist/components/CodeBlockExtension.d.ts +2 -0
- package/dist/components/CodePreview.d.ts +13 -0
- package/dist/components/MarkdownEditor.d.ts +10 -0
- package/dist/components/ServicesInspector.d.ts +49 -0
- package/dist/components/edit/CodeBlockView.d.ts +7 -0
- package/dist/components/edit/EditHistory.d.ts +10 -0
- package/dist/components/edit/EditModal.d.ts +20 -0
- package/dist/components/edit/FileTree.d.ts +8 -0
- package/dist/components/edit/MediaPreview.d.ts +6 -0
- package/dist/components/edit/SaveConfirmDialog.d.ts +9 -0
- package/dist/components/edit/api.d.ts +8 -0
- package/dist/components/edit/fileTypes.d.ts +14 -0
- package/dist/components/edit/index.d.ts +10 -0
- package/dist/components/edit/types.d.ts +41 -0
- package/dist/components/edit/useEditSession.d.ts +9 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/index.d.ts +3 -331
- package/dist/index.js +812 -142
- package/dist/lib/code-extractor.d.ts +44 -0
- package/dist/lib/diff.d.ts +90 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/vfs.d.ts +36 -0
- package/package.json +5 -4
- package/src/components/CodeBlockExtension.tsx +1 -1
- package/src/components/CodePreview.tsx +64 -4
- package/src/components/edit/CodeBlockView.tsx +188 -0
- package/src/components/edit/EditModal.tsx +172 -30
- package/src/components/edit/FileTree.tsx +67 -13
- package/src/components/edit/MediaPreview.tsx +124 -0
- package/src/components/edit/SaveConfirmDialog.tsx +60 -0
- package/src/components/edit/fileTypes.ts +125 -0
- package/src/components/edit/index.ts +4 -0
- package/src/components/edit/types.ts +3 -0
- package/src/components/edit/useEditSession.ts +56 -11
- package/src/index.ts +17 -0
- package/src/lib/diff.ts +2 -1
- package/src/lib/vfs.ts +28 -10
- package/tsup.config.ts +10 -5
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
export type FileCategory = 'compilable' | 'text' | 'media' | 'binary';
|
|
2
|
+
|
|
3
|
+
export interface FileTypeInfo {
|
|
4
|
+
category: FileCategory;
|
|
5
|
+
language: string | null;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const COMPILABLE_EXTENSIONS = ['.tsx', '.jsx', '.ts', '.js'];
|
|
10
|
+
const MEDIA_EXTENSIONS = ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.webm'];
|
|
11
|
+
const TEXT_EXTENSIONS = ['.json', '.yaml', '.yml', '.md', '.txt', '.css', '.html', '.xml', '.toml'];
|
|
12
|
+
|
|
13
|
+
const EXTENSION_TO_LANGUAGE: Record<string, string> = {
|
|
14
|
+
'.tsx': 'tsx',
|
|
15
|
+
'.jsx': 'jsx',
|
|
16
|
+
'.ts': 'typescript',
|
|
17
|
+
'.js': 'javascript',
|
|
18
|
+
'.json': 'json',
|
|
19
|
+
'.yaml': 'yaml',
|
|
20
|
+
'.yml': 'yaml',
|
|
21
|
+
'.md': 'markdown',
|
|
22
|
+
'.txt': 'text',
|
|
23
|
+
'.css': 'css',
|
|
24
|
+
'.html': 'html',
|
|
25
|
+
'.xml': 'xml',
|
|
26
|
+
'.toml': 'toml',
|
|
27
|
+
'.svg': 'xml',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const EXTENSION_TO_MIME: Record<string, string> = {
|
|
31
|
+
'.tsx': 'text/typescript-jsx',
|
|
32
|
+
'.jsx': 'text/javascript-jsx',
|
|
33
|
+
'.ts': 'text/typescript',
|
|
34
|
+
'.js': 'text/javascript',
|
|
35
|
+
'.json': 'application/json',
|
|
36
|
+
'.yaml': 'text/yaml',
|
|
37
|
+
'.yml': 'text/yaml',
|
|
38
|
+
'.md': 'text/markdown',
|
|
39
|
+
'.txt': 'text/plain',
|
|
40
|
+
'.css': 'text/css',
|
|
41
|
+
'.html': 'text/html',
|
|
42
|
+
'.xml': 'application/xml',
|
|
43
|
+
'.toml': 'text/toml',
|
|
44
|
+
'.svg': 'image/svg+xml',
|
|
45
|
+
'.png': 'image/png',
|
|
46
|
+
'.jpg': 'image/jpeg',
|
|
47
|
+
'.jpeg': 'image/jpeg',
|
|
48
|
+
'.gif': 'image/gif',
|
|
49
|
+
'.webp': 'image/webp',
|
|
50
|
+
'.mp4': 'video/mp4',
|
|
51
|
+
'.mov': 'video/quicktime',
|
|
52
|
+
'.webm': 'video/webm',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function getExtension(path: string): string {
|
|
56
|
+
const lastDot = path.lastIndexOf('.');
|
|
57
|
+
if (lastDot === -1) return '';
|
|
58
|
+
return path.slice(lastDot).toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getFileType(path: string): FileTypeInfo {
|
|
62
|
+
const ext = getExtension(path);
|
|
63
|
+
|
|
64
|
+
if (COMPILABLE_EXTENSIONS.includes(ext)) {
|
|
65
|
+
return {
|
|
66
|
+
category: 'compilable',
|
|
67
|
+
language: EXTENSION_TO_LANGUAGE[ext] ?? null,
|
|
68
|
+
mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (TEXT_EXTENSIONS.includes(ext)) {
|
|
73
|
+
return {
|
|
74
|
+
category: 'text',
|
|
75
|
+
language: EXTENSION_TO_LANGUAGE[ext] ?? null,
|
|
76
|
+
mimeType: EXTENSION_TO_MIME[ext] ?? 'text/plain',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (MEDIA_EXTENSIONS.includes(ext)) {
|
|
81
|
+
return {
|
|
82
|
+
category: 'media',
|
|
83
|
+
language: ext === '.svg' ? 'xml' : null,
|
|
84
|
+
mimeType: EXTENSION_TO_MIME[ext] ?? 'application/octet-stream',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
category: 'binary',
|
|
90
|
+
language: null,
|
|
91
|
+
mimeType: 'application/octet-stream',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function isCompilable(path: string): boolean {
|
|
96
|
+
return COMPILABLE_EXTENSIONS.includes(getExtension(path));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isMediaFile(path: string): boolean {
|
|
100
|
+
return MEDIA_EXTENSIONS.includes(getExtension(path));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isTextFile(path: string): boolean {
|
|
104
|
+
return TEXT_EXTENSIONS.includes(getExtension(path));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getLanguageFromExt(path: string): string | null {
|
|
108
|
+
const ext = getExtension(path);
|
|
109
|
+
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getMimeType(path: string): string {
|
|
113
|
+
const ext = getExtension(path);
|
|
114
|
+
return EXTENSION_TO_MIME[ext] ?? 'application/octet-stream';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isImageFile(path: string): boolean {
|
|
118
|
+
const ext = getExtension(path);
|
|
119
|
+
return ['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isVideoFile(path: string): boolean {
|
|
123
|
+
const ext = getExtension(path);
|
|
124
|
+
return ['.mp4', '.mov', '.webm'].includes(ext);
|
|
125
|
+
}
|
|
@@ -23,8 +23,11 @@ export interface EditSessionActions {
|
|
|
23
23
|
updateActiveFile: (content: string) => void;
|
|
24
24
|
setActiveFile: (path: string) => void;
|
|
25
25
|
clearError: () => void;
|
|
26
|
+
replaceFile: (path: string, content: string, encoding?: 'utf8' | 'base64') => void;
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
export type FileEncoding = 'utf8' | 'base64';
|
|
30
|
+
|
|
28
31
|
// Convenience getters
|
|
29
32
|
export function getActiveContent(state: EditSessionState): string {
|
|
30
33
|
return state.project.files.get(state.activeFile)?.content ?? '';
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import { useState, useCallback, useMemo } from
|
|
2
|
-
import type { VirtualProject } from
|
|
3
|
-
import { createSingleFileProject } from
|
|
4
|
-
import { sendEditRequest } from
|
|
1
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
|
2
|
+
import type { VirtualProject } from "@aprovan/patchwork-compiler";
|
|
3
|
+
import { createSingleFileProject } from "@aprovan/patchwork-compiler";
|
|
4
|
+
import { sendEditRequest } from "./api";
|
|
5
5
|
import type {
|
|
6
6
|
EditHistoryEntry,
|
|
7
7
|
EditSessionState,
|
|
8
8
|
EditSessionActions,
|
|
9
9
|
CompileFn,
|
|
10
|
-
} from
|
|
10
|
+
} from "./types";
|
|
11
11
|
|
|
12
12
|
export interface UseEditSessionOptions {
|
|
13
|
-
originalCode
|
|
13
|
+
originalCode?: string;
|
|
14
|
+
originalProject?: VirtualProject;
|
|
14
15
|
compile?: CompileFn;
|
|
15
16
|
apiEndpoint?: string;
|
|
16
17
|
}
|
|
@@ -25,13 +26,28 @@ function cloneProject(project: VirtualProject): VirtualProject {
|
|
|
25
26
|
export function useEditSession(
|
|
26
27
|
options: UseEditSessionOptions,
|
|
27
28
|
): EditSessionState & EditSessionActions {
|
|
28
|
-
const {
|
|
29
|
+
const {
|
|
30
|
+
originalCode,
|
|
31
|
+
originalProject: providedProject,
|
|
32
|
+
compile,
|
|
33
|
+
apiEndpoint,
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
console.log(
|
|
37
|
+
"[useEditSession] providedProject:",
|
|
38
|
+
providedProject?.id,
|
|
39
|
+
"files:",
|
|
40
|
+
providedProject ? Array.from(providedProject.files.keys()) : "none",
|
|
41
|
+
);
|
|
29
42
|
|
|
30
43
|
const originalProject = useMemo(
|
|
31
|
-
() => createSingleFileProject(originalCode),
|
|
32
|
-
[originalCode],
|
|
44
|
+
() => providedProject ?? createSingleFileProject(originalCode ?? ""),
|
|
45
|
+
[providedProject, originalCode],
|
|
33
46
|
);
|
|
34
47
|
|
|
48
|
+
// Track the last project we synced from to detect reference changes
|
|
49
|
+
const lastSyncedProjectRef = useRef<VirtualProject>(originalProject);
|
|
50
|
+
|
|
35
51
|
const [project, setProject] = useState<VirtualProject>(originalProject);
|
|
36
52
|
const [activeFile, setActiveFile] = useState(originalProject.entry);
|
|
37
53
|
const [history, setHistory] = useState<EditHistoryEntry[]>([]);
|
|
@@ -40,6 +56,18 @@ export function useEditSession(
|
|
|
40
56
|
const [streamingNotes, setStreamingNotes] = useState<string[]>([]);
|
|
41
57
|
const [pendingPrompt, setPendingPrompt] = useState<string | null>(null);
|
|
42
58
|
|
|
59
|
+
// Sync state when the original project reference changes (new project or files loaded)
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (originalProject !== lastSyncedProjectRef.current) {
|
|
62
|
+
lastSyncedProjectRef.current = originalProject;
|
|
63
|
+
setProject(originalProject);
|
|
64
|
+
setActiveFile(originalProject.entry);
|
|
65
|
+
setHistory([]);
|
|
66
|
+
setError(null);
|
|
67
|
+
setStreamingNotes([]);
|
|
68
|
+
}
|
|
69
|
+
}, [originalProject]);
|
|
70
|
+
|
|
43
71
|
const performEdit = useCallback(
|
|
44
72
|
async (
|
|
45
73
|
currentCode: string,
|
|
@@ -85,7 +113,7 @@ export function useEditSession(
|
|
|
85
113
|
);
|
|
86
114
|
|
|
87
115
|
const currentCode = useMemo(
|
|
88
|
-
() => project.files.get(activeFile)?.content ??
|
|
116
|
+
() => project.files.get(activeFile)?.content ?? "",
|
|
89
117
|
[project, activeFile],
|
|
90
118
|
);
|
|
91
119
|
|
|
@@ -110,7 +138,7 @@ export function useEditSession(
|
|
|
110
138
|
});
|
|
111
139
|
setHistory((prev) => [...prev, ...result.entries]);
|
|
112
140
|
} catch (err) {
|
|
113
|
-
setError(err instanceof Error ? err.message :
|
|
141
|
+
setError(err instanceof Error ? err.message : "Edit failed");
|
|
114
142
|
} finally {
|
|
115
143
|
setIsApplying(false);
|
|
116
144
|
setStreamingNotes([]);
|
|
@@ -142,6 +170,22 @@ export function useEditSession(
|
|
|
142
170
|
[activeFile],
|
|
143
171
|
);
|
|
144
172
|
|
|
173
|
+
const replaceFile = useCallback(
|
|
174
|
+
(path: string, content: string, encoding: "utf8" | "base64" = "utf8") => {
|
|
175
|
+
setProject((prev) => {
|
|
176
|
+
const updated = cloneProject(prev);
|
|
177
|
+
const file = updated.files.get(path);
|
|
178
|
+
if (file) {
|
|
179
|
+
updated.files.set(path, { ...file, content, encoding });
|
|
180
|
+
} else {
|
|
181
|
+
updated.files.set(path, { path, content, encoding });
|
|
182
|
+
}
|
|
183
|
+
return updated;
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
[],
|
|
187
|
+
);
|
|
188
|
+
|
|
145
189
|
const clearError = useCallback(() => {
|
|
146
190
|
setError(null);
|
|
147
191
|
}, []);
|
|
@@ -160,5 +204,6 @@ export function useEditSession(
|
|
|
160
204
|
updateActiveFile,
|
|
161
205
|
setActiveFile,
|
|
162
206
|
clearError,
|
|
207
|
+
replaceFile,
|
|
163
208
|
};
|
|
164
209
|
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ export {
|
|
|
12
12
|
EditModal,
|
|
13
13
|
EditHistory,
|
|
14
14
|
FileTree,
|
|
15
|
+
SaveConfirmDialog,
|
|
16
|
+
CodeBlockView,
|
|
17
|
+
MediaPreview,
|
|
15
18
|
useEditSession,
|
|
16
19
|
sendEditRequest,
|
|
17
20
|
type EditModalProps,
|
|
@@ -25,8 +28,22 @@ export {
|
|
|
25
28
|
type CompileFn,
|
|
26
29
|
type EditApiOptions,
|
|
27
30
|
type FileTreeProps,
|
|
31
|
+
type SaveConfirmDialogProps,
|
|
32
|
+
type CodeBlockViewProps,
|
|
33
|
+
type MediaPreviewProps,
|
|
34
|
+
type FileCategory,
|
|
35
|
+
type FileTypeInfo,
|
|
36
|
+
type FileEncoding,
|
|
28
37
|
getActiveContent,
|
|
29
38
|
getFiles,
|
|
39
|
+
getFileType,
|
|
40
|
+
isCompilable,
|
|
41
|
+
isMediaFile,
|
|
42
|
+
isTextFile,
|
|
43
|
+
isImageFile,
|
|
44
|
+
isVideoFile,
|
|
45
|
+
getLanguageFromExt,
|
|
46
|
+
getMimeType,
|
|
30
47
|
} from './components/edit';
|
|
31
48
|
|
|
32
49
|
// Lib utilities
|
package/src/lib/diff.ts
CHANGED
|
@@ -155,7 +155,6 @@ export interface ParsedEditResponse {
|
|
|
155
155
|
/**
|
|
156
156
|
* Parse progress notes and diffs from an edit response.
|
|
157
157
|
*
|
|
158
|
-
* New format uses tagged attributes on code fences:
|
|
159
158
|
* ```diff note="Adding handler" path="@/components/Button.tsx"
|
|
160
159
|
* <<<<<<< SEARCH
|
|
161
160
|
* exact code
|
|
@@ -275,6 +274,8 @@ export function applyDiffs(
|
|
|
275
274
|
}
|
|
276
275
|
|
|
277
276
|
export function hasDiffBlocks(text: string): boolean {
|
|
277
|
+
// Reset lastIndex to avoid state issues from the global regex flag
|
|
278
|
+
DIFF_BLOCK_REGEX.lastIndex = 0;
|
|
278
279
|
return DIFF_BLOCK_REGEX.test(text);
|
|
279
280
|
}
|
|
280
281
|
|
package/src/lib/vfs.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
VFSStore,
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
VFSStore,
|
|
3
|
+
HttpBackend,
|
|
4
|
+
type ChangeRecord,
|
|
4
5
|
type VirtualProject,
|
|
5
|
-
type VirtualFile
|
|
6
6
|
} from '@aprovan/patchwork-compiler';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* VFS client for persisting virtual projects to the stitchery server.
|
|
10
|
-
* Uses
|
|
10
|
+
* Uses HttpBackend which makes HTTP requests to /vfs routes.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// VFS base URL - points to stitchery server's VFS routes
|
|
@@ -41,12 +41,15 @@ let storeInstance: VFSStore | null = null;
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Get the VFS store instance (creates one if needed).
|
|
44
|
-
* Store uses
|
|
44
|
+
* Store uses HttpBackend to persist to the stitchery server.
|
|
45
45
|
*/
|
|
46
46
|
export function getVFSStore(): VFSStore {
|
|
47
47
|
if (!storeInstance) {
|
|
48
|
-
const
|
|
49
|
-
storeInstance = new VFSStore(
|
|
48
|
+
const provider = new HttpBackend({ baseUrl: VFS_BASE_URL });
|
|
49
|
+
storeInstance = new VFSStore(provider, {
|
|
50
|
+
sync: true,
|
|
51
|
+
conflictStrategy: 'local-wins',
|
|
52
|
+
});
|
|
50
53
|
}
|
|
51
54
|
return storeInstance;
|
|
52
55
|
}
|
|
@@ -88,9 +91,24 @@ export async function listProjects(): Promise<string[]> {
|
|
|
88
91
|
/**
|
|
89
92
|
* Save a single file to the VFS.
|
|
90
93
|
*/
|
|
91
|
-
export async function saveFile(
|
|
94
|
+
export async function saveFile(path: string, content: string): Promise<void> {
|
|
92
95
|
const store = getVFSStore();
|
|
93
|
-
await store.
|
|
96
|
+
await store.writeFile(path, content);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function loadFile(
|
|
100
|
+
path: string,
|
|
101
|
+
encoding?: 'utf8' | 'base64',
|
|
102
|
+
): Promise<string> {
|
|
103
|
+
const store = getVFSStore();
|
|
104
|
+
return store.readFile(path, encoding);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function subscribeToChanges(
|
|
108
|
+
callback: (record: ChangeRecord) => void,
|
|
109
|
+
): () => void {
|
|
110
|
+
const store = getVFSStore();
|
|
111
|
+
return store.on('change', callback);
|
|
94
112
|
}
|
|
95
113
|
|
|
96
114
|
/**
|
package/tsup.config.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { defineConfig } from
|
|
1
|
+
import { defineConfig } from "tsup";
|
|
2
2
|
|
|
3
3
|
export default defineConfig({
|
|
4
|
-
entry: [
|
|
5
|
-
format: [
|
|
6
|
-
dts:
|
|
4
|
+
entry: ["src/index.ts"],
|
|
5
|
+
format: ["esm"],
|
|
6
|
+
dts: false,
|
|
7
7
|
clean: true,
|
|
8
|
-
external: [
|
|
8
|
+
external: [
|
|
9
|
+
"react",
|
|
10
|
+
"react-dom",
|
|
11
|
+
"@aprovan/bobbin",
|
|
12
|
+
"@aprovan/patchwork-compiler",
|
|
13
|
+
],
|
|
9
14
|
treeshake: true,
|
|
10
15
|
});
|