wiki-viewer 1.8.2 → 1.8.3
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/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +5 -5
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/signin/page/build-manifest.json +2 -2
- package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-inabx._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__00lep-u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{0~w0_katex_dist_katex_mjs_04mhawr._.js → 0~w0_katex_dist_katex_mjs_0hyln.b._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09~x2v~._.js → [root-of-the-server]__0du58sw._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +19 -19
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0shlang._.js → node_modules__pnpm_0.kyqfq._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_04n0h6.._.js → node_modules__pnpm_0.~goq~._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_011vy_q._.js → node_modules__pnpm_00y7foi._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0yiz.-t._.js → node_modules__pnpm_03.1jli._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_093dp2q._.js → node_modules__pnpm_03z3na-._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_01o_5qt._.js → node_modules__pnpm_05~w-ok._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0b8qx0g._.js → node_modules__pnpm_09d5yyu._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0wmx9kq._.js → node_modules__pnpm_09eo8jh._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0nw5ou2._.js → node_modules__pnpm_0adm8h6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0bbc---._.js → node_modules__pnpm_0arbjru._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0t9jb7d._.js → node_modules__pnpm_0b5fla7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_124u4o2._.js → node_modules__pnpm_0beormu._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_13tajgx._.js → node_modules__pnpm_0c7u76a._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_09ej62_._.js → node_modules__pnpm_0dbr7c2._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0s0ur~d._.js → node_modules__pnpm_0d~ida~._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_09ljbdz._.js → node_modules__pnpm_0elpfsb._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hlzy8f._.js → node_modules__pnpm_0fh.6ir._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_09~_ob2._.js → node_modules__pnpm_0fm6wcx._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0o.dn8o._.js → node_modules__pnpm_0iikbcy._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hk6xys._.js → node_modules__pnpm_0mvih7b._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0a01ng7._.js → node_modules__pnpm_0qd2ovj._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0c.309.._.js → node_modules__pnpm_0s4yn43._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0sq3c2r._.js → node_modules__pnpm_0s8fvbn._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xfx0sf._.js → node_modules__pnpm_0tq8zs6._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_11sut1u._.js → node_modules__pnpm_0vyv.lz._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0c8aunj._.js → node_modules__pnpm_0xa.ko1._.js} +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0.96tmo._.js → node_modules__pnpm_12.ymhl._.js} +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +5 -5
- package/.next/standalone/.next/server/middleware-manifest.json +1 -1
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/static/chunks/{0vp-ahhagxptc.js → 06iw_iy5e90d4.js} +1 -1
- package/.next/standalone/.next/static/chunks/0d~o0h0ssgra7.js +129 -0
- package/.next/standalone/.next/static/chunks/167ph4xws_-61.css +2 -0
- package/.next/standalone/.next/static/chunks/{turbopack-12~z-mr7b9b50.js → turbopack-0lf4~b-ofzbm6.js} +1 -1
- package/.next/standalone/AGENTS.md +104 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/src/app/api/wiki/app/route.ts +7 -4
- package/.next/standalone/src/app/page.tsx +580 -192
- package/.next/standalone/src/components/editor/backlinks-panel.tsx +152 -0
- package/.next/standalone/src/components/editor/document-outline.tsx +221 -0
- package/.next/standalone/src/components/editor/editor.tsx +145 -48
- package/.next/standalone/src/components/editor/node-app-viewer.tsx +65 -7
- package/.next/standalone/src/components/editor/slash-commands.tsx +216 -41
- package/.next/standalone/src/components/editor/source-viewer.tsx +29 -2
- package/.next/standalone/src/components/search/search-command-dialog.tsx +183 -46
- package/.next/standalone/src/lib/app-runner.ts +57 -16
- package/.next/standalone/src/stores/pin-store.ts +55 -0
- package/.next/standalone/src/stores/recent-store.ts +50 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/.next/standalone/.next/static/chunks/0c_~zevf2n0r4.css +0 -2
- package/.next/standalone/.next/static/chunks/16r--8~rpehcz.js +0 -129
- package/.next/standalone/src/components/wiki/markdown-preview.tsx +0 -120
- /package/.next/standalone/.next/static/{t_k0ztuf0V-iA-kgeez18 → Pkj31aCfRVk0Zy2rKA1wf}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{t_k0ztuf0V-iA-kgeez18 → Pkj31aCfRVk0Zy2rKA1wf}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{t_k0ztuf0V-iA-kgeez18 → Pkj31aCfRVk0Zy2rKA1wf}/_ssgManifest.js +0 -0
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { cellAround, isInTable } from "@tiptap/pm/tables";
|
|
4
4
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
5
|
-
import { Code2, FilePlus, Loader2, Sparkles } from "lucide-react";
|
|
5
|
+
import { AlertCircle, Check, Code2, FilePlus, Loader2, Sparkles } from "lucide-react";
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { findNodeByPath } from "@/lib/cabinets/tree";
|
|
8
8
|
import { markdownToHtml } from "@/lib/markdown/to-html";
|
|
9
9
|
import { htmlToMarkdown } from "@/lib/markdown/to-markdown";
|
|
10
|
+
import { parseFrontmatter } from "@/lib/markdown/parse-frontmatter";
|
|
10
11
|
import { useAIPanelStore } from "@/stores/ai-panel-store";
|
|
11
12
|
import { useEditorStore } from "@/stores/editor-store";
|
|
12
13
|
import { useTreeStore } from "@/stores/tree-store";
|
|
@@ -16,6 +17,7 @@ import type { TreeNode } from "@/types";
|
|
|
16
17
|
import { useProofStore } from "@/stores/proof-store";
|
|
17
18
|
import { captureSuggestion } from "@/lib/proof/suggest-capture";
|
|
18
19
|
import { wsFetch, withWs } from "@/lib/workspace-client";
|
|
20
|
+
import { showError } from "@/lib/toast";
|
|
19
21
|
import { EditorBubbleMenu } from "./bubble-menu";
|
|
20
22
|
import { EditorToolbar } from "./editor-toolbar";
|
|
21
23
|
import { editorExtensions } from "./extensions";
|
|
@@ -26,12 +28,15 @@ import { ProofSpanPopover } from "./proof-span-popover";
|
|
|
26
28
|
import { SuggestionCard } from "./suggestion-card";
|
|
27
29
|
import { SuggestEditPopover } from "./suggest-edit-popover";
|
|
28
30
|
import { SlashCommands } from "./slash-commands";
|
|
31
|
+
import { DocumentOutline } from "./document-outline";
|
|
32
|
+
import { BacklinksPanel } from "./backlinks-panel";
|
|
29
33
|
import { TableMenu } from "./table-menu";
|
|
30
34
|
import {
|
|
31
35
|
useWikiLinkCreate,
|
|
32
36
|
type WikiCreateResult,
|
|
33
37
|
} from "./wiki-link-create-dialog";
|
|
34
38
|
import { WikiLinkPicker } from "./wiki-link-picker";
|
|
39
|
+
import { FrontmatterHeader } from "@/components/wiki/frontmatter-header";
|
|
35
40
|
|
|
36
41
|
async function uploadFile(
|
|
37
42
|
pagePath: string,
|
|
@@ -44,10 +49,14 @@ async function uploadFile(
|
|
|
44
49
|
method: "POST",
|
|
45
50
|
body: formData,
|
|
46
51
|
});
|
|
47
|
-
if (!res.ok)
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
showError(`Upload failed: ${file.name}`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
48
56
|
const data = await res.json();
|
|
49
57
|
return data.url;
|
|
50
58
|
} catch {
|
|
59
|
+
showError(`Upload failed: ${file.name}`);
|
|
51
60
|
return null;
|
|
52
61
|
}
|
|
53
62
|
}
|
|
@@ -136,7 +145,13 @@ function resolveInternalLink(
|
|
|
136
145
|
return findPageBySlug(slug, currentPath, nodes);
|
|
137
146
|
}
|
|
138
147
|
|
|
139
|
-
|
|
148
|
+
type KBEditorMode = "viewing" | "editing" | "suggesting";
|
|
149
|
+
|
|
150
|
+
interface KBEditorProps {
|
|
151
|
+
mode?: KBEditorMode;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function KBEditor({ mode }: KBEditorProps = {}) {
|
|
140
155
|
const {
|
|
141
156
|
currentPath,
|
|
142
157
|
content,
|
|
@@ -148,9 +163,18 @@ export function KBEditor() {
|
|
|
148
163
|
editMode,
|
|
149
164
|
setEditMode,
|
|
150
165
|
} = useEditorStore();
|
|
166
|
+
const effectiveMode = mode ?? editMode;
|
|
167
|
+
const isViewing = effectiveMode === "viewing";
|
|
168
|
+
const isSuggesting = effectiveMode === "suggesting";
|
|
169
|
+
const parsedViewingContent = useMemo(
|
|
170
|
+
() => (isViewing ? parseFrontmatter(content) : { data: {}, body: content }),
|
|
171
|
+
[content, isViewing],
|
|
172
|
+
);
|
|
151
173
|
const nodes = useTreeStore((s) => s.nodes);
|
|
152
174
|
const editorMaxW = useViewWidthStore((s) => VIEW_WIDTH_CSS[s.width]);
|
|
153
|
-
const isRtl =
|
|
175
|
+
const isRtl = isViewing
|
|
176
|
+
? parsedViewingContent.data.dir === "rtl"
|
|
177
|
+
: frontmatter?.dir === "rtl";
|
|
154
178
|
const { open: openAI, clearMessages } = useAIPanelStore();
|
|
155
179
|
const { open: openWikiCreate, Dialog: WikiCreateDialog } =
|
|
156
180
|
useWikiLinkCreate();
|
|
@@ -161,6 +185,8 @@ export function KBEditor() {
|
|
|
161
185
|
openWikiCreateRef.current = openWikiCreate;
|
|
162
186
|
|
|
163
187
|
const isLoadingRef = useRef(false);
|
|
188
|
+
const isViewingRef = useRef(isViewing);
|
|
189
|
+
isViewingRef.current = isViewing;
|
|
164
190
|
const [sourceMode, setSourceMode] = useState(false);
|
|
165
191
|
const [sourceText, setSourceText] = useState("");
|
|
166
192
|
// Reset the tab to "page" whenever the path changes — opening a new folder
|
|
@@ -193,6 +219,7 @@ export function KBEditor() {
|
|
|
193
219
|
useEffect(() => {
|
|
194
220
|
if (typeof window === "undefined") return;
|
|
195
221
|
if (!currentPath) return;
|
|
222
|
+
if (isViewing) return;
|
|
196
223
|
if (!/\.(md|markdown)$/i.test(currentPath)) return;
|
|
197
224
|
|
|
198
225
|
const path = currentPath;
|
|
@@ -221,7 +248,7 @@ export function KBEditor() {
|
|
|
221
248
|
document.removeEventListener("visibilitychange", onHidden);
|
|
222
249
|
ping("close");
|
|
223
250
|
};
|
|
224
|
-
}, [currentPath]);
|
|
251
|
+
}, [currentPath, isViewing]);
|
|
225
252
|
|
|
226
253
|
// Subscribe to chokidar SSE: when current file changes on disk, reload sidecar.
|
|
227
254
|
useEffect(() => {
|
|
@@ -243,6 +270,9 @@ export function KBEditor() {
|
|
|
243
270
|
.getState()
|
|
244
271
|
.loadSnapshot(activePath)
|
|
245
272
|
.then(() => useProofStore.getState().loadSidecar(activePath));
|
|
273
|
+
if (isViewingRef.current) {
|
|
274
|
+
void useEditorStore.getState().loadPage(activePath);
|
|
275
|
+
}
|
|
246
276
|
}
|
|
247
277
|
} catch {
|
|
248
278
|
// ignore malformed events
|
|
@@ -400,6 +430,7 @@ export function KBEditor() {
|
|
|
400
430
|
|
|
401
431
|
const flushSuggestions = useCallback(async () => {
|
|
402
432
|
if (flushingRef.current) return;
|
|
433
|
+
if (isViewingRef.current) return;
|
|
403
434
|
if (useEditorStore.getState().editMode !== "suggesting") return;
|
|
404
435
|
if (!suggestDirtyRef.current) return;
|
|
405
436
|
const ed = editorRef.current;
|
|
@@ -527,7 +558,7 @@ export function KBEditor() {
|
|
|
527
558
|
|
|
528
559
|
const handleUpdate = useCallback(
|
|
529
560
|
({ editor }: { editor: ReturnType<typeof useEditor> }) => {
|
|
530
|
-
if (isLoadingRef.current || !editor) return;
|
|
561
|
+
if (isLoadingRef.current || isViewingRef.current || !editor) return;
|
|
531
562
|
// In suggesting mode, mark the edit dirty so the next block-change or
|
|
532
563
|
// blur flushes it into suggestions. Still push content to the store so
|
|
533
564
|
// the store guard (no autosave in suggesting mode) keeps it in sync.
|
|
@@ -544,6 +575,7 @@ export function KBEditor() {
|
|
|
544
575
|
const editor = useEditor({
|
|
545
576
|
extensions: editorExtensions,
|
|
546
577
|
content: "",
|
|
578
|
+
editable: !isViewing,
|
|
547
579
|
onUpdate: handleUpdate,
|
|
548
580
|
onBlur: () => {
|
|
549
581
|
void flushSuggestions();
|
|
@@ -622,6 +654,8 @@ export function KBEditor() {
|
|
|
622
654
|
?.scrollIntoView({ behavior: "smooth" });
|
|
623
655
|
}, 200);
|
|
624
656
|
}
|
|
657
|
+
} else if (isViewingRef.current) {
|
|
658
|
+
return true;
|
|
625
659
|
} else {
|
|
626
660
|
void openWikiCreateRef.current(slug).then((result) => {
|
|
627
661
|
if (result.ok) {
|
|
@@ -667,6 +701,7 @@ export function KBEditor() {
|
|
|
667
701
|
return true;
|
|
668
702
|
},
|
|
669
703
|
handlePaste: (_view, event) => {
|
|
704
|
+
if (isViewingRef.current) return false;
|
|
670
705
|
const files = event.clipboardData?.files;
|
|
671
706
|
const pagePath = useEditorStore.getState().currentPath;
|
|
672
707
|
|
|
@@ -696,6 +731,7 @@ export function KBEditor() {
|
|
|
696
731
|
return false;
|
|
697
732
|
},
|
|
698
733
|
handleDrop: (_view, event) => {
|
|
734
|
+
if (isViewingRef.current) return false;
|
|
699
735
|
const files = event.dataTransfer?.files;
|
|
700
736
|
if (!files || files.length === 0) return false;
|
|
701
737
|
|
|
@@ -731,6 +767,11 @@ export function KBEditor() {
|
|
|
731
767
|
const editorRef = useRef<typeof editor>(editor);
|
|
732
768
|
editorRef.current = editor;
|
|
733
769
|
|
|
770
|
+
useEffect(() => {
|
|
771
|
+
editor?.setEditable(!isViewing);
|
|
772
|
+
if (isViewing) setSourceMode(false);
|
|
773
|
+
}, [editor, isViewing]);
|
|
774
|
+
|
|
734
775
|
// When content updates from store (after loadPage), set it in editor
|
|
735
776
|
const prevPathRef = useRef<string | null>(null);
|
|
736
777
|
const renderedKeyRef = useRef<string | null>(null);
|
|
@@ -750,7 +791,8 @@ export function KBEditor() {
|
|
|
750
791
|
if (isLoading && content === "") return;
|
|
751
792
|
// Dedupe identical (path, content) renders — e.g. cached paint followed
|
|
752
793
|
// by a fresh fetch that returned the same markdown.
|
|
753
|
-
const
|
|
794
|
+
const renderMarkdown = parsedViewingContent.body;
|
|
795
|
+
const key = `${currentPath} ${renderMarkdown}`;
|
|
754
796
|
if (renderedKeyRef.current === key) {
|
|
755
797
|
if (renderedPath !== currentPath) setRenderedPath(currentPath);
|
|
756
798
|
return;
|
|
@@ -759,7 +801,10 @@ export function KBEditor() {
|
|
|
759
801
|
|
|
760
802
|
const setContent = async () => {
|
|
761
803
|
isLoadingRef.current = true;
|
|
762
|
-
const html = await markdownToHtml(
|
|
804
|
+
const html = await markdownToHtml(
|
|
805
|
+
renderMarkdown,
|
|
806
|
+
isViewing ? { pagePath: currentPath, sanitize: true } : currentPath,
|
|
807
|
+
);
|
|
763
808
|
editor.commands.setContent(html);
|
|
764
809
|
renderedKeyRef.current = key;
|
|
765
810
|
setRenderedPath(currentPath);
|
|
@@ -769,7 +814,18 @@ export function KBEditor() {
|
|
|
769
814
|
};
|
|
770
815
|
|
|
771
816
|
setContent();
|
|
772
|
-
}, [editor, content, currentPath, isLoading, renderedPath]);
|
|
817
|
+
}, [editor, content, currentPath, isLoading, renderedPath, parsedViewingContent.body, isViewing]);
|
|
818
|
+
|
|
819
|
+
useEffect(() => {
|
|
820
|
+
if (!isViewing || !renderedPath) return;
|
|
821
|
+
const container = scrollContainerRef.current;
|
|
822
|
+
if (!container) return;
|
|
823
|
+
container
|
|
824
|
+
.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')
|
|
825
|
+
.forEach((input) => {
|
|
826
|
+
input.disabled = true;
|
|
827
|
+
});
|
|
828
|
+
}, [isViewing, renderedPath, parsedViewingContent.body]);
|
|
773
829
|
|
|
774
830
|
const showLoadingOverlay =
|
|
775
831
|
currentPath !== null && (isLoading || renderedPath !== currentPath);
|
|
@@ -915,22 +971,24 @@ export function KBEditor() {
|
|
|
915
971
|
</div>
|
|
916
972
|
) : (
|
|
917
973
|
<>
|
|
918
|
-
|
|
919
|
-
<div className="flex-
|
|
920
|
-
|
|
974
|
+
{!isViewing && (
|
|
975
|
+
<div className="flex items-center min-w-0">
|
|
976
|
+
<div className="flex-1 min-w-0">
|
|
977
|
+
{!sourceMode && <EditorToolbar editor={editor} />}
|
|
978
|
+
</div>
|
|
979
|
+
<button
|
|
980
|
+
onClick={toggleSourceMode}
|
|
981
|
+
className={`flex items-center gap-1.5 px-3 py-1 mr-2 text-[11px] rounded-md transition-colors border border-border ${
|
|
982
|
+
sourceMode
|
|
983
|
+
? "bg-primary text-primary-foreground"
|
|
984
|
+
: "text-muted-foreground hover:bg-accent"
|
|
985
|
+
}`}
|
|
986
|
+
>
|
|
987
|
+
<Code2 className="h-3 w-3" />
|
|
988
|
+
{sourceMode ? "Preview" : "Markdown"}
|
|
989
|
+
</button>
|
|
921
990
|
</div>
|
|
922
|
-
|
|
923
|
-
onClick={toggleSourceMode}
|
|
924
|
-
className={`flex items-center gap-1.5 px-3 py-1 mr-2 text-[11px] rounded-md transition-colors border border-border ${
|
|
925
|
-
sourceMode
|
|
926
|
-
? "bg-primary text-primary-foreground"
|
|
927
|
-
: "text-muted-foreground hover:bg-accent"
|
|
928
|
-
}`}
|
|
929
|
-
>
|
|
930
|
-
<Code2 className="h-3 w-3" />
|
|
931
|
-
{sourceMode ? "Preview" : "Markdown"}
|
|
932
|
-
</button>
|
|
933
|
-
</div>
|
|
991
|
+
)}
|
|
934
992
|
|
|
935
993
|
{sourceMode ? (
|
|
936
994
|
<div
|
|
@@ -946,15 +1004,16 @@ export function KBEditor() {
|
|
|
946
1004
|
</div>
|
|
947
1005
|
) : (
|
|
948
1006
|
<div className="flex-1 relative" dir={isRtl ? "rtl" : undefined}>
|
|
949
|
-
{
|
|
1007
|
+
{isSuggesting && (
|
|
950
1008
|
<div className="absolute top-0 inset-x-0 z-20 flex items-center justify-center gap-2 px-3 py-1 bg-primary/10 border-b border-primary/20 text-[11px] text-primary pointer-events-none">
|
|
951
1009
|
Suggesting mode · your edits become suggestions for review
|
|
952
1010
|
</div>
|
|
953
1011
|
)}
|
|
1012
|
+
<DocumentOutline editor={editor} scrollContainerRef={scrollContainerRef} />
|
|
954
1013
|
<div
|
|
955
1014
|
ref={scrollContainerRef}
|
|
956
1015
|
className={`absolute inset-0 overflow-y-auto ${
|
|
957
|
-
|
|
1016
|
+
isSuggesting ? "pt-7" : ""
|
|
958
1017
|
}`}
|
|
959
1018
|
style={{ ["--editor-max-w" as string]: editorMaxW }}
|
|
960
1019
|
data-editor-scroll
|
|
@@ -1043,7 +1102,17 @@ export function KBEditor() {
|
|
|
1043
1102
|
/>
|
|
1044
1103
|
)}
|
|
1045
1104
|
|
|
1105
|
+
{isViewing && Object.keys(parsedViewingContent.data).length > 0 && (
|
|
1106
|
+
<div className="max-w-[var(--editor-max-w,48rem)] mx-auto px-4 sm:px-8 pt-6">
|
|
1107
|
+
<FrontmatterHeader
|
|
1108
|
+
data={parsedViewingContent.data as Record<string, never>}
|
|
1109
|
+
/>
|
|
1110
|
+
</div>
|
|
1111
|
+
)}
|
|
1046
1112
|
<EditorContent editor={editor} />
|
|
1113
|
+
{currentPath && /\.(md|markdown)$/i.test(currentPath) && (
|
|
1114
|
+
<BacklinksPanel currentPath={currentPath} />
|
|
1115
|
+
)}
|
|
1047
1116
|
{/* Proof-span hover delegation */}
|
|
1048
1117
|
<div
|
|
1049
1118
|
aria-hidden="true"
|
|
@@ -1071,17 +1140,21 @@ export function KBEditor() {
|
|
|
1071
1140
|
}}
|
|
1072
1141
|
/>
|
|
1073
1142
|
)}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1143
|
+
{!isViewing && (
|
|
1144
|
+
<>
|
|
1145
|
+
<EditorBubbleMenu
|
|
1146
|
+
editor={editor}
|
|
1147
|
+
onSuggestEdit={openSuggestForSelection}
|
|
1148
|
+
onComment={openCommentForSelection}
|
|
1149
|
+
/>
|
|
1150
|
+
<TableMenu editor={editor} />
|
|
1151
|
+
<SlashCommands editor={editor} />
|
|
1152
|
+
<WikiLinkPicker
|
|
1153
|
+
editor={editor}
|
|
1154
|
+
onCreateRequest={openWikiCreateRef.current}
|
|
1155
|
+
/>
|
|
1156
|
+
</>
|
|
1157
|
+
)}
|
|
1085
1158
|
|
|
1086
1159
|
{/* AI Edit Prompt + slash hint */}
|
|
1087
1160
|
<div className="max-w-[var(--editor-max-w,48rem)] mx-auto px-8 pb-8 flex items-center gap-4">
|
|
@@ -1092,12 +1165,14 @@ export function KBEditor() {
|
|
|
1092
1165
|
<Sparkles className="h-3.5 w-3.5 group-hover:text-primary transition-colors" />
|
|
1093
1166
|
<span>Edit with AI</span>
|
|
1094
1167
|
</button>
|
|
1095
|
-
|
|
1096
|
-
<
|
|
1097
|
-
/
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1168
|
+
{!isViewing && (
|
|
1169
|
+
<span className="text-[11px] text-muted-foreground/30 select-none">
|
|
1170
|
+
<kbd className="rounded px-1 py-0.5 font-mono text-[10px] ring-1 ring-foreground/10">
|
|
1171
|
+
/
|
|
1172
|
+
</kbd>{" "}
|
|
1173
|
+
for commands
|
|
1174
|
+
</span>
|
|
1175
|
+
)}
|
|
1101
1176
|
</div>
|
|
1102
1177
|
</div>
|
|
1103
1178
|
|
|
@@ -1112,7 +1187,8 @@ export function KBEditor() {
|
|
|
1112
1187
|
</div>
|
|
1113
1188
|
)}
|
|
1114
1189
|
|
|
1115
|
-
{
|
|
1190
|
+
{!isViewing && (
|
|
1191
|
+
/* Status bar */
|
|
1116
1192
|
<div className="flex items-center justify-between px-4 py-1 border-t border-border text-xs text-muted-foreground/60">
|
|
1117
1193
|
<span className="text-[10.5px] text-muted-foreground/30 select-none hidden sm:block">
|
|
1118
1194
|
<kbd className="rounded px-1 font-mono text-[9.5px] ring-1 ring-foreground/10">
|
|
@@ -1159,13 +1235,34 @@ export function KBEditor() {
|
|
|
1159
1235
|
Suggesting
|
|
1160
1236
|
</button>
|
|
1161
1237
|
</div>
|
|
1162
|
-
<span
|
|
1163
|
-
{
|
|
1164
|
-
|
|
1165
|
-
|
|
1238
|
+
<span
|
|
1239
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10.5px] transition-all duration-300 ${
|
|
1240
|
+
saveStatus === "idle"
|
|
1241
|
+
? "opacity-0 pointer-events-none"
|
|
1242
|
+
: "opacity-100"
|
|
1243
|
+
} ${
|
|
1244
|
+
saveStatus === "saving"
|
|
1245
|
+
? "bg-muted text-muted-foreground"
|
|
1246
|
+
: saveStatus === "saved"
|
|
1247
|
+
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
|
1248
|
+
: saveStatus === "error"
|
|
1249
|
+
? "bg-destructive/10 text-destructive"
|
|
1250
|
+
: ""
|
|
1251
|
+
}`}
|
|
1252
|
+
>
|
|
1253
|
+
{saveStatus === "saving" && (
|
|
1254
|
+
<><Loader2 className="h-2.5 w-2.5 animate-spin" />Saving…</>
|
|
1255
|
+
)}
|
|
1256
|
+
{saveStatus === "saved" && (
|
|
1257
|
+
<><Check className="h-2.5 w-2.5" />Saved</>
|
|
1258
|
+
)}
|
|
1259
|
+
{saveStatus === "error" && (
|
|
1260
|
+
<><AlertCircle className="h-2.5 w-2.5" />Save failed</>
|
|
1261
|
+
)}
|
|
1166
1262
|
</span>
|
|
1167
1263
|
</div>
|
|
1168
1264
|
</div>
|
|
1265
|
+
)}
|
|
1169
1266
|
</>
|
|
1170
1267
|
)}
|
|
1171
1268
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
AlertCircle,
|
|
5
|
+
ChevronDown,
|
|
5
6
|
ExternalLink,
|
|
6
7
|
Loader2,
|
|
7
8
|
Play,
|
|
@@ -12,6 +13,14 @@ import {
|
|
|
12
13
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
14
|
import { ViewerToolbar } from "@/components/layout/viewer-toolbar";
|
|
14
15
|
import { Button } from "@/components/ui/button";
|
|
16
|
+
import {
|
|
17
|
+
DropdownMenu,
|
|
18
|
+
DropdownMenuContent,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuLabel,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "@/components/ui/dropdown-menu";
|
|
15
24
|
import type { AppStatus } from "@/lib/app-runner";
|
|
16
25
|
import { wsFetch } from "@/lib/workspace-client";
|
|
17
26
|
|
|
@@ -25,6 +34,8 @@ interface StatusResponse {
|
|
|
25
34
|
port?: number;
|
|
26
35
|
error?: string;
|
|
27
36
|
logs: string[];
|
|
37
|
+
scripts?: string[];
|
|
38
|
+
defaultScript?: string | null;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
const STATUS_LABEL: Record<AppStatus, string> = {
|
|
@@ -42,6 +53,8 @@ export function NodeAppViewer({ path, title }: Props) {
|
|
|
42
53
|
const [error, setError] = useState<string | null>(null);
|
|
43
54
|
const [showLogs, setShowLogs] = useState(false);
|
|
44
55
|
const [iframeKey, setIframeKey] = useState(0);
|
|
56
|
+
const [scripts, setScripts] = useState<string[]>([]);
|
|
57
|
+
const [defaultScript, setDefaultScript] = useState<string | null>(null);
|
|
45
58
|
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
46
59
|
const logsEndRef = useRef<HTMLDivElement>(null);
|
|
47
60
|
|
|
@@ -62,6 +75,8 @@ export function NodeAppViewer({ path, title }: Props) {
|
|
|
62
75
|
const data: StatusResponse = await res.json();
|
|
63
76
|
setStatus(data.status);
|
|
64
77
|
setLogs(data.logs ?? []);
|
|
78
|
+
if (data.scripts) setScripts(data.scripts);
|
|
79
|
+
if (data.defaultScript !== undefined) setDefaultScript(data.defaultScript);
|
|
65
80
|
if (data.port) setPort(data.port);
|
|
66
81
|
if (data.error) setError(data.error);
|
|
67
82
|
if (data.status === "running" || data.status === "stopped" || data.status === "error") {
|
|
@@ -92,7 +107,7 @@ export function NodeAppViewer({ path, title }: Props) {
|
|
|
92
107
|
fetchStatus();
|
|
93
108
|
}, [fetchStatus]);
|
|
94
109
|
|
|
95
|
-
const handleLaunch = async () => {
|
|
110
|
+
const handleLaunch = async (script?: string) => {
|
|
96
111
|
setError(null);
|
|
97
112
|
setLogs([]);
|
|
98
113
|
setStatus("starting");
|
|
@@ -100,7 +115,7 @@ export function NodeAppViewer({ path, title }: Props) {
|
|
|
100
115
|
const res = await wsFetch("/api/wiki/app", {
|
|
101
116
|
method: "POST",
|
|
102
117
|
headers: { "Content-Type": "application/json" },
|
|
103
|
-
body: JSON.stringify({ path }),
|
|
118
|
+
body: JSON.stringify({ path, script }),
|
|
104
119
|
});
|
|
105
120
|
const data: { port?: number; error?: string } = await res.json();
|
|
106
121
|
if (!res.ok || data.error) {
|
|
@@ -134,6 +149,8 @@ export function NodeAppViewer({ path, title }: Props) {
|
|
|
134
149
|
};
|
|
135
150
|
|
|
136
151
|
const isTransient = status === "installing" || status === "starting";
|
|
152
|
+
// Scripts other than the default — offered in the combo dropdown.
|
|
153
|
+
const altScripts = scripts.filter((s) => s !== defaultScript);
|
|
137
154
|
|
|
138
155
|
return (
|
|
139
156
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
@@ -216,10 +233,51 @@ export function NodeAppViewer({ path, title }: Props) {
|
|
|
216
233
|
Node.js app — will be started on a local port
|
|
217
234
|
</p>
|
|
218
235
|
</div>
|
|
219
|
-
<
|
|
220
|
-
<
|
|
221
|
-
|
|
222
|
-
|
|
236
|
+
<div className="inline-flex items-stretch overflow-hidden rounded-full bg-primary text-primary-foreground shadow-e-1">
|
|
237
|
+
<button
|
|
238
|
+
type="button"
|
|
239
|
+
onClick={() => handleLaunch()}
|
|
240
|
+
className="inline-flex items-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors hover:bg-primary-foreground/10 focus-visible:outline-none"
|
|
241
|
+
>
|
|
242
|
+
<Play className="h-4 w-4" />
|
|
243
|
+
Launch app
|
|
244
|
+
{defaultScript && (
|
|
245
|
+
<span className="opacity-70">({defaultScript})</span>
|
|
246
|
+
)}
|
|
247
|
+
</button>
|
|
248
|
+
{altScripts.length > 0 && (
|
|
249
|
+
<DropdownMenu>
|
|
250
|
+
<DropdownMenuTrigger asChild>
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
className="inline-flex items-center justify-center border-l border-l-primary-foreground/20 px-2 transition-colors hover:bg-primary-foreground/10 focus-visible:outline-none"
|
|
254
|
+
aria-label="Choose script to launch"
|
|
255
|
+
>
|
|
256
|
+
<ChevronDown className="h-4 w-4" />
|
|
257
|
+
</button>
|
|
258
|
+
</DropdownMenuTrigger>
|
|
259
|
+
<DropdownMenuContent align="end">
|
|
260
|
+
<DropdownMenuLabel>Run script</DropdownMenuLabel>
|
|
261
|
+
<DropdownMenuSeparator />
|
|
262
|
+
{scripts.map((s) => (
|
|
263
|
+
<DropdownMenuItem
|
|
264
|
+
key={s}
|
|
265
|
+
onSelect={() => handleLaunch(s)}
|
|
266
|
+
className="gap-2 font-mono text-xs"
|
|
267
|
+
>
|
|
268
|
+
<Play className="h-3.5 w-3.5" />
|
|
269
|
+
{s}
|
|
270
|
+
{s === defaultScript && (
|
|
271
|
+
<span className="ml-auto text-[10px] text-muted-foreground">
|
|
272
|
+
default
|
|
273
|
+
</span>
|
|
274
|
+
)}
|
|
275
|
+
</DropdownMenuItem>
|
|
276
|
+
))}
|
|
277
|
+
</DropdownMenuContent>
|
|
278
|
+
</DropdownMenu>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
223
281
|
</div>
|
|
224
282
|
)}
|
|
225
283
|
|
|
@@ -248,7 +306,7 @@ export function NodeAppViewer({ path, title }: Props) {
|
|
|
248
306
|
{error ?? "Unknown error"}
|
|
249
307
|
</p>
|
|
250
308
|
</div>
|
|
251
|
-
<Button onClick={handleLaunch} variant="outline" className="gap-2">
|
|
309
|
+
<Button onClick={() => handleLaunch()} variant="outline" className="gap-2">
|
|
252
310
|
<RefreshCw className="h-4 w-4" />
|
|
253
311
|
Try again
|
|
254
312
|
</Button>
|