@gmickel/gno 0.7.0 → 0.8.2
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/LICENSE +21 -0
- package/README.md +90 -50
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/assets/screenshots/webui-ask-answer.png +0 -0
- package/assets/screenshots/webui-collections.png +0 -0
- package/assets/screenshots/webui-editor.png +0 -0
- package/assets/screenshots/webui-home.png +0 -0
- package/assets/skill/SKILL.md +12 -12
- package/assets/skill/cli-reference.md +59 -57
- package/assets/skill/examples.md +8 -7
- package/assets/skill/mcp-reference.md +8 -4
- package/package.json +32 -25
- package/src/app/constants.ts +43 -42
- package/src/cli/colors.ts +1 -1
- package/src/cli/commands/ask.ts +44 -43
- package/src/cli/commands/cleanup.ts +9 -8
- package/src/cli/commands/collection/add.ts +12 -12
- package/src/cli/commands/collection/index.ts +4 -4
- package/src/cli/commands/collection/list.ts +26 -25
- package/src/cli/commands/collection/remove.ts +10 -10
- package/src/cli/commands/collection/rename.ts +10 -10
- package/src/cli/commands/context/add.ts +1 -1
- package/src/cli/commands/context/check.ts +17 -17
- package/src/cli/commands/context/index.ts +4 -4
- package/src/cli/commands/context/list.ts +11 -11
- package/src/cli/commands/context/rm.ts +1 -1
- package/src/cli/commands/doctor.ts +86 -84
- package/src/cli/commands/embed.ts +30 -28
- package/src/cli/commands/get.ts +27 -26
- package/src/cli/commands/index-cmd.ts +9 -9
- package/src/cli/commands/index.ts +16 -16
- package/src/cli/commands/init.ts +13 -12
- package/src/cli/commands/ls.ts +20 -19
- package/src/cli/commands/mcp/config.ts +30 -28
- package/src/cli/commands/mcp/index.ts +4 -4
- package/src/cli/commands/mcp/install.ts +17 -17
- package/src/cli/commands/mcp/paths.ts +133 -133
- package/src/cli/commands/mcp/status.ts +21 -21
- package/src/cli/commands/mcp/uninstall.ts +13 -13
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/models/clear.ts +12 -11
- package/src/cli/commands/models/index.ts +5 -5
- package/src/cli/commands/models/list.ts +31 -30
- package/src/cli/commands/models/path.ts +1 -1
- package/src/cli/commands/models/pull.ts +19 -18
- package/src/cli/commands/models/use.ts +4 -4
- package/src/cli/commands/multi-get.ts +38 -36
- package/src/cli/commands/query.ts +21 -20
- package/src/cli/commands/ref-parser.ts +10 -10
- package/src/cli/commands/reset.ts +40 -39
- package/src/cli/commands/search.ts +14 -13
- package/src/cli/commands/serve.ts +4 -4
- package/src/cli/commands/shared.ts +11 -10
- package/src/cli/commands/skill/index.ts +5 -5
- package/src/cli/commands/skill/install.ts +18 -17
- package/src/cli/commands/skill/paths-cmd.ts +11 -10
- package/src/cli/commands/skill/paths.ts +23 -23
- package/src/cli/commands/skill/show.ts +13 -12
- package/src/cli/commands/skill/uninstall.ts +16 -15
- package/src/cli/commands/status.ts +25 -24
- package/src/cli/commands/update.ts +3 -3
- package/src/cli/commands/vsearch.ts +17 -16
- package/src/cli/context.ts +5 -5
- package/src/cli/errors.ts +3 -3
- package/src/cli/format/search-results.ts +37 -37
- package/src/cli/options.ts +43 -43
- package/src/cli/program.ts +455 -459
- package/src/cli/progress.ts +1 -1
- package/src/cli/run.ts +24 -23
- package/src/collection/add.ts +9 -8
- package/src/collection/index.ts +3 -3
- package/src/collection/remove.ts +7 -6
- package/src/collection/types.ts +6 -6
- package/src/config/defaults.ts +1 -1
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +19 -18
- package/src/config/paths.ts +9 -8
- package/src/config/saver.ts +14 -13
- package/src/config/types.ts +53 -52
- package/src/converters/adapters/markitdownTs/adapter.ts +21 -19
- package/src/converters/adapters/officeparser/adapter.ts +18 -16
- package/src/converters/canonicalize.ts +12 -12
- package/src/converters/errors.ts +26 -22
- package/src/converters/index.ts +8 -8
- package/src/converters/mime.ts +25 -25
- package/src/converters/native/markdown.ts +10 -9
- package/src/converters/native/plaintext.ts +8 -7
- package/src/converters/path.ts +2 -2
- package/src/converters/pipeline.ts +11 -10
- package/src/converters/registry.ts +8 -8
- package/src/converters/types.ts +14 -14
- package/src/converters/versions.ts +4 -4
- package/src/index.ts +4 -4
- package/src/ingestion/chunker.ts +10 -9
- package/src/ingestion/index.ts +6 -6
- package/src/ingestion/language.ts +62 -62
- package/src/ingestion/sync.ts +50 -49
- package/src/ingestion/types.ts +10 -10
- package/src/ingestion/walker.ts +14 -13
- package/src/llm/cache.ts +51 -49
- package/src/llm/errors.ts +40 -36
- package/src/llm/index.ts +9 -9
- package/src/llm/lockfile.ts +6 -6
- package/src/llm/nodeLlamaCpp/adapter.ts +13 -12
- package/src/llm/nodeLlamaCpp/embedding.ts +9 -8
- package/src/llm/nodeLlamaCpp/generation.ts +7 -6
- package/src/llm/nodeLlamaCpp/lifecycle.ts +11 -10
- package/src/llm/nodeLlamaCpp/rerank.ts +6 -5
- package/src/llm/policy.ts +5 -5
- package/src/llm/registry.ts +6 -5
- package/src/llm/types.ts +2 -2
- package/src/mcp/resources/index.ts +15 -13
- package/src/mcp/server.ts +25 -23
- package/src/mcp/tools/get.ts +25 -23
- package/src/mcp/tools/index.ts +32 -29
- package/src/mcp/tools/multi-get.ts +34 -32
- package/src/mcp/tools/query.ts +29 -27
- package/src/mcp/tools/search.ts +14 -12
- package/src/mcp/tools/status.ts +12 -11
- package/src/mcp/tools/vsearch.ts +26 -24
- package/src/pipeline/answer.ts +9 -9
- package/src/pipeline/chunk-lookup.ts +1 -1
- package/src/pipeline/contextual.ts +4 -4
- package/src/pipeline/expansion.ts +23 -21
- package/src/pipeline/explain.ts +21 -21
- package/src/pipeline/fusion.ts +9 -9
- package/src/pipeline/hybrid.ts +41 -42
- package/src/pipeline/index.ts +10 -10
- package/src/pipeline/query-language.ts +39 -39
- package/src/pipeline/rerank.ts +8 -7
- package/src/pipeline/search.ts +22 -22
- package/src/pipeline/types.ts +8 -8
- package/src/pipeline/vsearch.ts +21 -24
- package/src/serve/CLAUDE.md +21 -15
- package/src/serve/config-sync.ts +9 -8
- package/src/serve/context.ts +19 -18
- package/src/serve/index.ts +1 -1
- package/src/serve/jobs.ts +7 -7
- package/src/serve/public/app.tsx +79 -25
- package/src/serve/public/components/AddCollectionDialog.tsx +382 -0
- package/src/serve/public/components/CaptureButton.tsx +60 -0
- package/src/serve/public/components/CaptureModal.tsx +365 -0
- package/src/serve/public/components/IndexingProgress.tsx +333 -0
- package/src/serve/public/components/ShortcutHelpModal.tsx +106 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +42 -32
- package/src/serve/public/components/ai-elements/conversation.tsx +16 -14
- package/src/serve/public/components/ai-elements/inline-citation.tsx +33 -32
- package/src/serve/public/components/ai-elements/loader.tsx +5 -4
- package/src/serve/public/components/ai-elements/message.tsx +39 -37
- package/src/serve/public/components/ai-elements/prompt-input.tsx +97 -95
- package/src/serve/public/components/ai-elements/sources.tsx +12 -10
- package/src/serve/public/components/ai-elements/suggestion.tsx +10 -9
- package/src/serve/public/components/editor/CodeMirrorEditor.tsx +142 -0
- package/src/serve/public/components/editor/MarkdownPreview.tsx +311 -0
- package/src/serve/public/components/editor/index.ts +6 -0
- package/src/serve/public/components/preset-selector.tsx +29 -28
- package/src/serve/public/components/ui/badge.tsx +13 -12
- package/src/serve/public/components/ui/button-group.tsx +13 -12
- package/src/serve/public/components/ui/button.tsx +23 -22
- package/src/serve/public/components/ui/card.tsx +16 -16
- package/src/serve/public/components/ui/carousel.tsx +36 -35
- package/src/serve/public/components/ui/collapsible.tsx +1 -1
- package/src/serve/public/components/ui/command.tsx +17 -15
- package/src/serve/public/components/ui/dialog.tsx +13 -12
- package/src/serve/public/components/ui/dropdown-menu.tsx +13 -12
- package/src/serve/public/components/ui/hover-card.tsx +6 -5
- package/src/serve/public/components/ui/input-group.tsx +45 -43
- package/src/serve/public/components/ui/input.tsx +6 -6
- package/src/serve/public/components/ui/progress.tsx +5 -4
- package/src/serve/public/components/ui/scroll-area.tsx +11 -10
- package/src/serve/public/components/ui/select.tsx +19 -18
- package/src/serve/public/components/ui/separator.tsx +6 -5
- package/src/serve/public/components/ui/table.tsx +18 -18
- package/src/serve/public/components/ui/textarea.tsx +4 -4
- package/src/serve/public/components/ui/tooltip.tsx +5 -4
- package/src/serve/public/globals.css +27 -4
- package/src/serve/public/hooks/use-api.ts +8 -8
- package/src/serve/public/hooks/useCaptureModal.tsx +83 -0
- package/src/serve/public/hooks/useKeyboardShortcuts.ts +85 -0
- package/src/serve/public/index.html +4 -4
- package/src/serve/public/lib/utils.ts +6 -0
- package/src/serve/public/pages/Ask.tsx +27 -26
- package/src/serve/public/pages/Browse.tsx +28 -27
- package/src/serve/public/pages/Collections.tsx +439 -0
- package/src/serve/public/pages/Dashboard.tsx +166 -40
- package/src/serve/public/pages/DocView.tsx +258 -73
- package/src/serve/public/pages/DocumentEditor.tsx +510 -0
- package/src/serve/public/pages/Search.tsx +80 -58
- package/src/serve/routes/api.ts +272 -155
- package/src/serve/security.ts +4 -4
- package/src/serve/server.ts +66 -48
- package/src/store/index.ts +5 -5
- package/src/store/migrations/001-initial.ts +24 -23
- package/src/store/migrations/002-documents-fts.ts +7 -6
- package/src/store/migrations/index.ts +4 -4
- package/src/store/migrations/runner.ts +17 -15
- package/src/store/sqlite/adapter.ts +123 -121
- package/src/store/sqlite/fts5-snowball.ts +24 -23
- package/src/store/sqlite/index.ts +1 -1
- package/src/store/sqlite/setup.ts +12 -12
- package/src/store/sqlite/types.ts +4 -4
- package/src/store/types.ts +19 -19
- package/src/store/vector/index.ts +3 -3
- package/src/store/vector/sqlite-vec.ts +23 -20
- package/src/store/vector/stats.ts +10 -8
- package/src/store/vector/types.ts +2 -2
- package/vendor/fts5-snowball/README.md +6 -6
- package/assets/screenshots/webui-ask-answer.jpg +0 -0
- package/assets/screenshots/webui-home.jpg +0 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DocumentEditor - Split-view markdown editor with auto-save.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - CodeMirror 6 for editing
|
|
6
|
+
* - Live markdown preview
|
|
7
|
+
* - Debounced auto-save (2s)
|
|
8
|
+
* - Keyboard shortcuts (Cmd+S to save)
|
|
9
|
+
* - Unsaved changes warning
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
AlertCircleIcon,
|
|
14
|
+
ArrowLeftIcon,
|
|
15
|
+
CheckIcon,
|
|
16
|
+
CloudIcon,
|
|
17
|
+
EyeIcon,
|
|
18
|
+
EyeOffIcon,
|
|
19
|
+
Loader2Icon,
|
|
20
|
+
PenIcon,
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
23
|
+
|
|
24
|
+
import { Loader } from "../components/ai-elements/loader";
|
|
25
|
+
import {
|
|
26
|
+
CodeMirrorEditor,
|
|
27
|
+
type CodeMirrorEditorRef,
|
|
28
|
+
MarkdownPreview,
|
|
29
|
+
} from "../components/editor";
|
|
30
|
+
import { Badge } from "../components/ui/badge";
|
|
31
|
+
import { Button } from "../components/ui/button";
|
|
32
|
+
import {
|
|
33
|
+
Dialog,
|
|
34
|
+
DialogContent,
|
|
35
|
+
DialogDescription,
|
|
36
|
+
DialogFooter,
|
|
37
|
+
DialogHeader,
|
|
38
|
+
DialogTitle,
|
|
39
|
+
} from "../components/ui/dialog";
|
|
40
|
+
import { Separator } from "../components/ui/separator";
|
|
41
|
+
import {
|
|
42
|
+
Tooltip,
|
|
43
|
+
TooltipContent,
|
|
44
|
+
TooltipProvider,
|
|
45
|
+
TooltipTrigger,
|
|
46
|
+
} from "../components/ui/tooltip";
|
|
47
|
+
import { apiFetch } from "../hooks/use-api";
|
|
48
|
+
|
|
49
|
+
interface PageProps {
|
|
50
|
+
navigate: (to: string | number) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface DocData {
|
|
54
|
+
docid: string;
|
|
55
|
+
uri: string;
|
|
56
|
+
title: string | null;
|
|
57
|
+
content: string | null;
|
|
58
|
+
contentAvailable: boolean;
|
|
59
|
+
collection: string;
|
|
60
|
+
relPath: string;
|
|
61
|
+
source: {
|
|
62
|
+
mime: string;
|
|
63
|
+
ext: string;
|
|
64
|
+
modifiedAt?: string;
|
|
65
|
+
sizeBytes?: number;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type SaveStatus = "saved" | "saving" | "unsaved" | "error";
|
|
70
|
+
|
|
71
|
+
function useDebouncedCallback<T extends unknown[]>(
|
|
72
|
+
callback: (...args: T) => void | Promise<void>,
|
|
73
|
+
delay: number
|
|
74
|
+
) {
|
|
75
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
76
|
+
|
|
77
|
+
const debouncedFn = useCallback(
|
|
78
|
+
(...args: T) => {
|
|
79
|
+
if (timeoutRef.current) {
|
|
80
|
+
clearTimeout(timeoutRef.current);
|
|
81
|
+
}
|
|
82
|
+
timeoutRef.current = setTimeout(() => {
|
|
83
|
+
void callback(...args);
|
|
84
|
+
}, delay);
|
|
85
|
+
},
|
|
86
|
+
[callback, delay]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const cancel = useCallback(() => {
|
|
90
|
+
if (timeoutRef.current) {
|
|
91
|
+
clearTimeout(timeoutRef.current);
|
|
92
|
+
timeoutRef.current = null;
|
|
93
|
+
}
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
const flush = useCallback(
|
|
97
|
+
(...args: T) => {
|
|
98
|
+
cancel();
|
|
99
|
+
void callback(...args);
|
|
100
|
+
},
|
|
101
|
+
[callback, cancel]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
return () => {
|
|
106
|
+
if (timeoutRef.current) {
|
|
107
|
+
clearTimeout(timeoutRef.current);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}, []);
|
|
111
|
+
|
|
112
|
+
return { debouncedFn, cancel, flush };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatTime(date: Date): string {
|
|
116
|
+
return date.toLocaleTimeString("en-US", {
|
|
117
|
+
hour: "2-digit",
|
|
118
|
+
minute: "2-digit",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default function DocumentEditor({ navigate }: PageProps) {
|
|
123
|
+
const [doc, setDoc] = useState<DocData | null>(null);
|
|
124
|
+
const [error, setError] = useState<string | null>(null);
|
|
125
|
+
const [loading, setLoading] = useState(true);
|
|
126
|
+
|
|
127
|
+
const [content, setContent] = useState("");
|
|
128
|
+
const [originalContent, setOriginalContent] = useState("");
|
|
129
|
+
const [saveStatus, setSaveStatus] = useState<SaveStatus>("saved");
|
|
130
|
+
const [lastSaved, setLastSaved] = useState<Date | null>(null);
|
|
131
|
+
const [saveError, setSaveError] = useState<string | null>(null);
|
|
132
|
+
|
|
133
|
+
const [showPreview, setShowPreview] = useState(true);
|
|
134
|
+
const [showUnsavedDialog, setShowUnsavedDialog] = useState(false);
|
|
135
|
+
const editorRef = useRef<CodeMirrorEditorRef>(null);
|
|
136
|
+
|
|
137
|
+
const hasUnsavedChanges = content !== originalContent;
|
|
138
|
+
|
|
139
|
+
// Save function
|
|
140
|
+
const saveDocument = useCallback(
|
|
141
|
+
async (contentToSave: string) => {
|
|
142
|
+
if (!doc) return;
|
|
143
|
+
|
|
144
|
+
setSaveStatus("saving");
|
|
145
|
+
setSaveError(null);
|
|
146
|
+
|
|
147
|
+
const { error: err } = await apiFetch(
|
|
148
|
+
`/api/docs/${encodeURIComponent(doc.docid)}`,
|
|
149
|
+
{
|
|
150
|
+
method: "PUT",
|
|
151
|
+
body: JSON.stringify({ content: contentToSave }),
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (err) {
|
|
156
|
+
setSaveStatus("error");
|
|
157
|
+
setSaveError(err);
|
|
158
|
+
} else {
|
|
159
|
+
setSaveStatus("saved");
|
|
160
|
+
setOriginalContent(contentToSave);
|
|
161
|
+
setLastSaved(new Date());
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
[doc]
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// Debounced auto-save
|
|
168
|
+
const { debouncedFn: debouncedSave, flush: flushSave } = useDebouncedCallback(
|
|
169
|
+
saveDocument,
|
|
170
|
+
2000
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Handle content changes
|
|
174
|
+
const handleContentChange = useCallback(
|
|
175
|
+
(newContent: string) => {
|
|
176
|
+
setContent(newContent);
|
|
177
|
+
if (newContent !== originalContent) {
|
|
178
|
+
setSaveStatus("unsaved");
|
|
179
|
+
debouncedSave(newContent);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
[originalContent, debouncedSave]
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Force save (Cmd+S)
|
|
186
|
+
const handleForceSave = useCallback(() => {
|
|
187
|
+
if (hasUnsavedChanges) {
|
|
188
|
+
flushSave(content);
|
|
189
|
+
}
|
|
190
|
+
}, [hasUnsavedChanges, flushSave, content]);
|
|
191
|
+
|
|
192
|
+
// Load document
|
|
193
|
+
useEffect(() => {
|
|
194
|
+
const params = new URLSearchParams(window.location.search);
|
|
195
|
+
const uri = params.get("uri");
|
|
196
|
+
|
|
197
|
+
if (!uri) {
|
|
198
|
+
setError("No document URI provided");
|
|
199
|
+
setLoading(false);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
void apiFetch<DocData>(`/api/doc?uri=${encodeURIComponent(uri)}`).then(
|
|
204
|
+
({ data, error: err }) => {
|
|
205
|
+
setLoading(false);
|
|
206
|
+
if (err) {
|
|
207
|
+
setError(err);
|
|
208
|
+
} else if (data) {
|
|
209
|
+
setDoc(data);
|
|
210
|
+
const docContent = data.content ?? "";
|
|
211
|
+
setContent(docContent);
|
|
212
|
+
setOriginalContent(docContent);
|
|
213
|
+
// Ensure CodeMirror reflects content after async load
|
|
214
|
+
requestAnimationFrame(() => {
|
|
215
|
+
editorRef.current?.setValue(docContent);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}, []);
|
|
221
|
+
|
|
222
|
+
// Keyboard shortcuts
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
225
|
+
const isMeta = e.metaKey || e.ctrlKey;
|
|
226
|
+
|
|
227
|
+
// Cmd+S / Ctrl+S - Save
|
|
228
|
+
if (isMeta && e.key === "s") {
|
|
229
|
+
e.preventDefault();
|
|
230
|
+
handleForceSave();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Editor-specific shortcuts - only when focus is in CodeMirror
|
|
235
|
+
const target = e.target as HTMLElement;
|
|
236
|
+
const inEditor = !!target.closest(".cm-editor");
|
|
237
|
+
|
|
238
|
+
// Cmd+B - Bold (editor only)
|
|
239
|
+
if (isMeta && e.key === "b" && inEditor) {
|
|
240
|
+
e.preventDefault();
|
|
241
|
+
editorRef.current?.wrapSelection("**", "**");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Cmd+I - Italic (editor only)
|
|
246
|
+
if (isMeta && e.key === "i" && inEditor) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
editorRef.current?.wrapSelection("*", "*");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Cmd+K - Link (editor only)
|
|
253
|
+
if (isMeta && e.key === "k" && inEditor) {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
editorRef.current?.wrapSelection("[", "](url)");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Escape - Close (with warning if unsaved)
|
|
260
|
+
if (e.key === "Escape") {
|
|
261
|
+
e.preventDefault();
|
|
262
|
+
if (hasUnsavedChanges) {
|
|
263
|
+
setShowUnsavedDialog(true);
|
|
264
|
+
} else {
|
|
265
|
+
navigate(-1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
271
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
272
|
+
}, [handleForceSave, hasUnsavedChanges, navigate]);
|
|
273
|
+
|
|
274
|
+
// Warn before leaving with unsaved changes
|
|
275
|
+
useEffect(() => {
|
|
276
|
+
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
|
277
|
+
if (hasUnsavedChanges) {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
e.returnValue = ""; // Required for modern browsers
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
284
|
+
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
285
|
+
}, [hasUnsavedChanges]);
|
|
286
|
+
|
|
287
|
+
// Handle close/back
|
|
288
|
+
const handleClose = () => {
|
|
289
|
+
if (hasUnsavedChanges) {
|
|
290
|
+
setShowUnsavedDialog(true);
|
|
291
|
+
} else {
|
|
292
|
+
navigate(-1);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Discard and close
|
|
297
|
+
const handleDiscardAndClose = () => {
|
|
298
|
+
setShowUnsavedDialog(false);
|
|
299
|
+
navigate(-1);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Save and close
|
|
303
|
+
const handleSaveAndClose = async () => {
|
|
304
|
+
await saveDocument(content);
|
|
305
|
+
setShowUnsavedDialog(false);
|
|
306
|
+
navigate(-1);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// Save status indicator
|
|
310
|
+
const SaveStatusIndicator = () => {
|
|
311
|
+
const statusConfig = {
|
|
312
|
+
saved: {
|
|
313
|
+
icon: CheckIcon,
|
|
314
|
+
text: lastSaved ? `Saved at ${formatTime(lastSaved)}` : "Saved",
|
|
315
|
+
className: "text-green-500",
|
|
316
|
+
},
|
|
317
|
+
saving: {
|
|
318
|
+
icon: Loader2Icon,
|
|
319
|
+
text: "Saving...",
|
|
320
|
+
className: "text-muted-foreground animate-spin",
|
|
321
|
+
},
|
|
322
|
+
unsaved: {
|
|
323
|
+
icon: CloudIcon,
|
|
324
|
+
text: "Unsaved changes",
|
|
325
|
+
className: "text-yellow-500",
|
|
326
|
+
},
|
|
327
|
+
error: {
|
|
328
|
+
icon: AlertCircleIcon,
|
|
329
|
+
text: saveError ?? "Save failed",
|
|
330
|
+
className: "text-destructive",
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const { icon: Icon, text, className } = statusConfig[saveStatus];
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<TooltipProvider>
|
|
338
|
+
<Tooltip>
|
|
339
|
+
<TooltipTrigger asChild>
|
|
340
|
+
<div className="flex items-center gap-1.5 text-sm">
|
|
341
|
+
<Icon className={`size-4 ${className}`} />
|
|
342
|
+
<span className="hidden text-muted-foreground sm:inline">
|
|
343
|
+
{saveStatus === "saved" && lastSaved
|
|
344
|
+
? formatTime(lastSaved)
|
|
345
|
+
: text}
|
|
346
|
+
</span>
|
|
347
|
+
</div>
|
|
348
|
+
</TooltipTrigger>
|
|
349
|
+
<TooltipContent>
|
|
350
|
+
<p>{text}</p>
|
|
351
|
+
</TooltipContent>
|
|
352
|
+
</Tooltip>
|
|
353
|
+
</TooltipProvider>
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Loading state
|
|
358
|
+
if (loading) {
|
|
359
|
+
return (
|
|
360
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
361
|
+
<div className="flex flex-col items-center gap-4">
|
|
362
|
+
<Loader className="text-primary" size={32} />
|
|
363
|
+
<p className="text-muted-foreground">Loading document...</p>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Error state
|
|
370
|
+
if (error || !doc) {
|
|
371
|
+
return (
|
|
372
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
373
|
+
<div className="flex flex-col items-center gap-4 text-center">
|
|
374
|
+
<AlertCircleIcon className="size-12 text-destructive" />
|
|
375
|
+
<h2 className="font-semibold text-xl">Failed to load document</h2>
|
|
376
|
+
<p className="text-muted-foreground">{error ?? "Unknown error"}</p>
|
|
377
|
+
<Button onClick={() => navigate(-1)} variant="outline">
|
|
378
|
+
<ArrowLeftIcon className="mr-2 size-4" />
|
|
379
|
+
Go Back
|
|
380
|
+
</Button>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return (
|
|
387
|
+
<div className="flex h-screen flex-col overflow-hidden">
|
|
388
|
+
{/* Toolbar */}
|
|
389
|
+
<header className="glass shrink-0 border-border/50 border-b">
|
|
390
|
+
<div className="flex items-center gap-3 px-4 py-2">
|
|
391
|
+
{/* Back button */}
|
|
392
|
+
<Button
|
|
393
|
+
className="gap-1.5"
|
|
394
|
+
onClick={handleClose}
|
|
395
|
+
size="sm"
|
|
396
|
+
variant="ghost"
|
|
397
|
+
>
|
|
398
|
+
<ArrowLeftIcon className="size-4" />
|
|
399
|
+
<span className="hidden sm:inline">Back</span>
|
|
400
|
+
</Button>
|
|
401
|
+
|
|
402
|
+
<Separator className="h-5" orientation="vertical" />
|
|
403
|
+
|
|
404
|
+
{/* Document info */}
|
|
405
|
+
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
406
|
+
<PenIcon className="size-4 shrink-0 text-muted-foreground" />
|
|
407
|
+
<h1 className="truncate font-medium">{doc.title || doc.relPath}</h1>
|
|
408
|
+
{hasUnsavedChanges && (
|
|
409
|
+
<Badge
|
|
410
|
+
className="shrink-0 bg-yellow-500/20 text-yellow-500"
|
|
411
|
+
variant="outline"
|
|
412
|
+
>
|
|
413
|
+
Unsaved
|
|
414
|
+
</Badge>
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
|
|
418
|
+
{/* Save status */}
|
|
419
|
+
<SaveStatusIndicator />
|
|
420
|
+
|
|
421
|
+
<Separator className="h-5" orientation="vertical" />
|
|
422
|
+
|
|
423
|
+
{/* Preview toggle */}
|
|
424
|
+
<TooltipProvider>
|
|
425
|
+
<Tooltip>
|
|
426
|
+
<TooltipTrigger asChild>
|
|
427
|
+
<Button
|
|
428
|
+
onClick={() => setShowPreview(!showPreview)}
|
|
429
|
+
size="sm"
|
|
430
|
+
variant={showPreview ? "secondary" : "ghost"}
|
|
431
|
+
>
|
|
432
|
+
{showPreview ? (
|
|
433
|
+
<EyeIcon className="size-4" />
|
|
434
|
+
) : (
|
|
435
|
+
<EyeOffIcon className="size-4" />
|
|
436
|
+
)}
|
|
437
|
+
</Button>
|
|
438
|
+
</TooltipTrigger>
|
|
439
|
+
<TooltipContent>
|
|
440
|
+
<p>{showPreview ? "Hide preview" : "Show preview"}</p>
|
|
441
|
+
</TooltipContent>
|
|
442
|
+
</Tooltip>
|
|
443
|
+
</TooltipProvider>
|
|
444
|
+
|
|
445
|
+
{/* Save button */}
|
|
446
|
+
<Button
|
|
447
|
+
disabled={!hasUnsavedChanges || saveStatus === "saving"}
|
|
448
|
+
onClick={handleForceSave}
|
|
449
|
+
size="sm"
|
|
450
|
+
>
|
|
451
|
+
{saveStatus === "saving" ? (
|
|
452
|
+
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
453
|
+
) : (
|
|
454
|
+
<CloudIcon className="mr-1.5 size-4" />
|
|
455
|
+
)}
|
|
456
|
+
Save
|
|
457
|
+
</Button>
|
|
458
|
+
</div>
|
|
459
|
+
</header>
|
|
460
|
+
|
|
461
|
+
{/* Editor area */}
|
|
462
|
+
<div className="flex min-h-0 flex-1">
|
|
463
|
+
{/* Editor pane */}
|
|
464
|
+
<div
|
|
465
|
+
className={`min-h-0 overflow-hidden ${showPreview ? "w-1/2 border-border/30 border-r" : "w-full"}`}
|
|
466
|
+
>
|
|
467
|
+
<CodeMirrorEditor
|
|
468
|
+
className="h-full"
|
|
469
|
+
initialContent={content}
|
|
470
|
+
onChange={handleContentChange}
|
|
471
|
+
ref={editorRef}
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
{/* Preview pane */}
|
|
476
|
+
{showPreview && (
|
|
477
|
+
<div className="w-1/2 min-h-0 overflow-auto bg-background p-6">
|
|
478
|
+
<div className="mx-auto max-w-3xl">
|
|
479
|
+
<MarkdownPreview content={content} />
|
|
480
|
+
</div>
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
{/* Unsaved changes dialog */}
|
|
486
|
+
<Dialog onOpenChange={setShowUnsavedDialog} open={showUnsavedDialog}>
|
|
487
|
+
<DialogContent>
|
|
488
|
+
<DialogHeader>
|
|
489
|
+
<DialogTitle>Unsaved changes</DialogTitle>
|
|
490
|
+
<DialogDescription>
|
|
491
|
+
You have unsaved changes. What would you like to do?
|
|
492
|
+
</DialogDescription>
|
|
493
|
+
</DialogHeader>
|
|
494
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
495
|
+
<Button
|
|
496
|
+
onClick={() => setShowUnsavedDialog(false)}
|
|
497
|
+
variant="outline"
|
|
498
|
+
>
|
|
499
|
+
Cancel
|
|
500
|
+
</Button>
|
|
501
|
+
<Button onClick={handleDiscardAndClose} variant="destructive">
|
|
502
|
+
Discard
|
|
503
|
+
</Button>
|
|
504
|
+
<Button onClick={handleSaveAndClose}>Save & Close</Button>
|
|
505
|
+
</DialogFooter>
|
|
506
|
+
</DialogContent>
|
|
507
|
+
</Dialog>
|
|
508
|
+
</div>
|
|
509
|
+
);
|
|
510
|
+
}
|