wiki-viewer 1.0.0 → 1.1.0
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/.next/standalone/README.md +365 -99
- package/.next/standalone/agents/bootstrap-prompt.md +1 -0
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +236 -0
- package/.next/standalone/bin/wiki-viewer.js +431 -33
- package/.next/standalone/docs/agent-collab-plan.md +1615 -0
- package/.next/standalone/docs/agent-collab-v2-plan.md +771 -0
- package/.next/standalone/package.json +19 -2
- package/.next/standalone/pnpm-lock.yaml +1368 -325
- package/.next/standalone/pnpm-workspace.yaml +7 -1
- package/.next/standalone/src/app/api/agent/activity/route.ts +58 -0
- package/.next/standalone/src/app/api/agent/admin/agents/[agentId]/revoke/route.ts +40 -0
- package/.next/standalone/src/app/api/agent/admin/agents/route.ts +33 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/approve/route.ts +83 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/[regId]/deny/route.ts +36 -0
- package/.next/standalone/src/app/api/agent/admin/registrations/route.ts +27 -0
- package/.next/standalone/src/app/api/agent/events/[...path]/route.ts +136 -0
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +202 -0
- package/.next/standalone/src/app/api/agent/internal/span/route.ts +117 -0
- package/.next/standalone/src/app/api/agent/register/[regId]/route.ts +50 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +110 -0
- package/.next/standalone/src/app/api/agent/settings/route.ts +30 -0
- package/.next/standalone/src/app/api/agent/settings/token/regenerate/route.ts +20 -0
- package/.next/standalone/src/app/api/agent/sidecar/[...path]/route.ts +49 -0
- package/.next/standalone/src/app/api/agents/install/route.ts +83 -0
- package/.next/standalone/src/app/api/agents/skill/route.ts +20 -0
- package/.next/standalone/src/app/api/agents/skill.tar.gz/route.ts +87 -0
- package/.next/standalone/src/app/api/auth/[...all]/route.ts +7 -0
- package/.next/standalone/src/app/api/owner/init/route.ts +14 -0
- package/.next/standalone/src/app/api/system/auth-settings/route.ts +85 -0
- package/.next/standalone/src/app/api/system/browse/route.ts +4 -0
- package/.next/standalone/src/app/api/system/clear-root/route.ts +8 -1
- package/.next/standalone/src/app/api/system/config/route.ts +5 -1
- package/.next/standalone/src/app/api/system/pins/route.ts +7 -0
- package/.next/standalone/src/app/api/system/reveal/route.ts +7 -0
- package/.next/standalone/src/app/api/system/root-status/route.ts +5 -1
- package/.next/standalone/src/app/api/system/set-root/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/app/route.ts +15 -0
- package/.next/standalone/src/app/api/wiki/content/route.ts +110 -11
- package/.next/standalone/src/app/api/wiki/folder/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/move/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/new-file/route.ts +55 -0
- package/.next/standalone/src/app/api/wiki/page/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/route.ts +10 -0
- package/.next/standalone/src/app/api/wiki/slugs/route.ts +5 -1
- package/.next/standalone/src/app/api/wiki/upload/route.ts +7 -0
- package/.next/standalone/src/app/api/wiki/watch/route.ts +6 -1
- package/.next/standalone/src/app/globals.css +70 -0
- package/.next/standalone/src/app/page.tsx +461 -217
- package/.next/standalone/src/app/signin/page.tsx +217 -0
- package/.next/standalone/src/components/ai-panel/activity-row.tsx +64 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +322 -0
- package/.next/standalone/src/components/ai-panel/token-section.tsx +312 -0
- package/.next/standalone/src/components/auth-settings-sheet.tsx +209 -0
- package/.next/standalone/src/components/editor/bubble-menu.tsx +41 -2
- package/.next/standalone/src/components/editor/comment-pip.tsx +56 -0
- package/.next/standalone/src/components/editor/comment-thread.tsx +252 -0
- package/.next/standalone/src/components/editor/editor.tsx +513 -10
- package/.next/standalone/src/components/editor/extensions/proof-span.ts +60 -0
- package/.next/standalone/src/components/editor/extensions.ts +2 -0
- package/.next/standalone/src/components/editor/proof-span-popover.tsx +161 -0
- package/.next/standalone/src/components/editor/suggest-edit-popover.tsx +228 -0
- package/.next/standalone/src/components/editor/suggestion-card.tsx +201 -0
- package/.next/standalone/src/components/view-width-toggle.tsx +47 -0
- package/.next/standalone/src/components/wiki/markdown-preview.tsx +120 -0
- package/.next/standalone/src/lib/auth/allowlist.ts +50 -0
- package/.next/standalone/src/lib/auth/client.ts +4 -0
- package/.next/standalone/src/lib/auth/csrf.ts +68 -0
- package/.next/standalone/src/lib/auth/server.ts +164 -0
- package/.next/standalone/src/lib/config.ts +4 -0
- package/.next/standalone/src/lib/markdown/parse-frontmatter.ts +20 -0
- package/.next/standalone/src/lib/markdown/sanitize-schema.ts +106 -0
- package/.next/standalone/src/lib/markdown/to-html.ts +46 -8
- package/.next/standalone/src/lib/markdown/to-markdown.ts +14 -0
- package/.next/standalone/src/lib/proof/activity-shared.ts +31 -0
- package/.next/standalone/src/lib/proof/activity.ts +74 -0
- package/.next/standalone/src/lib/proof/auth.ts +132 -0
- package/.next/standalone/src/lib/proof/block-refs.ts +133 -0
- package/.next/standalone/src/lib/proof/blocks.ts +73 -0
- package/.next/standalone/src/lib/proof/client-auth.ts +23 -0
- package/.next/standalone/src/lib/proof/event-bus.ts +47 -0
- package/.next/standalone/src/lib/proof/file-lock.ts +38 -0
- package/.next/standalone/src/lib/proof/glob.ts +51 -0
- package/.next/standalone/src/lib/proof/idempotency.ts +32 -0
- package/.next/standalone/src/lib/proof/mutex.ts +32 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +678 -0
- package/.next/standalone/src/lib/proof/owner-auth.ts +2 -0
- package/.next/standalone/src/lib/proof/pending.ts +97 -0
- package/.next/standalone/src/lib/proof/proof-span.ts +141 -0
- package/.next/standalone/src/lib/proof/rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/register-rate-limit.ts +53 -0
- package/.next/standalone/src/lib/proof/registry.ts +190 -0
- package/.next/standalone/src/lib/proof/sidecar.ts +66 -0
- package/.next/standalone/src/lib/proof/suggest-capture.ts +69 -0
- package/.next/standalone/src/lib/proof/types.ts +170 -0
- package/.next/standalone/src/lib/proof-config.ts +14 -0
- package/.next/standalone/src/middleware.ts +38 -0
- package/.next/standalone/src/stores/ai-panel-store.ts +58 -3
- package/.next/standalone/src/stores/editor-store.ts +115 -9
- package/.next/standalone/src/stores/proof-store.ts +123 -0
- package/.next/standalone/src/stores/view-width-store.ts +62 -0
- package/.next/standalone/src/tests/proof/activity-aggregator.test.ts +146 -0
- package/.next/standalone/src/tests/proof/agent-ownership.test.ts +140 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +57 -0
- package/.next/standalone/src/tests/proof/better-auth.test.ts +68 -0
- package/.next/standalone/src/tests/proof/block-refs.test.ts +108 -0
- package/.next/standalone/src/tests/proof/blocks.test.ts +92 -0
- package/.next/standalone/src/tests/proof/comments-ops.test.ts +177 -0
- package/.next/standalone/src/tests/proof/editor-roundtrip.test.ts +85 -0
- package/.next/standalone/src/tests/proof/external-edit.test.ts +58 -0
- package/.next/standalone/src/tests/proof/file-lock.test.ts +80 -0
- package/.next/standalone/src/tests/proof/glob.test.ts +82 -0
- package/.next/standalone/src/tests/proof/helpers/auth-session.ts +27 -0
- package/.next/standalone/src/tests/proof/markdown-to-html.test.ts +46 -0
- package/.next/standalone/src/tests/proof/ops-applier.test.ts +368 -0
- package/.next/standalone/src/tests/proof/owner-init.test.ts +2 -0
- package/.next/standalone/src/tests/proof/parse-frontmatter.test.ts +60 -0
- package/.next/standalone/src/tests/proof/proof-span.test.ts +98 -0
- package/.next/standalone/src/tests/proof/rate-limit.test.ts +54 -0
- package/.next/standalone/src/tests/proof/registration-flow.test.ts +330 -0
- package/.next/standalone/src/tests/proof/registry.test.ts +169 -0
- package/.next/standalone/src/tests/proof/routes.test.ts +516 -0
- package/.next/standalone/src/tests/proof/sidecar-trim.test.ts +58 -0
- package/.next/standalone/src/tests/proof/suggestion-ops.test.ts +280 -0
- package/.next/standalone/src/tests/proof/wiki-content-put.test.ts +140 -0
- package/.next/standalone/src/tests/proof/wiki-routes-auth.test.ts +208 -0
- package/README.md +365 -99
- package/bin/wiki-viewer.js +431 -33
- package/package.json +19 -2
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
AlertCircle,
|
|
5
|
+
Bot,
|
|
5
6
|
Check,
|
|
6
7
|
ChevronDown,
|
|
7
8
|
ChevronRight,
|
|
8
9
|
File,
|
|
10
|
+
FilePlus,
|
|
9
11
|
FileText,
|
|
10
12
|
Folder,
|
|
11
13
|
FolderOpen,
|
|
@@ -17,15 +19,16 @@ import {
|
|
|
17
19
|
PanelLeftClose,
|
|
18
20
|
PanelLeftOpen,
|
|
19
21
|
Pencil,
|
|
22
|
+
Plus,
|
|
20
23
|
RefreshCw,
|
|
24
|
+
Settings,
|
|
21
25
|
Terminal,
|
|
22
26
|
Trash2,
|
|
23
27
|
Upload,
|
|
24
28
|
X,
|
|
25
29
|
} from "lucide-react";
|
|
26
30
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
27
|
-
|
|
28
|
-
import remarkGfm from "remark-gfm";
|
|
31
|
+
|
|
29
32
|
import { ConfirmDialog } from "@/components/confirm-dialog";
|
|
30
33
|
import { CsvViewer } from "@/components/editor/csv-viewer";
|
|
31
34
|
import { KBEditor } from "@/components/editor/editor";
|
|
@@ -42,15 +45,27 @@ import { SourceViewer } from "@/components/editor/source-viewer";
|
|
|
42
45
|
import { WebsiteViewer } from "@/components/editor/website-viewer";
|
|
43
46
|
import { NodeAppViewer } from "@/components/editor/node-app-viewer";
|
|
44
47
|
import { DirPicker } from "@/components/dir-picker";
|
|
48
|
+
import {
|
|
49
|
+
DropdownMenu,
|
|
50
|
+
DropdownMenuContent,
|
|
51
|
+
DropdownMenuItem,
|
|
52
|
+
DropdownMenuTrigger,
|
|
53
|
+
} from "@/components/ui/dropdown-menu";
|
|
45
54
|
import { ThemeToggle } from "@/components/theme-toggle";
|
|
55
|
+
import { AuthSettingsSheet } from "@/components/auth-settings-sheet";
|
|
46
56
|
import { Button } from "@/components/ui/button";
|
|
47
57
|
import { Card } from "@/components/ui/card";
|
|
48
58
|
import { FrontmatterHeader } from "@/components/wiki/frontmatter-header";
|
|
59
|
+
import { MarkdownPreview } from "@/components/wiki/markdown-preview";
|
|
49
60
|
import { parseFrontmatter } from "@/lib/markdown/parse-frontmatter";
|
|
50
|
-
|
|
61
|
+
|
|
51
62
|
import { showError } from "@/lib/toast";
|
|
52
63
|
import { cn } from "@/lib/utils";
|
|
64
|
+
import { AIPanel } from "@/components/ai-panel/ai-panel";
|
|
65
|
+
import { useAIPanelStore } from "@/stores/ai-panel-store";
|
|
53
66
|
import { useEditorStore } from "@/stores/editor-store";
|
|
67
|
+
import { useViewWidthStore, VIEW_WIDTH_CLASS } from "@/stores/view-width-store";
|
|
68
|
+
import { ViewWidthToggle } from "@/components/view-width-toggle";
|
|
54
69
|
import { useWikiSlugsStore } from "@/stores/wiki-slugs-store";
|
|
55
70
|
|
|
56
71
|
interface TreeNode {
|
|
@@ -128,9 +143,21 @@ function viewerKindFor(
|
|
|
128
143
|
return "fallback";
|
|
129
144
|
}
|
|
130
145
|
|
|
146
|
+
const TEXT_EDITABLE_EXTS = new Set([
|
|
147
|
+
"txt", "md", "markdown", "json", "yaml", "yml", "toml", "csv", "tsv",
|
|
148
|
+
"xml", "html", "css", "js", "ts", "tsx", "jsx", "sh", "bash", "zsh",
|
|
149
|
+
"rb", "py", "go", "rs", "java", "c", "cpp", "h", "php", "swift", "kt",
|
|
150
|
+
"lua", "sql", "scss", "mmd", "mermaid", "ini", "env", "log", "conf",
|
|
151
|
+
]);
|
|
152
|
+
|
|
131
153
|
function isText(name: string) {
|
|
132
154
|
const kind = viewerKindFor(name, "file");
|
|
133
|
-
|
|
155
|
+
if (kind === "editor" || kind === "text") return true;
|
|
156
|
+
return TEXT_EDITABLE_EXTS.has(ext(name));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isMarkdown(name: string) {
|
|
160
|
+
return ["md", "markdown"].includes(ext(name));
|
|
134
161
|
}
|
|
135
162
|
function isImage(name: string) {
|
|
136
163
|
return viewerKindFor(name, "file") === "image";
|
|
@@ -204,6 +231,15 @@ export default function Page() {
|
|
|
204
231
|
|
|
205
232
|
const editorCurrentPath = useEditorStore((s) => s.currentPath);
|
|
206
233
|
|
|
234
|
+
// Path captured from the URL at first render, before any effect can clear it.
|
|
235
|
+
// Restore reads from this ref (never the live URL) so URL sync can't break it.
|
|
236
|
+
const initialUrlPathRef = useRef<string | null>(
|
|
237
|
+
typeof window !== "undefined"
|
|
238
|
+
? new URLSearchParams(window.location.search).get("path")
|
|
239
|
+
: null,
|
|
240
|
+
);
|
|
241
|
+
const didRestoreRef = useRef(false);
|
|
242
|
+
|
|
207
243
|
const [roots, setRoots] = useState<TreeNode[]>([]);
|
|
208
244
|
const [rootLoaded, setRootLoaded] = useState(false);
|
|
209
245
|
const [rootLoading, setRootLoading] = useState(false);
|
|
@@ -219,6 +255,10 @@ export default function Page() {
|
|
|
219
255
|
const [newFolderName, setNewFolderName] = useState("");
|
|
220
256
|
const [folderError, setFolderError] = useState<string | null>(null);
|
|
221
257
|
|
|
258
|
+
const [newFileParent, setNewFileParent] = useState<string | null>(null);
|
|
259
|
+
const [newFileName, setNewFileName] = useState("");
|
|
260
|
+
const [fileCreateError, setFileCreateError] = useState<string | null>(null);
|
|
261
|
+
|
|
222
262
|
const [deletingPath, setDeletingPath] = useState<string | null>(null);
|
|
223
263
|
const [deletingIsDir, setDeletingIsDir] = useState(false);
|
|
224
264
|
|
|
@@ -230,6 +270,7 @@ export default function Page() {
|
|
|
230
270
|
const [appFullscreen, setAppFullscreen] = useState(false);
|
|
231
271
|
const [appKey, setAppKey] = useState(0);
|
|
232
272
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
273
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
233
274
|
const [fileContent, setFileContent] = useState<string | null>(null);
|
|
234
275
|
const [fileLoading, setFileLoading] = useState(false);
|
|
235
276
|
const [editing, setEditing] = useState(false);
|
|
@@ -263,10 +304,58 @@ export default function Page() {
|
|
|
263
304
|
});
|
|
264
305
|
}, [rootLoaded]);
|
|
265
306
|
|
|
307
|
+
// Expand every ancestor folder of `p` so the file is visible in the tree.
|
|
308
|
+
// Best-effort: requires the root nodes to already be loaded.
|
|
309
|
+
const revealPath = useCallback(async (p: string) => {
|
|
310
|
+
const parts = p.split("/");
|
|
311
|
+
if (parts.length <= 1) return;
|
|
312
|
+
|
|
313
|
+
// Fetch every ancestor's children first. We must NOT apply them with one
|
|
314
|
+
// setRoots per level: those updaters run against the same stale `prev`
|
|
315
|
+
// snapshot, so a nested prefix isn't in the tree yet and updateNodes
|
|
316
|
+
// silently no-ops (only the top level ever matched). Collect all levels,
|
|
317
|
+
// then splice them in a single pass so each parent's children exist before
|
|
318
|
+
// the next level is inserted.
|
|
319
|
+
const levels: { prefix: string; children: TreeNode[] }[] = [];
|
|
320
|
+
let prefix = "";
|
|
321
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
322
|
+
prefix = prefix ? `${prefix}/${parts[i]}` : parts[i];
|
|
323
|
+
levels.push({ prefix, children: await fetchDir(prefix) });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
setRoots((prev) => {
|
|
327
|
+
let next = prev;
|
|
328
|
+
for (const { prefix: pfx, children } of levels) {
|
|
329
|
+
next = updateNodes(next, pfx, (n) => ({
|
|
330
|
+
...n,
|
|
331
|
+
children,
|
|
332
|
+
expanded: true,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
return next;
|
|
336
|
+
});
|
|
337
|
+
}, []);
|
|
338
|
+
|
|
266
339
|
const reloadDir = useCallback(async (dir: string) => {
|
|
267
340
|
const fresh = await fetchDir(dir);
|
|
268
341
|
if (dir === "") {
|
|
269
|
-
|
|
342
|
+
// Merge: keep expanded state + loaded children for nodes that still
|
|
343
|
+
// exist. A blind setRoots(fresh) would collapse the tree and wipe any
|
|
344
|
+
// expansion done by revealPath on reload (watcher fires this often).
|
|
345
|
+
setRoots((prev) => {
|
|
346
|
+
const prevByPath = new Map(prev.map((n) => [n.path, n]));
|
|
347
|
+
return fresh.map((n) => {
|
|
348
|
+
const old = prevByPath.get(n.path);
|
|
349
|
+
if (old && (old.type === "dir" || old.type === "app")) {
|
|
350
|
+
return {
|
|
351
|
+
...n,
|
|
352
|
+
expanded: old.expanded,
|
|
353
|
+
children: old.children,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return n;
|
|
357
|
+
});
|
|
358
|
+
});
|
|
270
359
|
} else {
|
|
271
360
|
setRoots((prev) =>
|
|
272
361
|
updateNodes(prev, dir, (n) => ({
|
|
@@ -313,7 +402,7 @@ export default function Page() {
|
|
|
313
402
|
const refreshViewer = useCallback(async () => {
|
|
314
403
|
if (!openFile) return;
|
|
315
404
|
const kind = viewerKindFor(openFile.name, openFile.nodeType);
|
|
316
|
-
if (!["editor", "text"].includes(kind)) return;
|
|
405
|
+
if (!["editor", "text"].includes(kind) && !isText(openFile.name)) return;
|
|
317
406
|
setFileLoading(true);
|
|
318
407
|
try {
|
|
319
408
|
const res = await fetch(
|
|
@@ -448,7 +537,7 @@ export default function Page() {
|
|
|
448
537
|
setSaveError(null);
|
|
449
538
|
setFileContent(null);
|
|
450
539
|
const kind = viewerKindFor(node.name, node.type);
|
|
451
|
-
if (!["editor", "text"].includes(kind)) return;
|
|
540
|
+
if (!["editor", "text"].includes(kind) && !isText(node.name)) return;
|
|
452
541
|
setFileLoading(true);
|
|
453
542
|
try {
|
|
454
543
|
const res = await fetch(
|
|
@@ -476,6 +565,46 @@ export default function Page() {
|
|
|
476
565
|
} as TreeNode);
|
|
477
566
|
}, [editorCurrentPath]);
|
|
478
567
|
|
|
568
|
+
// Persist the open file to the URL (?path=) so reloads restore it.
|
|
569
|
+
// replaceState avoids polluting history on every file switch.
|
|
570
|
+
useEffect(() => {
|
|
571
|
+
if (typeof window === "undefined") return;
|
|
572
|
+
const url = new URL(window.location.href);
|
|
573
|
+
if (openFile) url.searchParams.set("path", openFile.path);
|
|
574
|
+
else url.searchParams.delete("path");
|
|
575
|
+
window.history.replaceState(null, "", url.toString());
|
|
576
|
+
}, [openFile]);
|
|
577
|
+
|
|
578
|
+
// Restore the open file from the URL once the root tree is loaded.
|
|
579
|
+
// Resolves node type from the parent dir listing (file vs app vs node-app).
|
|
580
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: openViewer is a hoisted stable fn; restore runs once
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
if (didRestoreRef.current) return;
|
|
583
|
+
if (!rootLoaded) return;
|
|
584
|
+
const target = initialUrlPathRef.current;
|
|
585
|
+
if (!target) {
|
|
586
|
+
didRestoreRef.current = true;
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
didRestoreRef.current = true;
|
|
590
|
+
|
|
591
|
+
void (async () => {
|
|
592
|
+
const parts = target.split("/");
|
|
593
|
+
const name = parts[parts.length - 1];
|
|
594
|
+
const parentDir = parts.slice(0, -1).join("/");
|
|
595
|
+
const siblings = await fetchDir(parentDir);
|
|
596
|
+
const match = siblings.find((s) => s.path === target);
|
|
597
|
+
if (!match) return; // file no longer exists; leave home view
|
|
598
|
+
await revealPath(target);
|
|
599
|
+
void openViewer({
|
|
600
|
+
path: target,
|
|
601
|
+
name,
|
|
602
|
+
type: match.type,
|
|
603
|
+
modifiedAt: match.modifiedAt,
|
|
604
|
+
} as TreeNode);
|
|
605
|
+
})();
|
|
606
|
+
}, [rootLoaded, revealPath]);
|
|
607
|
+
|
|
479
608
|
const handleChangeDir = async () => {
|
|
480
609
|
await fetch("/api/system/clear-root", { method: "POST" });
|
|
481
610
|
setOpenFile(null);
|
|
@@ -570,6 +699,39 @@ export default function Page() {
|
|
|
570
699
|
}
|
|
571
700
|
}
|
|
572
701
|
|
|
702
|
+
async function handleCreateFile() {
|
|
703
|
+
const raw = newFileName.trim();
|
|
704
|
+
if (!raw || newFileParent === null) return;
|
|
705
|
+
setFileCreateError(null);
|
|
706
|
+
const name = raw.includes(".") ? raw : `${raw}.md`;
|
|
707
|
+
const rel = newFileParent ? `${newFileParent}/${name}` : name;
|
|
708
|
+
const res = await fetch("/api/wiki/new-file", {
|
|
709
|
+
method: "POST",
|
|
710
|
+
headers: { "Content-Type": "application/json" },
|
|
711
|
+
body: JSON.stringify({ path: rel }),
|
|
712
|
+
});
|
|
713
|
+
if (res.ok) {
|
|
714
|
+
const parent = newFileParent;
|
|
715
|
+
setNewFileParent(null);
|
|
716
|
+
setNewFileName("");
|
|
717
|
+
await reloadDir(parent);
|
|
718
|
+
if (parent !== "") {
|
|
719
|
+
setRoots((prev) =>
|
|
720
|
+
updateNodes(prev, parent, (n) => ({ ...n, expanded: true })),
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
void openViewer({
|
|
724
|
+
path: rel,
|
|
725
|
+
name,
|
|
726
|
+
type: "file",
|
|
727
|
+
modifiedAt: new Date().toISOString(),
|
|
728
|
+
} as TreeNode);
|
|
729
|
+
} else {
|
|
730
|
+
const e: { error?: string } = await res.json();
|
|
731
|
+
setFileCreateError(e.error ?? "Failed");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
573
735
|
async function handleDelete() {
|
|
574
736
|
if (!deletingPath) return;
|
|
575
737
|
await fetch("/api/wiki", {
|
|
@@ -652,6 +814,17 @@ export default function Page() {
|
|
|
652
814
|
? viewerKindFor(openFile.name, openFile.nodeType)
|
|
653
815
|
: null;
|
|
654
816
|
|
|
817
|
+
const viewWidth = useViewWidthStore((s) => s.width);
|
|
818
|
+
// Width toggle only meaningful for text-flow viewers (long prose lines).
|
|
819
|
+
const widthAwareViewer =
|
|
820
|
+
openFileViewerKind === null ||
|
|
821
|
+
openFileViewerKind === "editor" ||
|
|
822
|
+
openFileViewerKind === "text" ||
|
|
823
|
+
openFileViewerKind === "source" ||
|
|
824
|
+
openFileViewerKind === "notebook" ||
|
|
825
|
+
openFileViewerKind === "fallback";
|
|
826
|
+
const contentWidthClass = widthAwareViewer ? VIEW_WIDTH_CLASS[viewWidth] : "";
|
|
827
|
+
|
|
655
828
|
function renderNodes(nodes: TreeNode[], depth = 0): React.ReactNode {
|
|
656
829
|
return nodes.map((node) => (
|
|
657
830
|
<div key={node.path}>
|
|
@@ -735,6 +908,20 @@ export default function Page() {
|
|
|
735
908
|
>
|
|
736
909
|
{node.type === "dir" && (
|
|
737
910
|
<>
|
|
911
|
+
<Button
|
|
912
|
+
size="sm"
|
|
913
|
+
variant="ghost"
|
|
914
|
+
className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground"
|
|
915
|
+
title="New file here (default .md)"
|
|
916
|
+
onClick={async () => {
|
|
917
|
+
if (!node.expanded) await toggleFolder(node);
|
|
918
|
+
setNewFileParent(node.path);
|
|
919
|
+
setNewFileName("");
|
|
920
|
+
setFileCreateError(null);
|
|
921
|
+
}}
|
|
922
|
+
>
|
|
923
|
+
<FilePlus className="h-3 w-3" />
|
|
924
|
+
</Button>
|
|
738
925
|
<Button
|
|
739
926
|
size="sm"
|
|
740
927
|
variant="ghost"
|
|
@@ -819,6 +1006,52 @@ export default function Page() {
|
|
|
819
1006
|
</div>
|
|
820
1007
|
)}
|
|
821
1008
|
|
|
1009
|
+
{newFileParent === node.path && node.type === "dir" && (
|
|
1010
|
+
<div
|
|
1011
|
+
className="flex items-center gap-1.5 px-2 py-1"
|
|
1012
|
+
style={{ paddingLeft: `${(depth + 1) * 14 + 8}px` }}
|
|
1013
|
+
>
|
|
1014
|
+
<span className="w-3.5 shrink-0" />
|
|
1015
|
+
<FileText className="h-4 w-4 shrink-0 text-accent" />
|
|
1016
|
+
<input
|
|
1017
|
+
autoFocus
|
|
1018
|
+
className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
|
|
1019
|
+
placeholder="filename (default .md)"
|
|
1020
|
+
value={newFileName}
|
|
1021
|
+
onChange={(e) => setNewFileName(e.target.value)}
|
|
1022
|
+
onKeyDown={(e) => {
|
|
1023
|
+
if (e.key === "Enter") handleCreateFile();
|
|
1024
|
+
if (e.key === "Escape") {
|
|
1025
|
+
setNewFileParent(null);
|
|
1026
|
+
setNewFileName("");
|
|
1027
|
+
}
|
|
1028
|
+
}}
|
|
1029
|
+
/>
|
|
1030
|
+
{fileCreateError && (
|
|
1031
|
+
<span className="text-xs text-destructive">{fileCreateError}</span>
|
|
1032
|
+
)}
|
|
1033
|
+
<Button
|
|
1034
|
+
size="sm"
|
|
1035
|
+
variant="ghost"
|
|
1036
|
+
className="h-6 w-6 p-0"
|
|
1037
|
+
onClick={handleCreateFile}
|
|
1038
|
+
>
|
|
1039
|
+
<Check className="h-3 w-3" />
|
|
1040
|
+
</Button>
|
|
1041
|
+
<Button
|
|
1042
|
+
size="sm"
|
|
1043
|
+
variant="ghost"
|
|
1044
|
+
className="h-6 w-6 p-0"
|
|
1045
|
+
onClick={() => {
|
|
1046
|
+
setNewFileParent(null);
|
|
1047
|
+
setNewFileName("");
|
|
1048
|
+
}}
|
|
1049
|
+
>
|
|
1050
|
+
<X className="h-3 w-3" />
|
|
1051
|
+
</Button>
|
|
1052
|
+
</div>
|
|
1053
|
+
)}
|
|
1054
|
+
|
|
822
1055
|
{(node.type === "dir" || node.type === "app") &&
|
|
823
1056
|
node.expanded &&
|
|
824
1057
|
node.children &&
|
|
@@ -859,12 +1092,76 @@ export default function Page() {
|
|
|
859
1092
|
{/* Tree sidebar */}
|
|
860
1093
|
{!sidebarCollapsed && (
|
|
861
1094
|
<Card className="flex flex-col w-72 shrink-0 overflow-hidden rounded-none border-r border-l-0 border-t-0 border-b-0">
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
<
|
|
1095
|
+
{/* Row 1: brand + collapse */}
|
|
1096
|
+
<div className="flex items-center justify-between gap-2 px-3 py-2 border-b bg-muted shrink-0">
|
|
1097
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
1098
|
+
<img src="/logo.svg" alt="Wiki Viewer" className="h-5 w-5 shrink-0" />
|
|
1099
|
+
<span className="truncate text-xs font-semibold tracking-tight">
|
|
1100
|
+
Wiki Viewer
|
|
1101
|
+
</span>
|
|
866
1102
|
</div>
|
|
867
|
-
<
|
|
1103
|
+
<Button
|
|
1104
|
+
size="sm"
|
|
1105
|
+
variant="ghost"
|
|
1106
|
+
className="h-7 w-7 p-0 shrink-0"
|
|
1107
|
+
title="Collapse sidebar"
|
|
1108
|
+
onClick={() => setSidebarCollapsed(true)}
|
|
1109
|
+
>
|
|
1110
|
+
<PanelLeftClose className="h-3.5 w-3.5" />
|
|
1111
|
+
</Button>
|
|
1112
|
+
</div>
|
|
1113
|
+
|
|
1114
|
+
{/* Row 2: actions toolbar */}
|
|
1115
|
+
<div className="flex items-center justify-between gap-1 px-3 py-1.5 border-b bg-muted/50 shrink-0">
|
|
1116
|
+
<DropdownMenu>
|
|
1117
|
+
<DropdownMenuTrigger asChild>
|
|
1118
|
+
<Button
|
|
1119
|
+
size="sm"
|
|
1120
|
+
variant="ghost"
|
|
1121
|
+
className="h-7 gap-1 px-2 text-xs"
|
|
1122
|
+
disabled={uploading}
|
|
1123
|
+
>
|
|
1124
|
+
{uploading ? (
|
|
1125
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
1126
|
+
) : (
|
|
1127
|
+
<Plus className="h-3.5 w-3.5" />
|
|
1128
|
+
)}
|
|
1129
|
+
New
|
|
1130
|
+
<ChevronDown className="h-3 w-3 opacity-60" />
|
|
1131
|
+
</Button>
|
|
1132
|
+
</DropdownMenuTrigger>
|
|
1133
|
+
<DropdownMenuContent align="start" className="w-44">
|
|
1134
|
+
<DropdownMenuItem
|
|
1135
|
+
onClick={() => {
|
|
1136
|
+
setNewFileParent("");
|
|
1137
|
+
setNewFileName("");
|
|
1138
|
+
setFileCreateError(null);
|
|
1139
|
+
}}
|
|
1140
|
+
>
|
|
1141
|
+
<FilePlus className="mr-2 h-3.5 w-3.5" />
|
|
1142
|
+
New file
|
|
1143
|
+
</DropdownMenuItem>
|
|
1144
|
+
<DropdownMenuItem
|
|
1145
|
+
onClick={() => {
|
|
1146
|
+
setNewFolderParent("");
|
|
1147
|
+
setNewFolderName("");
|
|
1148
|
+
setFolderError(null);
|
|
1149
|
+
}}
|
|
1150
|
+
>
|
|
1151
|
+
<FolderPlus className="mr-2 h-3.5 w-3.5" />
|
|
1152
|
+
New folder
|
|
1153
|
+
</DropdownMenuItem>
|
|
1154
|
+
<DropdownMenuItem
|
|
1155
|
+
onClick={() => triggerUpload("")}
|
|
1156
|
+
disabled={uploading}
|
|
1157
|
+
>
|
|
1158
|
+
<Upload className="mr-2 h-3.5 w-3.5" />
|
|
1159
|
+
Upload
|
|
1160
|
+
</DropdownMenuItem>
|
|
1161
|
+
</DropdownMenuContent>
|
|
1162
|
+
</DropdownMenu>
|
|
1163
|
+
|
|
1164
|
+
<div className="flex items-center gap-0.5">
|
|
868
1165
|
<Button
|
|
869
1166
|
size="sm"
|
|
870
1167
|
variant="ghost"
|
|
@@ -879,42 +1176,24 @@ export default function Page() {
|
|
|
879
1176
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
880
1177
|
)}
|
|
881
1178
|
</Button>
|
|
1179
|
+
<ThemeToggle />
|
|
882
1180
|
<Button
|
|
883
1181
|
size="sm"
|
|
884
1182
|
variant="ghost"
|
|
885
1183
|
className="h-7 w-7 p-0"
|
|
886
|
-
title="
|
|
887
|
-
onClick={() =>
|
|
888
|
-
setNewFolderParent("");
|
|
889
|
-
setNewFolderName("");
|
|
890
|
-
setFolderError(null);
|
|
891
|
-
}}
|
|
892
|
-
>
|
|
893
|
-
<FolderPlus className="h-3.5 w-3.5" />
|
|
894
|
-
</Button>
|
|
895
|
-
<Button
|
|
896
|
-
size="sm"
|
|
897
|
-
variant="ghost"
|
|
898
|
-
className="h-7 w-7 p-0"
|
|
899
|
-
title="Upload to root"
|
|
900
|
-
onClick={() => triggerUpload("")}
|
|
901
|
-
disabled={uploading}
|
|
1184
|
+
title="AI Agent panel"
|
|
1185
|
+
onClick={() => useAIPanelStore.getState().toggle()}
|
|
902
1186
|
>
|
|
903
|
-
|
|
904
|
-
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
|
905
|
-
) : (
|
|
906
|
-
<Upload className="h-3.5 w-3.5" />
|
|
907
|
-
)}
|
|
1187
|
+
<Bot className="h-3.5 w-3.5" />
|
|
908
1188
|
</Button>
|
|
909
|
-
<ThemeToggle />
|
|
910
1189
|
<Button
|
|
911
1190
|
size="sm"
|
|
912
1191
|
variant="ghost"
|
|
913
1192
|
className="h-7 w-7 p-0"
|
|
914
|
-
title="
|
|
915
|
-
onClick={() =>
|
|
1193
|
+
title="Settings"
|
|
1194
|
+
onClick={() => setSettingsOpen(true)}
|
|
916
1195
|
>
|
|
917
|
-
<
|
|
1196
|
+
<Settings className="h-3.5 w-3.5" />
|
|
918
1197
|
</Button>
|
|
919
1198
|
</div>
|
|
920
1199
|
</div>
|
|
@@ -967,6 +1246,48 @@ export default function Page() {
|
|
|
967
1246
|
</div>
|
|
968
1247
|
)}
|
|
969
1248
|
|
|
1249
|
+
{newFileParent === "" && (
|
|
1250
|
+
<div className="flex items-center gap-1.5 px-2 py-1 border-b shrink-0">
|
|
1251
|
+
<FileText className="h-4 w-4 shrink-0 text-accent" />
|
|
1252
|
+
<input
|
|
1253
|
+
autoFocus
|
|
1254
|
+
className="flex-1 bg-transparent text-sm outline-none border-b border-border min-w-0"
|
|
1255
|
+
placeholder="filename (default .md)"
|
|
1256
|
+
value={newFileName}
|
|
1257
|
+
onChange={(e) => setNewFileName(e.target.value)}
|
|
1258
|
+
onKeyDown={(e) => {
|
|
1259
|
+
if (e.key === "Enter") handleCreateFile();
|
|
1260
|
+
if (e.key === "Escape") {
|
|
1261
|
+
setNewFileParent(null);
|
|
1262
|
+
setNewFileName("");
|
|
1263
|
+
}
|
|
1264
|
+
}}
|
|
1265
|
+
/>
|
|
1266
|
+
{fileCreateError && (
|
|
1267
|
+
<span className="text-xs text-destructive">{fileCreateError}</span>
|
|
1268
|
+
)}
|
|
1269
|
+
<Button
|
|
1270
|
+
size="sm"
|
|
1271
|
+
variant="ghost"
|
|
1272
|
+
className="h-6 w-6 p-0"
|
|
1273
|
+
onClick={handleCreateFile}
|
|
1274
|
+
>
|
|
1275
|
+
<Check className="h-3 w-3" />
|
|
1276
|
+
</Button>
|
|
1277
|
+
<Button
|
|
1278
|
+
size="sm"
|
|
1279
|
+
variant="ghost"
|
|
1280
|
+
className="h-6 w-6 p-0"
|
|
1281
|
+
onClick={() => {
|
|
1282
|
+
setNewFileParent(null);
|
|
1283
|
+
setNewFileName("");
|
|
1284
|
+
}}
|
|
1285
|
+
>
|
|
1286
|
+
<X className="h-3 w-3" />
|
|
1287
|
+
</Button>
|
|
1288
|
+
</div>
|
|
1289
|
+
)}
|
|
1290
|
+
|
|
970
1291
|
<div
|
|
971
1292
|
className={cn(
|
|
972
1293
|
"flex-1 overflow-auto py-1",
|
|
@@ -1077,6 +1398,23 @@ export default function Page() {
|
|
|
1077
1398
|
</span>
|
|
1078
1399
|
</div>
|
|
1079
1400
|
<div className="flex items-center gap-1 shrink-0">
|
|
1401
|
+
{openFileViewerKind === "html" &&
|
|
1402
|
+
!editing &&
|
|
1403
|
+
fileContent !== null && (
|
|
1404
|
+
<Button
|
|
1405
|
+
size="sm"
|
|
1406
|
+
variant="ghost"
|
|
1407
|
+
className="h-7 w-7 p-0"
|
|
1408
|
+
title="Edit source"
|
|
1409
|
+
onClick={() => {
|
|
1410
|
+
setEditing(true);
|
|
1411
|
+
setEditContent(fileContent);
|
|
1412
|
+
setSaveError(null);
|
|
1413
|
+
}}
|
|
1414
|
+
>
|
|
1415
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
1416
|
+
</Button>
|
|
1417
|
+
)}
|
|
1080
1418
|
<Button
|
|
1081
1419
|
size="sm"
|
|
1082
1420
|
variant="ghost"
|
|
@@ -1099,18 +1437,60 @@ export default function Page() {
|
|
|
1099
1437
|
size="sm"
|
|
1100
1438
|
variant="ghost"
|
|
1101
1439
|
className="h-7 w-7 p-0"
|
|
1102
|
-
onClick={() =>
|
|
1440
|
+
onClick={() => {
|
|
1441
|
+
setOpenFile(null);
|
|
1442
|
+
setEditing(false);
|
|
1443
|
+
}}
|
|
1103
1444
|
>
|
|
1104
1445
|
<X className="h-3.5 w-3.5" />
|
|
1105
1446
|
</Button>
|
|
1106
1447
|
</div>
|
|
1107
1448
|
</div>
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1449
|
+
{editing && openFileViewerKind === "html" ? (
|
|
1450
|
+
<div className="flex-1 flex flex-col overflow-hidden min-h-0">
|
|
1451
|
+
<textarea
|
|
1452
|
+
value={editContent}
|
|
1453
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
1454
|
+
spellCheck={false}
|
|
1455
|
+
className="flex-1 w-full min-h-0 resize-none bg-background text-foreground px-4 py-3 font-mono text-[13px] leading-relaxed outline-none border-0"
|
|
1456
|
+
/>
|
|
1457
|
+
<div className="border-t px-4 py-2 flex items-center justify-end gap-2 bg-muted shrink-0">
|
|
1458
|
+
{saveError && (
|
|
1459
|
+
<span className="text-xs text-destructive mr-auto">
|
|
1460
|
+
{saveError}
|
|
1461
|
+
</span>
|
|
1462
|
+
)}
|
|
1463
|
+
<Button
|
|
1464
|
+
size="sm"
|
|
1465
|
+
variant="ghost"
|
|
1466
|
+
onClick={() => {
|
|
1467
|
+
setEditing(false);
|
|
1468
|
+
setSaveError(null);
|
|
1469
|
+
}}
|
|
1470
|
+
>
|
|
1471
|
+
Cancel
|
|
1472
|
+
</Button>
|
|
1473
|
+
<Button
|
|
1474
|
+
size="sm"
|
|
1475
|
+
className="gap-1"
|
|
1476
|
+
onClick={handleSave}
|
|
1477
|
+
disabled={saving}
|
|
1478
|
+
>
|
|
1479
|
+
{saving && (
|
|
1480
|
+
<Loader2 className="h-3 w-3 animate-spin" />
|
|
1481
|
+
)}
|
|
1482
|
+
Save
|
|
1483
|
+
</Button>
|
|
1484
|
+
</div>
|
|
1485
|
+
</div>
|
|
1486
|
+
) : (
|
|
1487
|
+
<WebsiteViewer
|
|
1488
|
+
key={appKey}
|
|
1489
|
+
path={openFile.path}
|
|
1490
|
+
title={openFile.name}
|
|
1491
|
+
src={websiteSrc}
|
|
1492
|
+
/>
|
|
1493
|
+
)}
|
|
1114
1494
|
</div>
|
|
1115
1495
|
);
|
|
1116
1496
|
})()
|
|
@@ -1144,9 +1524,11 @@ export default function Page() {
|
|
|
1144
1524
|
setEditing(true);
|
|
1145
1525
|
setEditContent(fileContent);
|
|
1146
1526
|
setSaveError(null);
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1527
|
+
if (isMarkdown(openFile.name)) {
|
|
1528
|
+
void useEditorStore
|
|
1529
|
+
.getState()
|
|
1530
|
+
.loadPage(openFile.path);
|
|
1531
|
+
}
|
|
1150
1532
|
}}
|
|
1151
1533
|
>
|
|
1152
1534
|
<Pencil className="h-3.5 w-3.5" />
|
|
@@ -1170,6 +1552,7 @@ export default function Page() {
|
|
|
1170
1552
|
)}
|
|
1171
1553
|
</Button>
|
|
1172
1554
|
)}
|
|
1555
|
+
{widthAwareViewer && <ViewWidthToggle />}
|
|
1173
1556
|
<Button
|
|
1174
1557
|
size="sm"
|
|
1175
1558
|
variant="ghost"
|
|
@@ -1187,10 +1570,20 @@ export default function Page() {
|
|
|
1187
1570
|
|
|
1188
1571
|
{editing ? (
|
|
1189
1572
|
<div className="flex-1 flex flex-col overflow-hidden min-h-0">
|
|
1190
|
-
|
|
1573
|
+
{isMarkdown(openFile.name) ? (
|
|
1574
|
+
<KBEditor />
|
|
1575
|
+
) : (
|
|
1576
|
+
<textarea
|
|
1577
|
+
value={editContent}
|
|
1578
|
+
onChange={(e) => setEditContent(e.target.value)}
|
|
1579
|
+
spellCheck={false}
|
|
1580
|
+
className="flex-1 w-full min-h-0 resize-none bg-background text-foreground px-4 py-3 font-mono text-[13px] leading-relaxed outline-none border-0"
|
|
1581
|
+
/>
|
|
1582
|
+
)}
|
|
1191
1583
|
</div>
|
|
1192
1584
|
) : (
|
|
1193
1585
|
<div className="flex-1 overflow-auto p-4 min-h-0">
|
|
1586
|
+
<div className={cn("mx-auto w-full", contentWidthClass)}>
|
|
1194
1587
|
{fileLoading ? (
|
|
1195
1588
|
<div className="flex justify-center py-8">
|
|
1196
1589
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
@@ -1248,176 +1641,24 @@ export default function Page() {
|
|
|
1248
1641
|
<FrontmatterHeader
|
|
1249
1642
|
data={data as Record<string, never>}
|
|
1250
1643
|
/>
|
|
1251
|
-
<
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
</h3>
|
|
1268
|
-
),
|
|
1269
|
-
h4: ({ children }) => (
|
|
1270
|
-
<h4 className="text-base font-normal mt-3 mb-1">
|
|
1271
|
-
{children}
|
|
1272
|
-
</h4>
|
|
1273
|
-
),
|
|
1274
|
-
p: ({ children }) => (
|
|
1275
|
-
<p className="text-sm leading-relaxed mb-3">
|
|
1276
|
-
{children}
|
|
1277
|
-
</p>
|
|
1278
|
-
),
|
|
1279
|
-
ul: ({ children }) => (
|
|
1280
|
-
<ul className="list-disc pl-5 mb-3 space-y-1 text-sm">
|
|
1281
|
-
{children}
|
|
1282
|
-
</ul>
|
|
1283
|
-
),
|
|
1284
|
-
ol: ({ children }) => (
|
|
1285
|
-
<ol className="list-decimal pl-5 mb-3 space-y-1 text-sm">
|
|
1286
|
-
{children}
|
|
1287
|
-
</ol>
|
|
1288
|
-
),
|
|
1289
|
-
li: ({ children }) => (
|
|
1290
|
-
<li className="leading-relaxed">
|
|
1291
|
-
{children}
|
|
1292
|
-
</li>
|
|
1293
|
-
),
|
|
1294
|
-
blockquote: ({ children }) => (
|
|
1295
|
-
<blockquote className="border-l-4 border-muted-foreground/30 pl-4 italic text-muted-foreground my-3 text-sm">
|
|
1296
|
-
{children}
|
|
1297
|
-
</blockquote>
|
|
1298
|
-
),
|
|
1299
|
-
code: ({ className, children, ...props }) => {
|
|
1300
|
-
const isBlock =
|
|
1301
|
-
className?.includes("language-");
|
|
1302
|
-
return isBlock ? (
|
|
1303
|
-
<code
|
|
1304
|
-
className={`block bg-muted rounded-sm px-3 py-2 text-xs font-mono overflow-x-auto my-3 ${className ?? ""}`}
|
|
1305
|
-
{...props}
|
|
1306
|
-
>
|
|
1307
|
-
{children}
|
|
1308
|
-
</code>
|
|
1309
|
-
) : (
|
|
1310
|
-
<code
|
|
1311
|
-
className="bg-muted rounded-sm px-1 py-0.5 text-xs font-mono"
|
|
1312
|
-
{...props}
|
|
1313
|
-
>
|
|
1314
|
-
{children}
|
|
1315
|
-
</code>
|
|
1316
|
-
);
|
|
1317
|
-
},
|
|
1318
|
-
pre: ({ children }) => (
|
|
1319
|
-
<pre className="my-3 overflow-x-auto">
|
|
1320
|
-
{children}
|
|
1321
|
-
</pre>
|
|
1322
|
-
),
|
|
1323
|
-
a: ({ href, children, ...rest }) => {
|
|
1324
|
-
const props = rest as Record<
|
|
1325
|
-
string,
|
|
1326
|
-
unknown
|
|
1327
|
-
>;
|
|
1328
|
-
if (props["data-wiki-link"] === "true") {
|
|
1329
|
-
const slug =
|
|
1330
|
-
(props["data-slug"] as string) ?? "";
|
|
1331
|
-
const anchor = props["data-anchor"] as
|
|
1332
|
-
| string
|
|
1333
|
-
| undefined;
|
|
1334
|
-
const broken =
|
|
1335
|
-
slug &&
|
|
1336
|
-
!useWikiSlugsStore
|
|
1337
|
-
.getState()
|
|
1338
|
-
.has(slug);
|
|
1339
|
-
return (
|
|
1340
|
-
<a
|
|
1341
|
-
href={href}
|
|
1342
|
-
className="wiki-link"
|
|
1343
|
-
data-wiki-link="true"
|
|
1344
|
-
data-slug={slug}
|
|
1345
|
-
data-anchor={anchor}
|
|
1346
|
-
data-broken={
|
|
1347
|
-
broken ? "true" : undefined
|
|
1348
|
-
}
|
|
1349
|
-
onClick={(e) => {
|
|
1350
|
-
e.preventDefault();
|
|
1351
|
-
if (!slug) return;
|
|
1352
|
-
const dir = useWikiSlugsStore
|
|
1353
|
-
.getState()
|
|
1354
|
-
.getDir(slug);
|
|
1355
|
-
if (!dir) return;
|
|
1356
|
-
const targetPath =
|
|
1357
|
-
dir === "root"
|
|
1358
|
-
? `${slug}.md`
|
|
1359
|
-
: `${dir}/${slug}.md`;
|
|
1360
|
-
void openViewer({
|
|
1361
|
-
path: targetPath,
|
|
1362
|
-
name: `${slug}.md`,
|
|
1363
|
-
type: "file",
|
|
1364
|
-
} as TreeNode);
|
|
1365
|
-
if (anchor) {
|
|
1366
|
-
setTimeout(() => {
|
|
1367
|
-
document
|
|
1368
|
-
.getElementById(anchor)
|
|
1369
|
-
?.scrollIntoView({
|
|
1370
|
-
behavior: "smooth",
|
|
1371
|
-
});
|
|
1372
|
-
}, 200);
|
|
1373
|
-
}
|
|
1374
|
-
}}
|
|
1375
|
-
>
|
|
1376
|
-
{children}
|
|
1377
|
-
</a>
|
|
1378
|
-
);
|
|
1379
|
-
}
|
|
1380
|
-
return (
|
|
1381
|
-
<a
|
|
1382
|
-
href={href}
|
|
1383
|
-
className="text-primary underline hover:no-underline"
|
|
1384
|
-
target="_blank"
|
|
1385
|
-
rel="noreferrer"
|
|
1386
|
-
>
|
|
1387
|
-
{children}
|
|
1388
|
-
</a>
|
|
1389
|
-
);
|
|
1390
|
-
},
|
|
1391
|
-
strong: ({ children }) => (
|
|
1392
|
-
<strong className="font-normal">
|
|
1393
|
-
{children}
|
|
1394
|
-
</strong>
|
|
1395
|
-
),
|
|
1396
|
-
em: ({ children }) => (
|
|
1397
|
-
<em className="italic">{children}</em>
|
|
1398
|
-
),
|
|
1399
|
-
hr: () => <hr className="my-4 border-border" />,
|
|
1400
|
-
table: ({ children }) => (
|
|
1401
|
-
<div className="overflow-x-auto my-3">
|
|
1402
|
-
<table className="w-full text-sm border-collapse">
|
|
1403
|
-
{children}
|
|
1404
|
-
</table>
|
|
1405
|
-
</div>
|
|
1406
|
-
),
|
|
1407
|
-
th: ({ children }) => (
|
|
1408
|
-
<th className="border border-border px-3 py-1.5 bg-muted font-normal text-left">
|
|
1409
|
-
{children}
|
|
1410
|
-
</th>
|
|
1411
|
-
),
|
|
1412
|
-
td: ({ children }) => (
|
|
1413
|
-
<td className="border border-border px-3 py-1.5">
|
|
1414
|
-
{children}
|
|
1415
|
-
</td>
|
|
1416
|
-
),
|
|
1644
|
+
<MarkdownPreview
|
|
1645
|
+
markdown={body}
|
|
1646
|
+
pagePath={openFile.path}
|
|
1647
|
+
onNavigate={(targetPath, anchor) => {
|
|
1648
|
+
void openViewer({
|
|
1649
|
+
path: targetPath,
|
|
1650
|
+
name: targetPath.split('/').pop() ?? targetPath,
|
|
1651
|
+
type: 'file',
|
|
1652
|
+
} as TreeNode);
|
|
1653
|
+
if (anchor) {
|
|
1654
|
+
setTimeout(() => {
|
|
1655
|
+
document
|
|
1656
|
+
.getElementById(anchor)
|
|
1657
|
+
?.scrollIntoView({ behavior: 'smooth' });
|
|
1658
|
+
}, 200);
|
|
1659
|
+
}
|
|
1417
1660
|
}}
|
|
1418
|
-
|
|
1419
|
-
{body}
|
|
1420
|
-
</ReactMarkdown>
|
|
1661
|
+
/>
|
|
1421
1662
|
</>
|
|
1422
1663
|
);
|
|
1423
1664
|
})()
|
|
@@ -1435,6 +1676,7 @@ export default function Page() {
|
|
|
1435
1676
|
Preview not available for this file type.
|
|
1436
1677
|
</p>
|
|
1437
1678
|
)}
|
|
1679
|
+
</div>
|
|
1438
1680
|
</div>
|
|
1439
1681
|
)}
|
|
1440
1682
|
|
|
@@ -1480,6 +1722,8 @@ export default function Page() {
|
|
|
1480
1722
|
)}
|
|
1481
1723
|
</div>
|
|
1482
1724
|
|
|
1725
|
+
<AuthSettingsSheet open={settingsOpen} onOpenChange={setSettingsOpen} />
|
|
1726
|
+
<AIPanel currentPath={openFile?.path} />
|
|
1483
1727
|
<input
|
|
1484
1728
|
ref={fileInputRef}
|
|
1485
1729
|
type="file"
|