@hienlh/ppm 0.2.20 → 0.2.21
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/CHANGELOG.md +31 -0
- package/CLAUDE.md +18 -1
- package/bun.lock +57 -59
- package/dist/ppm +0 -0
- package/dist/web/assets/chat-tab-C_U7EwM9.js +6 -0
- package/dist/web/assets/code-editor-DuarTBEe.js +1 -0
- package/dist/web/assets/diff-viewer-sBWBgb7U.js +4 -0
- package/dist/web/assets/git-graph-fOKEZiot.js +1 -0
- package/dist/web/assets/index-3zt5mBwZ.css +2 -0
- package/dist/web/assets/index-CaUQy3Zs.js +21 -0
- package/dist/web/assets/input-CTnwfHVN.js +41 -0
- package/dist/web/assets/settings-tab-C5aWMqIA.js +1 -0
- package/dist/web/assets/{terminal-tab-sGlqTp7k.js → terminal-tab-BEFAYT4S.js} +1 -1
- package/dist/web/assets/use-monaco-theme-BxaccPmI.js +11 -0
- package/dist/web/index.html +35 -8
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +13 -8
- package/docs/project-roadmap.md +22 -4
- package/docs/system-architecture.md +59 -0
- package/package.json +6 -14
- package/src/providers/claude-agent-sdk.ts +2 -2
- package/src/providers/registry.ts +12 -11
- package/src/server/routes/projects.ts +43 -0
- package/src/server/routes/settings.ts +42 -8
- package/src/server/ws/chat.ts +2 -2
- package/src/services/config.service.ts +5 -1
- package/src/services/project.service.ts +1 -0
- package/src/types/config.ts +37 -0
- package/src/types/project.ts +1 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/css.worker.bundle.js +54268 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/editor.worker.bundle.js +14316 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/html.worker.bundle.js +30452 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/json.worker.bundle.js +22095 -0
- package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/ts.worker.bundle.js +225957 -0
- package/src/web/app.tsx +43 -5
- package/src/web/components/chat/chat-history-panel.tsx +106 -0
- package/src/web/components/chat/chat-tab.tsx +27 -19
- package/src/web/components/editor/code-editor.tsx +78 -197
- package/src/web/components/editor/diff-viewer.tsx +59 -176
- package/src/web/components/layout/add-project-form.tsx +151 -0
- package/src/web/components/layout/command-palette.tsx +3 -1
- package/src/web/components/layout/editor-panel.tsx +6 -4
- package/src/web/components/layout/mobile-drawer.tsx +48 -180
- package/src/web/components/layout/mobile-nav.tsx +89 -6
- package/src/web/components/layout/panel-layout.tsx +16 -10
- package/src/web/components/layout/project-bar.tsx +329 -0
- package/src/web/components/layout/project-bottom-sheet.tsx +345 -0
- package/src/web/components/layout/sidebar.tsx +56 -142
- package/src/web/components/layout/tab-bar.tsx +1 -6
- package/src/web/components/layout/tab-content.tsx +0 -10
- package/src/web/components/ui/dialog.tsx +1 -1
- package/src/web/lib/project-avatar.ts +45 -0
- package/src/web/lib/project-palette.ts +18 -0
- package/src/web/lib/use-monaco-theme.ts +29 -0
- package/src/web/stores/panel-store.ts +96 -9
- package/src/web/stores/project-store.ts +87 -3
- package/src/web/stores/settings-store.ts +31 -4
- package/src/web/stores/tab-store.ts +0 -2
- package/vite.config.ts +6 -2
- package/dist/web/assets/arrow-up-from-line-DjfWTP75.js +0 -1
- package/dist/web/assets/button-CQ5h5gxS.js +0 -41
- package/dist/web/assets/chat-tab-Cfw__7vJ.js +0 -6
- package/dist/web/assets/code-editor-D8Pz69sx.js +0 -2
- package/dist/web/assets/dialog-BL9i7XEo.js +0 -5
- package/dist/web/assets/diff-viewer-CWS5n7ur.js +0 -4
- package/dist/web/assets/dist-0XHv8Vwc.js +0 -1
- package/dist/web/assets/dist-Ca3N8Xbh.js +0 -46
- package/dist/web/assets/git-graph-DwA62J8-.js +0 -1
- package/dist/web/assets/git-status-panel-DaB-zzSF.js +0 -1
- package/dist/web/assets/index-BYIXPY6U.css +0 -2
- package/dist/web/assets/index-DbTCLiox.js +0 -17
- package/dist/web/assets/project-list-Z4lhtp6P.js +0 -1
- package/dist/web/assets/refresh-cw-S6I91MHO.js +0 -1
- package/dist/web/assets/settings-tab-BW6MGcir.js +0 -1
- package/dist/web/assets/trash-2-CGlFXde_.js +0 -1
- package/dist/web/assets/x-C0Rw5Giw.js +0 -1
- /package/dist/web/assets/{api-client-DzH9zCD7.js → api-client-BCjah751.js} +0 -0
- /package/dist/web/assets/{columns-2-DsiY76NQ.js → columns-2-DFQ3yid7.js} +0 -0
- /package/dist/web/assets/{copy-D_Q54D-v.js → copy-B-kLwqzg.js} +0 -0
- /package/dist/web/assets/{external-link-C6Y-D528.js → external-link-Dim3NH6h.js} +0 -0
- /package/dist/web/assets/{marked.esm-Cv8mjgnt.js → marked.esm-DhBtkBa8.js} +0 -0
- /package/dist/web/assets/{utils-D6me7KDg.js → utils-B-_GCz7E.js} +0 -0
|
@@ -1,54 +1,32 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback, useRef, useMemo } from "react";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import { javascript } from "@codemirror/lang-javascript";
|
|
5
|
-
import { python } from "@codemirror/lang-python";
|
|
6
|
-
import { html } from "@codemirror/lang-html";
|
|
7
|
-
import { css } from "@codemirror/lang-css";
|
|
8
|
-
import { json } from "@codemirror/lang-json";
|
|
9
|
-
import { markdown } from "@codemirror/lang-markdown";
|
|
2
|
+
import Editor, { type OnMount } from "@monaco-editor/react";
|
|
3
|
+
import type * as MonacoType from "monaco-editor";
|
|
10
4
|
import { marked } from "marked";
|
|
11
|
-
import type { Extension } from "@codemirror/state";
|
|
12
|
-
import { EditorView } from "@codemirror/view";
|
|
13
5
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
14
6
|
import { useTabStore } from "@/stores/tab-store";
|
|
15
7
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
8
|
+
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
16
9
|
import { Loader2, FileWarning, ExternalLink, Code, Eye, WrapText } from "lucide-react";
|
|
17
10
|
|
|
18
11
|
/** Image extensions renderable inline */
|
|
19
12
|
const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico"]);
|
|
20
13
|
|
|
21
|
-
/** PDF extension */
|
|
22
|
-
const PDF_EXT = "pdf";
|
|
23
|
-
|
|
24
14
|
function getFileExt(filename: string): string {
|
|
25
15
|
return filename.split(".").pop()?.toLowerCase() ?? "";
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
function
|
|
18
|
+
function getMonacoLanguage(filename: string): string {
|
|
29
19
|
const ext = getFileExt(filename);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return html();
|
|
41
|
-
case "css":
|
|
42
|
-
case "scss":
|
|
43
|
-
return css();
|
|
44
|
-
case "json":
|
|
45
|
-
return json();
|
|
46
|
-
case "md":
|
|
47
|
-
case "mdx":
|
|
48
|
-
return markdown();
|
|
49
|
-
default:
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
20
|
+
const map: Record<string, string> = {
|
|
21
|
+
js: "javascript", jsx: "javascript",
|
|
22
|
+
ts: "typescript", tsx: "typescript",
|
|
23
|
+
py: "python", html: "html",
|
|
24
|
+
css: "css", scss: "scss",
|
|
25
|
+
json: "json", md: "markdown", mdx: "markdown",
|
|
26
|
+
yaml: "yaml", yml: "yaml",
|
|
27
|
+
sh: "shell", bash: "shell",
|
|
28
|
+
};
|
|
29
|
+
return map[ext] ?? "plaintext";
|
|
52
30
|
}
|
|
53
31
|
|
|
54
32
|
interface CodeEditorProps {
|
|
@@ -66,45 +44,22 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
66
44
|
const [unsaved, setUnsaved] = useState(false);
|
|
67
45
|
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
68
46
|
const latestContentRef = useRef<string>("");
|
|
47
|
+
const editorRef = useRef<MonacoType.editor.IStandaloneCodeEditor | null>(null);
|
|
69
48
|
const { tabs, updateTab } = useTabStore();
|
|
70
|
-
|
|
71
49
|
const { wordWrap, toggleWordWrap } = useSettingsStore();
|
|
72
|
-
|
|
73
|
-
const toggleWordWrapRef = useRef(toggleWordWrap);
|
|
74
|
-
useEffect(() => { toggleWordWrapRef.current = toggleWordWrap; }, [toggleWordWrap]);
|
|
75
|
-
|
|
76
|
-
// Alt+Z handler — intercepts at DOM level to prevent macOS "Ω" insertion
|
|
77
|
-
const wrapKeyHandler = useMemo(() => EditorView.domEventHandlers({
|
|
78
|
-
keydown(e) {
|
|
79
|
-
if (e.altKey && (e.key === "z" || e.key === "Z")) {
|
|
80
|
-
e.preventDefault();
|
|
81
|
-
toggleWordWrapRef.current();
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
85
|
-
},
|
|
86
|
-
beforeinput(e) {
|
|
87
|
-
if (e.data === "Ω") { e.preventDefault(); return true; }
|
|
88
|
-
return false;
|
|
89
|
-
},
|
|
90
|
-
}), []);
|
|
50
|
+
const monacoTheme = useMonacoTheme();
|
|
91
51
|
|
|
92
52
|
const ownTab = tabs.find((t) => t.id === tabId);
|
|
93
53
|
const ext = filePath ? getFileExt(filePath) : "";
|
|
94
54
|
const isImage = IMAGE_EXTS.has(ext);
|
|
95
|
-
const isPdf = ext ===
|
|
55
|
+
const isPdf = ext === "pdf";
|
|
96
56
|
const isMarkdown = ext === "md" || ext === "mdx";
|
|
97
|
-
/** "edit" | "preview" for markdown files — default to preview */
|
|
98
57
|
const [mdMode, setMdMode] = useState<"edit" | "preview">("preview");
|
|
99
58
|
|
|
100
59
|
// Load file content
|
|
101
60
|
useEffect(() => {
|
|
102
61
|
if (!filePath || !projectName) return;
|
|
103
|
-
|
|
104
|
-
if (isImage || isPdf) {
|
|
105
|
-
setLoading(false);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
62
|
+
if (isImage || isPdf) { setLoading(false); return; }
|
|
108
63
|
|
|
109
64
|
setLoading(true);
|
|
110
65
|
setError(null);
|
|
@@ -124,9 +79,7 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
124
79
|
setLoading(false);
|
|
125
80
|
});
|
|
126
81
|
|
|
127
|
-
return () => {
|
|
128
|
-
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
129
|
-
};
|
|
82
|
+
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current); };
|
|
130
83
|
}, [filePath, projectName, isImage, isPdf]);
|
|
131
84
|
|
|
132
85
|
// Update tab title unsaved indicator
|
|
@@ -134,39 +87,38 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
134
87
|
if (!ownTab) return;
|
|
135
88
|
const baseName = filePath?.split("/").pop() ?? "Untitled";
|
|
136
89
|
const newTitle = unsaved ? `${baseName} \u25CF` : baseName;
|
|
137
|
-
if (ownTab.title !== newTitle) {
|
|
138
|
-
updateTab(ownTab.id, { title: newTitle });
|
|
139
|
-
}
|
|
90
|
+
if (ownTab.title !== newTitle) updateTab(ownTab.id, { title: newTitle });
|
|
140
91
|
}, [unsaved]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
141
92
|
|
|
142
93
|
const saveFile = useCallback(
|
|
143
94
|
async (text: string) => {
|
|
144
95
|
if (!filePath || !projectName) return;
|
|
145
96
|
try {
|
|
146
|
-
await api.put(`${projectUrl(projectName)}/files/write`, {
|
|
147
|
-
path: filePath,
|
|
148
|
-
content: text,
|
|
149
|
-
});
|
|
97
|
+
await api.put(`${projectUrl(projectName)}/files/write`, { path: filePath, content: text });
|
|
150
98
|
setUnsaved(false);
|
|
151
|
-
} catch {
|
|
152
|
-
// Silent save failure — user sees unsaved indicator persists
|
|
153
|
-
}
|
|
99
|
+
} catch { /* Silent — unsaved indicator persists */ }
|
|
154
100
|
},
|
|
155
101
|
[filePath, projectName],
|
|
156
102
|
);
|
|
157
103
|
|
|
158
|
-
function handleChange(value: string) {
|
|
159
|
-
|
|
160
|
-
|
|
104
|
+
function handleChange(value: string | undefined) {
|
|
105
|
+
const val = value ?? "";
|
|
106
|
+
setContent(val);
|
|
107
|
+
latestContentRef.current = val;
|
|
161
108
|
setUnsaved(true);
|
|
162
|
-
|
|
163
|
-
// Debounced auto-save (1s)
|
|
164
109
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
|
165
|
-
saveTimerRef.current = setTimeout(() =>
|
|
166
|
-
saveFile(latestContentRef.current);
|
|
167
|
-
}, 1000);
|
|
110
|
+
saveTimerRef.current = setTimeout(() => saveFile(latestContentRef.current), 1000);
|
|
168
111
|
}
|
|
169
112
|
|
|
113
|
+
const handleEditorMount: OnMount = useCallback((editor, monaco) => {
|
|
114
|
+
editorRef.current = editor;
|
|
115
|
+
// Alt+Z → toggle word wrap
|
|
116
|
+
editor.addCommand(
|
|
117
|
+
monaco.KeyMod.Alt | monaco.KeyCode.KeyZ,
|
|
118
|
+
() => useSettingsStore.getState().toggleWordWrap(),
|
|
119
|
+
);
|
|
120
|
+
}, []);
|
|
121
|
+
|
|
170
122
|
if (!filePath || !projectName) {
|
|
171
123
|
return (
|
|
172
124
|
<div className="flex items-center justify-center h-full text-text-secondary text-sm">
|
|
@@ -186,23 +138,13 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
186
138
|
|
|
187
139
|
if (error) {
|
|
188
140
|
return (
|
|
189
|
-
<div className="flex items-center justify-center h-full text-error text-sm">
|
|
190
|
-
{error}
|
|
191
|
-
</div>
|
|
141
|
+
<div className="flex items-center justify-center h-full text-error text-sm">{error}</div>
|
|
192
142
|
);
|
|
193
143
|
}
|
|
194
144
|
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
return <ImagePreview filePath={filePath} projectName={projectName} />;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// --- PDF viewer ---
|
|
201
|
-
if (isPdf) {
|
|
202
|
-
return <PdfPreview filePath={filePath} projectName={projectName} />;
|
|
203
|
-
}
|
|
145
|
+
if (isImage) return <ImagePreview filePath={filePath} projectName={projectName} />;
|
|
146
|
+
if (isPdf) return <PdfPreview filePath={filePath} projectName={projectName} />;
|
|
204
147
|
|
|
205
|
-
// --- Binary file (base64 encoding) — cannot edit ---
|
|
206
148
|
if (encoding === "base64") {
|
|
207
149
|
return (
|
|
208
150
|
<div className="flex flex-col items-center justify-center h-full gap-3 text-text-secondary">
|
|
@@ -213,46 +155,24 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
213
155
|
);
|
|
214
156
|
}
|
|
215
157
|
|
|
216
|
-
// --- Text editor ---
|
|
217
|
-
const extensions: Extension[] = [];
|
|
218
|
-
const langExt = getLanguageExtension(filePath);
|
|
219
|
-
if (langExt) extensions.push(langExt);
|
|
220
|
-
if (wordWrap) extensions.push(EditorView.lineWrapping);
|
|
221
|
-
extensions.push(wrapKeyHandler);
|
|
222
|
-
|
|
223
158
|
const mdModeButtons = isMarkdown ? (
|
|
224
159
|
<>
|
|
225
|
-
<button
|
|
226
|
-
|
|
227
|
-
onClick={() => setMdMode("edit")}
|
|
228
|
-
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
|
229
|
-
mdMode === "edit" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"
|
|
230
|
-
}`}
|
|
160
|
+
<button type="button" onClick={() => setMdMode("edit")}
|
|
161
|
+
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "edit" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
231
162
|
>
|
|
232
|
-
<Code className="size-3" />
|
|
233
|
-
Edit
|
|
163
|
+
<Code className="size-3" /> Edit
|
|
234
164
|
</button>
|
|
235
|
-
<button
|
|
236
|
-
|
|
237
|
-
onClick={() => setMdMode("preview")}
|
|
238
|
-
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
|
239
|
-
mdMode === "preview" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"
|
|
240
|
-
}`}
|
|
165
|
+
<button type="button" onClick={() => setMdMode("preview")}
|
|
166
|
+
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "preview" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
241
167
|
>
|
|
242
|
-
<Eye className="size-3" />
|
|
243
|
-
Preview
|
|
168
|
+
<Eye className="size-3" /> Preview
|
|
244
169
|
</button>
|
|
245
170
|
</>
|
|
246
171
|
) : null;
|
|
247
172
|
|
|
248
173
|
const wrapBtn = (
|
|
249
|
-
<button
|
|
250
|
-
|
|
251
|
-
onClick={toggleWordWrap}
|
|
252
|
-
title="Toggle word wrap (Alt+Z)"
|
|
253
|
-
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${
|
|
254
|
-
wordWrap ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"
|
|
255
|
-
}`}
|
|
174
|
+
<button type="button" onClick={toggleWordWrap} title="Toggle word wrap (Alt+Z)"
|
|
175
|
+
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${wordWrap ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
256
176
|
>
|
|
257
177
|
<WrapText className="size-3" />
|
|
258
178
|
<span className="hidden sm:inline">Wrap</span>
|
|
@@ -261,39 +181,41 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
261
181
|
|
|
262
182
|
return (
|
|
263
183
|
<div className="flex flex-col h-full w-full overflow-hidden">
|
|
264
|
-
{/* Desktop toolbar
|
|
184
|
+
{/* Desktop toolbar */}
|
|
265
185
|
<div className="hidden md:flex items-center gap-1 px-2 py-1 border-b shrink-0 bg-background">
|
|
266
186
|
{mdModeButtons}
|
|
267
187
|
<div className="flex-1" />
|
|
268
188
|
{wrapBtn}
|
|
269
189
|
</div>
|
|
270
190
|
|
|
271
|
-
{/* Markdown preview mode */}
|
|
272
191
|
{isMarkdown && mdMode === "preview" ? (
|
|
273
192
|
<MarkdownPreview content={content ?? ""} />
|
|
274
193
|
) : (
|
|
275
194
|
<div className="flex-1 overflow-hidden">
|
|
276
|
-
<
|
|
195
|
+
<Editor
|
|
196
|
+
height="100%"
|
|
197
|
+
language={getMonacoLanguage(filePath)}
|
|
277
198
|
value={content ?? ""}
|
|
278
199
|
onChange={handleChange}
|
|
279
|
-
|
|
280
|
-
theme={
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
200
|
+
onMount={handleEditorMount}
|
|
201
|
+
theme={monacoTheme}
|
|
202
|
+
options={{
|
|
203
|
+
fontSize: 13,
|
|
204
|
+
fontFamily: "Menlo, Monaco, Consolas, monospace",
|
|
205
|
+
wordWrap: wordWrap ? "on" : "off",
|
|
206
|
+
minimap: { enabled: false },
|
|
207
|
+
scrollBeyondLastLine: false,
|
|
208
|
+
automaticLayout: true,
|
|
209
|
+
lineNumbers: "on",
|
|
210
|
+
folding: true,
|
|
211
|
+
bracketPairColorization: { enabled: true },
|
|
291
212
|
}}
|
|
213
|
+
loading={<Loader2 className="size-5 animate-spin text-text-subtle" />}
|
|
292
214
|
/>
|
|
293
215
|
</div>
|
|
294
216
|
)}
|
|
295
217
|
|
|
296
|
-
{/* Mobile toolbar
|
|
218
|
+
{/* Mobile toolbar */}
|
|
297
219
|
<div className="md:hidden flex items-center gap-1 px-2 py-1 border-t shrink-0 bg-background">
|
|
298
220
|
{mdModeButtons}
|
|
299
221
|
<div className="flex-1" />
|
|
@@ -303,14 +225,10 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
303
225
|
);
|
|
304
226
|
}
|
|
305
227
|
|
|
306
|
-
/** Rendered markdown preview using marked */
|
|
307
228
|
function MarkdownPreview({ content }: { content: string }) {
|
|
308
229
|
const html = useMemo(() => {
|
|
309
|
-
try {
|
|
310
|
-
|
|
311
|
-
} catch {
|
|
312
|
-
return content;
|
|
313
|
-
}
|
|
230
|
+
try { return marked.parse(content, { gfm: true, breaks: true }) as string; }
|
|
231
|
+
catch { return content; }
|
|
314
232
|
}, [content]);
|
|
315
233
|
|
|
316
234
|
return (
|
|
@@ -321,7 +239,6 @@ function MarkdownPreview({ content }: { content: string }) {
|
|
|
321
239
|
);
|
|
322
240
|
}
|
|
323
241
|
|
|
324
|
-
/** Inline image preview with auth */
|
|
325
242
|
function ImagePreview({ filePath, projectName }: { filePath: string; projectName: string }) {
|
|
326
243
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
327
244
|
const [error, setError] = useState(false);
|
|
@@ -331,15 +248,8 @@ function ImagePreview({ filePath, projectName }: { filePath: string; projectName
|
|
|
331
248
|
const url = `${projectUrl(projectName)}/files/raw?path=${encodeURIComponent(filePath)}`;
|
|
332
249
|
const token = getAuthToken();
|
|
333
250
|
fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
|
|
334
|
-
.then((r) => {
|
|
335
|
-
|
|
336
|
-
return r.blob();
|
|
337
|
-
})
|
|
338
|
-
.then((blob) => {
|
|
339
|
-
const objUrl = URL.createObjectURL(blob);
|
|
340
|
-
revoke = objUrl;
|
|
341
|
-
setBlobUrl(objUrl);
|
|
342
|
-
})
|
|
251
|
+
.then((r) => { if (!r.ok) throw new Error("Failed"); return r.blob(); })
|
|
252
|
+
.then((blob) => { const u = URL.createObjectURL(blob); revoke = u; setBlobUrl(u); })
|
|
343
253
|
.catch(() => setError(true));
|
|
344
254
|
return () => { if (revoke) URL.revokeObjectURL(revoke); };
|
|
345
255
|
}, [filePath, projectName]);
|
|
@@ -352,15 +262,9 @@ function ImagePreview({ filePath, projectName }: { filePath: string; projectName
|
|
|
352
262
|
</div>
|
|
353
263
|
);
|
|
354
264
|
}
|
|
355
|
-
|
|
356
265
|
if (!blobUrl) {
|
|
357
|
-
return
|
|
358
|
-
<div className="flex items-center justify-center h-full">
|
|
359
|
-
<Loader2 className="size-5 animate-spin text-text-subtle" />
|
|
360
|
-
</div>
|
|
361
|
-
);
|
|
266
|
+
return <div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>;
|
|
362
267
|
}
|
|
363
|
-
|
|
364
268
|
return (
|
|
365
269
|
<div className="flex items-center justify-center h-full p-4 bg-surface overflow-auto">
|
|
366
270
|
<img src={blobUrl} alt={filePath} className="max-w-full max-h-full object-contain" />
|
|
@@ -368,7 +272,6 @@ function ImagePreview({ filePath, projectName }: { filePath: string; projectName
|
|
|
368
272
|
);
|
|
369
273
|
}
|
|
370
274
|
|
|
371
|
-
/** PDF preview — fetches with auth, opens blob in iframe or new tab */
|
|
372
275
|
function PdfPreview({ filePath, projectName }: { filePath: string; projectName: string }) {
|
|
373
276
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
374
277
|
const [error, setError] = useState(false);
|
|
@@ -378,22 +281,16 @@ function PdfPreview({ filePath, projectName }: { filePath: string; projectName:
|
|
|
378
281
|
const url = `${projectUrl(projectName)}/files/raw?path=${encodeURIComponent(filePath)}`;
|
|
379
282
|
const token = getAuthToken();
|
|
380
283
|
fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
|
|
381
|
-
.then((r) => {
|
|
382
|
-
if (!r.ok) throw new Error("Failed");
|
|
383
|
-
return r.blob();
|
|
384
|
-
})
|
|
284
|
+
.then((r) => { if (!r.ok) throw new Error("Failed"); return r.blob(); })
|
|
385
285
|
.then((blob) => {
|
|
386
|
-
const
|
|
387
|
-
revoke =
|
|
388
|
-
setBlobUrl(objUrl);
|
|
286
|
+
const u = URL.createObjectURL(new Blob([blob], { type: "application/pdf" }));
|
|
287
|
+
revoke = u; setBlobUrl(u);
|
|
389
288
|
})
|
|
390
289
|
.catch(() => setError(true));
|
|
391
290
|
return () => { if (revoke) URL.revokeObjectURL(revoke); };
|
|
392
291
|
}, [filePath, projectName]);
|
|
393
292
|
|
|
394
|
-
const openInNewTab = useCallback(() => {
|
|
395
|
-
if (blobUrl) window.open(blobUrl, "_blank");
|
|
396
|
-
}, [blobUrl]);
|
|
293
|
+
const openInNewTab = useCallback(() => { if (blobUrl) window.open(blobUrl, "_blank"); }, [blobUrl]);
|
|
397
294
|
|
|
398
295
|
if (error) {
|
|
399
296
|
return (
|
|
@@ -403,34 +300,18 @@ function PdfPreview({ filePath, projectName }: { filePath: string; projectName:
|
|
|
403
300
|
</div>
|
|
404
301
|
);
|
|
405
302
|
}
|
|
406
|
-
|
|
407
303
|
if (!blobUrl) {
|
|
408
|
-
return
|
|
409
|
-
<div className="flex items-center justify-center h-full">
|
|
410
|
-
<Loader2 className="size-5 animate-spin text-text-subtle" />
|
|
411
|
-
</div>
|
|
412
|
-
);
|
|
304
|
+
return <div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>;
|
|
413
305
|
}
|
|
414
|
-
|
|
415
306
|
return (
|
|
416
307
|
<div className="flex flex-col h-full">
|
|
417
|
-
{/* Toolbar */}
|
|
418
308
|
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-background shrink-0">
|
|
419
309
|
<span className="text-xs text-text-secondary truncate">{filePath}</span>
|
|
420
|
-
<button
|
|
421
|
-
|
|
422
|
-
className="flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary transition-colors"
|
|
423
|
-
>
|
|
424
|
-
<ExternalLink className="size-3" />
|
|
425
|
-
Open in new tab
|
|
310
|
+
<button onClick={openInNewTab} className="flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary transition-colors">
|
|
311
|
+
<ExternalLink className="size-3" /> Open in new tab
|
|
426
312
|
</button>
|
|
427
313
|
</div>
|
|
428
|
-
{
|
|
429
|
-
<iframe
|
|
430
|
-
src={blobUrl}
|
|
431
|
-
title={filePath}
|
|
432
|
-
className="flex-1 w-full border-none"
|
|
433
|
-
/>
|
|
314
|
+
<iframe src={blobUrl} title={filePath} className="flex-1 w-full border-none" />
|
|
434
315
|
</div>
|
|
435
316
|
);
|
|
436
317
|
}
|