@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
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
|
|
3
|
+
import { BookIcon, ChevronDownIcon } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
4
6
|
import {
|
|
5
7
|
Collapsible,
|
|
6
8
|
CollapsibleContent,
|
|
7
9
|
CollapsibleTrigger,
|
|
8
|
-
} from
|
|
10
|
+
} from "../ui/collapsible";
|
|
9
11
|
|
|
10
|
-
export type SourcesProps = ComponentProps<
|
|
12
|
+
export type SourcesProps = ComponentProps<typeof Collapsible>;
|
|
11
13
|
|
|
12
14
|
export const Sources = ({ className, ...props }: SourcesProps) => (
|
|
13
15
|
<Collapsible
|
|
14
|
-
className={cn(
|
|
16
|
+
className={cn("not-prose mb-4 text-primary text-xs", className)}
|
|
15
17
|
{...props}
|
|
16
18
|
/>
|
|
17
19
|
);
|
|
@@ -27,7 +29,7 @@ export const SourcesTrigger = ({
|
|
|
27
29
|
...props
|
|
28
30
|
}: SourcesTriggerProps) => (
|
|
29
31
|
<CollapsibleTrigger
|
|
30
|
-
className={cn(
|
|
32
|
+
className={cn("flex items-center gap-2", className)}
|
|
31
33
|
{...props}
|
|
32
34
|
>
|
|
33
35
|
{children ?? (
|
|
@@ -47,15 +49,15 @@ export const SourcesContent = ({
|
|
|
47
49
|
}: SourcesContentProps) => (
|
|
48
50
|
<CollapsibleContent
|
|
49
51
|
className={cn(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
"mt-3 flex w-fit flex-col gap-2",
|
|
53
|
+
"data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
|
52
54
|
className
|
|
53
55
|
)}
|
|
54
56
|
{...props}
|
|
55
57
|
/>
|
|
56
58
|
);
|
|
57
59
|
|
|
58
|
-
export type SourceProps = ComponentProps<
|
|
60
|
+
export type SourceProps = ComponentProps<"a">;
|
|
59
61
|
|
|
60
62
|
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
|
61
63
|
<a
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { ComponentProps } from
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { Button } from "../ui/button";
|
|
5
|
+
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
|
|
5
6
|
|
|
6
7
|
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
|
7
8
|
|
|
@@ -11,14 +12,14 @@ export const Suggestions = ({
|
|
|
11
12
|
...props
|
|
12
13
|
}: SuggestionsProps) => (
|
|
13
14
|
<ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
|
|
14
|
-
<div className={cn(
|
|
15
|
+
<div className={cn("flex w-max flex-nowrap items-center gap-2", className)}>
|
|
15
16
|
{children}
|
|
16
17
|
</div>
|
|
17
18
|
<ScrollBar className="hidden" orientation="horizontal" />
|
|
18
19
|
</ScrollArea>
|
|
19
20
|
);
|
|
20
21
|
|
|
21
|
-
export type SuggestionProps = Omit<ComponentProps<typeof Button>,
|
|
22
|
+
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
|
|
22
23
|
suggestion: string;
|
|
23
24
|
onClick?: (suggestion: string) => void;
|
|
24
25
|
};
|
|
@@ -27,8 +28,8 @@ export const Suggestion = ({
|
|
|
27
28
|
suggestion,
|
|
28
29
|
onClick,
|
|
29
30
|
className,
|
|
30
|
-
variant =
|
|
31
|
-
size =
|
|
31
|
+
variant = "outline",
|
|
32
|
+
size = "sm",
|
|
32
33
|
children,
|
|
33
34
|
...props
|
|
34
35
|
}: SuggestionProps) => {
|
|
@@ -38,7 +39,7 @@ export const Suggestion = ({
|
|
|
38
39
|
|
|
39
40
|
return (
|
|
40
41
|
<Button
|
|
41
|
-
className={cn(
|
|
42
|
+
className={cn("cursor-pointer rounded-full px-4", className)}
|
|
42
43
|
onClick={handleClick}
|
|
43
44
|
size={size}
|
|
44
45
|
type="button"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeMirror 6 editor wrapper component.
|
|
3
|
+
*
|
|
4
|
+
* Provides markdown editing with syntax highlighting and dark theme.
|
|
5
|
+
* Exposes imperative methods via ref: getValue, setValue, focus.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { markdown } from "@codemirror/lang-markdown";
|
|
9
|
+
import { oneDark } from "@codemirror/theme-one-dark";
|
|
10
|
+
import { EditorView, basicSetup } from "codemirror";
|
|
11
|
+
import {
|
|
12
|
+
forwardRef,
|
|
13
|
+
useEffect,
|
|
14
|
+
useImperativeHandle,
|
|
15
|
+
useRef,
|
|
16
|
+
type ForwardedRef,
|
|
17
|
+
} from "react";
|
|
18
|
+
|
|
19
|
+
export interface CodeMirrorEditorProps {
|
|
20
|
+
/** Initial content to display */
|
|
21
|
+
initialContent: string;
|
|
22
|
+
/** Called when content changes */
|
|
23
|
+
onChange: (content: string) => void;
|
|
24
|
+
/** Additional CSS classes */
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface CodeMirrorEditorRef {
|
|
29
|
+
/** Get current editor content */
|
|
30
|
+
getValue: () => string;
|
|
31
|
+
/** Set editor content programmatically */
|
|
32
|
+
setValue: (content: string) => void;
|
|
33
|
+
/** Focus the editor */
|
|
34
|
+
focus: () => void;
|
|
35
|
+
/** Wrap selected text with prefix and suffix */
|
|
36
|
+
wrapSelection: (prefix: string, suffix: string) => void;
|
|
37
|
+
/** Insert text at cursor position */
|
|
38
|
+
insertAtCursor: (text: string) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function CodeMirrorEditorInner(
|
|
42
|
+
{ initialContent, onChange, className }: CodeMirrorEditorProps,
|
|
43
|
+
ref: ForwardedRef<CodeMirrorEditorRef>
|
|
44
|
+
) {
|
|
45
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
47
|
+
const onChangeRef = useRef(onChange);
|
|
48
|
+
|
|
49
|
+
// Keep onChange ref current to avoid recreating editor on callback change
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
onChangeRef.current = onChange;
|
|
52
|
+
}, [onChange]);
|
|
53
|
+
|
|
54
|
+
// Initialize CodeMirror
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!containerRef.current) return;
|
|
57
|
+
|
|
58
|
+
const view = new EditorView({
|
|
59
|
+
doc: initialContent,
|
|
60
|
+
extensions: [
|
|
61
|
+
basicSetup,
|
|
62
|
+
markdown(),
|
|
63
|
+
oneDark,
|
|
64
|
+
EditorView.lineWrapping,
|
|
65
|
+
EditorView.updateListener.of((update) => {
|
|
66
|
+
if (update.docChanged) {
|
|
67
|
+
onChangeRef.current(update.state.doc.toString());
|
|
68
|
+
}
|
|
69
|
+
}),
|
|
70
|
+
// Dark theme base styling
|
|
71
|
+
EditorView.theme({
|
|
72
|
+
"&": {
|
|
73
|
+
height: "100%",
|
|
74
|
+
},
|
|
75
|
+
".cm-scroller": {
|
|
76
|
+
fontFamily: "ui-monospace, monospace",
|
|
77
|
+
fontSize: "14px",
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
parent: containerRef.current,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
viewRef.current = view;
|
|
85
|
+
return () => view.destroy();
|
|
86
|
+
// Only run on mount - initialContent should not trigger re-creation
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
// Expose imperative methods
|
|
91
|
+
useImperativeHandle(ref, () => ({
|
|
92
|
+
getValue: () => {
|
|
93
|
+
return viewRef.current?.state.doc.toString() ?? "";
|
|
94
|
+
},
|
|
95
|
+
setValue: (content: string) => {
|
|
96
|
+
const view = viewRef.current;
|
|
97
|
+
if (view) {
|
|
98
|
+
view.dispatch({
|
|
99
|
+
changes: { from: 0, to: view.state.doc.length, insert: content },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
focus: () => {
|
|
104
|
+
viewRef.current?.focus();
|
|
105
|
+
},
|
|
106
|
+
wrapSelection: (prefix: string, suffix: string) => {
|
|
107
|
+
const view = viewRef.current;
|
|
108
|
+
if (!view) return;
|
|
109
|
+
|
|
110
|
+
const { from, to } = view.state.selection.main;
|
|
111
|
+
const selectedText = view.state.doc.sliceString(from, to);
|
|
112
|
+
|
|
113
|
+
view.dispatch({
|
|
114
|
+
changes: {
|
|
115
|
+
from,
|
|
116
|
+
to,
|
|
117
|
+
insert: `${prefix}${selectedText}${suffix}`,
|
|
118
|
+
},
|
|
119
|
+
selection: {
|
|
120
|
+
anchor: from + prefix.length,
|
|
121
|
+
head: from + prefix.length + selectedText.length,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
view.focus();
|
|
125
|
+
},
|
|
126
|
+
insertAtCursor: (text: string) => {
|
|
127
|
+
const view = viewRef.current;
|
|
128
|
+
if (!view) return;
|
|
129
|
+
|
|
130
|
+
const pos = view.state.selection.main.head;
|
|
131
|
+
view.dispatch({
|
|
132
|
+
changes: { from: pos, to: pos, insert: text },
|
|
133
|
+
selection: { anchor: pos + text.length },
|
|
134
|
+
});
|
|
135
|
+
view.focus();
|
|
136
|
+
},
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
return <div ref={containerRef} className={className} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const CodeMirrorEditor = forwardRef(CodeMirrorEditorInner);
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownPreview - Renders markdown with syntax highlighting.
|
|
3
|
+
*
|
|
4
|
+
* Uses react-markdown with shiki syntax highlighting via CodeBlock.
|
|
5
|
+
* Matches the "Scholarly Dusk" design system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ComponentProps, FC, ReactNode } from "react";
|
|
9
|
+
import type { BundledLanguage } from "shiki";
|
|
10
|
+
|
|
11
|
+
import { ExternalLinkIcon } from "lucide-react";
|
|
12
|
+
import { memo } from "react";
|
|
13
|
+
import ReactMarkdown from "react-markdown";
|
|
14
|
+
import rehypeSanitize from "rehype-sanitize";
|
|
15
|
+
|
|
16
|
+
import { cn } from "../../lib/utils";
|
|
17
|
+
import { CodeBlock, CodeBlockCopyButton } from "../ai-elements/code-block";
|
|
18
|
+
|
|
19
|
+
export interface MarkdownPreviewProps {
|
|
20
|
+
/** Markdown content to render */
|
|
21
|
+
content: string;
|
|
22
|
+
/** Additional CSS classes */
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Inline code styling
|
|
27
|
+
const InlineCode: FC<ComponentProps<"code">> = ({
|
|
28
|
+
className,
|
|
29
|
+
children,
|
|
30
|
+
...props
|
|
31
|
+
}) => (
|
|
32
|
+
<code
|
|
33
|
+
className={cn(
|
|
34
|
+
"rounded-md bg-muted px-1.5 py-0.5 font-mono text-[0.9em] text-primary",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
>
|
|
39
|
+
{children}
|
|
40
|
+
</code>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Link handling - external links open in new tab
|
|
44
|
+
const Link: FC<ComponentProps<"a">> = ({
|
|
45
|
+
href,
|
|
46
|
+
children,
|
|
47
|
+
className,
|
|
48
|
+
...props
|
|
49
|
+
}) => {
|
|
50
|
+
const isExternal = href?.startsWith("http");
|
|
51
|
+
return (
|
|
52
|
+
<a
|
|
53
|
+
className={cn(
|
|
54
|
+
"text-primary underline decoration-primary/40 underline-offset-2 transition-colors hover:decoration-primary",
|
|
55
|
+
"inline-flex items-center gap-0.5",
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
href={href}
|
|
59
|
+
rel={isExternal ? "noopener noreferrer" : undefined}
|
|
60
|
+
target={isExternal ? "_blank" : undefined}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
{isExternal && <ExternalLinkIcon className="inline size-3 opacity-60" />}
|
|
65
|
+
</a>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Heading styles with proper hierarchy
|
|
70
|
+
const createHeading =
|
|
71
|
+
(level: 1 | 2 | 3 | 4 | 5 | 6): FC<{ children?: ReactNode }> =>
|
|
72
|
+
({ children }) => {
|
|
73
|
+
const Tag = `h${level}` as const;
|
|
74
|
+
const sizes = {
|
|
75
|
+
1: "text-3xl mt-8 mb-4 pb-2 border-b border-border/50",
|
|
76
|
+
2: "text-2xl mt-6 mb-3 pb-1.5 border-b border-border/30",
|
|
77
|
+
3: "text-xl mt-5 mb-2",
|
|
78
|
+
4: "text-lg mt-4 mb-2",
|
|
79
|
+
5: "text-base mt-3 mb-1 font-semibold",
|
|
80
|
+
6: "text-sm mt-3 mb-1 font-semibold text-muted-foreground",
|
|
81
|
+
};
|
|
82
|
+
return (
|
|
83
|
+
<Tag className={cn("font-serif tracking-tight", sizes[level])}>
|
|
84
|
+
{children}
|
|
85
|
+
</Tag>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Code block with syntax highlighting
|
|
90
|
+
const Pre: FC<ComponentProps<"pre">> = ({ children, ...props }) => {
|
|
91
|
+
// Extract code element from children
|
|
92
|
+
const codeElement = children as React.ReactElement<{
|
|
93
|
+
className?: string;
|
|
94
|
+
children?: string;
|
|
95
|
+
}>;
|
|
96
|
+
|
|
97
|
+
if (!codeElement?.props) {
|
|
98
|
+
return <pre {...props}>{children}</pre>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const className = codeElement.props.className ?? "";
|
|
102
|
+
const code = String(codeElement.props.children ?? "").trim();
|
|
103
|
+
|
|
104
|
+
// Extract language from className (e.g., "language-typescript")
|
|
105
|
+
const match = /language-(\w+)/.exec(className);
|
|
106
|
+
const language = (match?.[1] ?? "plaintext") as BundledLanguage;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="group/code my-4">
|
|
110
|
+
<CodeBlock
|
|
111
|
+
className="rounded-lg border border-border/60 bg-muted/30"
|
|
112
|
+
code={code}
|
|
113
|
+
language={language}
|
|
114
|
+
>
|
|
115
|
+
<CodeBlockCopyButton
|
|
116
|
+
className="opacity-0 transition-opacity group-hover/code:opacity-100"
|
|
117
|
+
size="icon-sm"
|
|
118
|
+
variant="ghost"
|
|
119
|
+
/>
|
|
120
|
+
</CodeBlock>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Blockquote with refined styling
|
|
126
|
+
const Blockquote: FC<ComponentProps<"blockquote">> = ({
|
|
127
|
+
children,
|
|
128
|
+
className,
|
|
129
|
+
...props
|
|
130
|
+
}) => (
|
|
131
|
+
<blockquote
|
|
132
|
+
className={cn(
|
|
133
|
+
"my-4 border-l-2 border-secondary/60 bg-secondary/5 py-2 pr-4 pl-4 italic text-muted-foreground",
|
|
134
|
+
"[&>p]:mb-0",
|
|
135
|
+
className
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</blockquote>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// List styles
|
|
144
|
+
const UnorderedList: FC<ComponentProps<"ul">> = ({
|
|
145
|
+
children,
|
|
146
|
+
className,
|
|
147
|
+
...props
|
|
148
|
+
}) => (
|
|
149
|
+
<ul
|
|
150
|
+
className={cn("my-3 ml-6 list-disc space-y-1 [&>li]:pl-1", className)}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
{children}
|
|
154
|
+
</ul>
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const OrderedList: FC<ComponentProps<"ol">> = ({
|
|
158
|
+
children,
|
|
159
|
+
className,
|
|
160
|
+
...props
|
|
161
|
+
}) => (
|
|
162
|
+
<ol
|
|
163
|
+
className={cn("my-3 ml-6 list-decimal space-y-1 [&>li]:pl-1", className)}
|
|
164
|
+
{...props}
|
|
165
|
+
>
|
|
166
|
+
{children}
|
|
167
|
+
</ol>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Table styles
|
|
171
|
+
const Table: FC<ComponentProps<"table">> = ({
|
|
172
|
+
children,
|
|
173
|
+
className,
|
|
174
|
+
...props
|
|
175
|
+
}) => (
|
|
176
|
+
<div className="my-4 overflow-x-auto rounded-lg border border-border/60">
|
|
177
|
+
<table
|
|
178
|
+
className={cn("w-full border-collapse text-sm", className)}
|
|
179
|
+
{...props}
|
|
180
|
+
>
|
|
181
|
+
{children}
|
|
182
|
+
</table>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const TableHead: FC<ComponentProps<"thead">> = ({
|
|
187
|
+
children,
|
|
188
|
+
className,
|
|
189
|
+
...props
|
|
190
|
+
}) => (
|
|
191
|
+
<thead className={cn("bg-muted/50", className)} {...props}>
|
|
192
|
+
{children}
|
|
193
|
+
</thead>
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const TableRow: FC<ComponentProps<"tr">> = ({
|
|
197
|
+
children,
|
|
198
|
+
className,
|
|
199
|
+
...props
|
|
200
|
+
}) => (
|
|
201
|
+
<tr
|
|
202
|
+
className={cn("border-b border-border/40 last:border-0", className)}
|
|
203
|
+
{...props}
|
|
204
|
+
>
|
|
205
|
+
{children}
|
|
206
|
+
</tr>
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const TableCell: FC<ComponentProps<"td">> = ({
|
|
210
|
+
children,
|
|
211
|
+
className,
|
|
212
|
+
...props
|
|
213
|
+
}) => (
|
|
214
|
+
<td className={cn("px-4 py-2", className)} {...props}>
|
|
215
|
+
{children}
|
|
216
|
+
</td>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const TableHeaderCell: FC<ComponentProps<"th">> = ({
|
|
220
|
+
children,
|
|
221
|
+
className,
|
|
222
|
+
...props
|
|
223
|
+
}) => (
|
|
224
|
+
<th className={cn("px-4 py-2 text-left font-semibold", className)} {...props}>
|
|
225
|
+
{children}
|
|
226
|
+
</th>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Horizontal rule
|
|
230
|
+
const Hr: FC = () => <hr className="my-6 border-0 border-t border-border/50" />;
|
|
231
|
+
|
|
232
|
+
// Paragraph
|
|
233
|
+
const Paragraph: FC<ComponentProps<"p">> = ({
|
|
234
|
+
children,
|
|
235
|
+
className,
|
|
236
|
+
...props
|
|
237
|
+
}) => (
|
|
238
|
+
<p className={cn("mb-4 leading-relaxed last:mb-0", className)} {...props}>
|
|
239
|
+
{children}
|
|
240
|
+
</p>
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Image with proper styling
|
|
244
|
+
const Image: FC<ComponentProps<"img">> = ({ alt, className, ...props }) => (
|
|
245
|
+
// Alt is already passed via props spread, explicit declaration for required attr
|
|
246
|
+
<img
|
|
247
|
+
alt={alt ?? ""}
|
|
248
|
+
className={cn(
|
|
249
|
+
"my-4 max-w-full rounded-lg border border-border/40",
|
|
250
|
+
className
|
|
251
|
+
)}
|
|
252
|
+
{...props}
|
|
253
|
+
/>
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Component mapping for react-markdown
|
|
257
|
+
const components = {
|
|
258
|
+
h1: createHeading(1),
|
|
259
|
+
h2: createHeading(2),
|
|
260
|
+
h3: createHeading(3),
|
|
261
|
+
h4: createHeading(4),
|
|
262
|
+
h5: createHeading(5),
|
|
263
|
+
h6: createHeading(6),
|
|
264
|
+
p: Paragraph,
|
|
265
|
+
a: Link,
|
|
266
|
+
code: InlineCode,
|
|
267
|
+
pre: Pre,
|
|
268
|
+
blockquote: Blockquote,
|
|
269
|
+
ul: UnorderedList,
|
|
270
|
+
ol: OrderedList,
|
|
271
|
+
table: Table,
|
|
272
|
+
thead: TableHead,
|
|
273
|
+
tr: TableRow,
|
|
274
|
+
td: TableCell,
|
|
275
|
+
th: TableHeaderCell,
|
|
276
|
+
hr: Hr,
|
|
277
|
+
img: Image,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Renders markdown content with syntax highlighting and proper styling.
|
|
282
|
+
* Sanitizes HTML to prevent XSS attacks.
|
|
283
|
+
*/
|
|
284
|
+
export const MarkdownPreview = memo(
|
|
285
|
+
({ content, className }: MarkdownPreviewProps) => {
|
|
286
|
+
if (!content) {
|
|
287
|
+
return (
|
|
288
|
+
<div className={cn("text-muted-foreground italic", className)}>
|
|
289
|
+
No content to display
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div
|
|
296
|
+
className={cn(
|
|
297
|
+
"prose prose-invert max-w-none",
|
|
298
|
+
"text-foreground/90",
|
|
299
|
+
"[&>*:first-child]:mt-0",
|
|
300
|
+
className
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
<ReactMarkdown components={components} rehypePlugins={[rehypeSanitize]}>
|
|
304
|
+
{content}
|
|
305
|
+
</ReactMarkdown>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
MarkdownPreview.displayName = "MarkdownPreview";
|