@gmickel/gno 0.3.5 → 0.5.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/README.md +74 -7
- package/package.json +30 -1
- package/src/cli/commands/ask.ts +12 -187
- package/src/cli/commands/embed.ts +10 -4
- package/src/cli/commands/models/pull.ts +9 -4
- package/src/cli/commands/serve.ts +19 -0
- package/src/cli/commands/vsearch.ts +5 -2
- package/src/cli/program.ts +28 -0
- package/src/config/types.ts +11 -6
- package/src/llm/registry.ts +3 -1
- package/src/mcp/tools/vsearch.ts +5 -2
- package/src/pipeline/answer.ts +224 -0
- package/src/pipeline/contextual.ts +57 -0
- package/src/pipeline/expansion.ts +49 -31
- package/src/pipeline/explain.ts +11 -3
- package/src/pipeline/fusion.ts +20 -9
- package/src/pipeline/hybrid.ts +57 -40
- package/src/pipeline/index.ts +7 -0
- package/src/pipeline/rerank.ts +55 -27
- package/src/pipeline/types.ts +0 -3
- package/src/pipeline/vsearch.ts +3 -2
- package/src/serve/CLAUDE.md +91 -0
- package/src/serve/bunfig.toml +2 -0
- package/src/serve/context.ts +181 -0
- package/src/serve/index.ts +7 -0
- package/src/serve/public/app.tsx +56 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +176 -0
- package/src/serve/public/components/ai-elements/conversation.tsx +98 -0
- package/src/serve/public/components/ai-elements/inline-citation.tsx +285 -0
- package/src/serve/public/components/ai-elements/loader.tsx +96 -0
- package/src/serve/public/components/ai-elements/message.tsx +443 -0
- package/src/serve/public/components/ai-elements/prompt-input.tsx +1421 -0
- package/src/serve/public/components/ai-elements/sources.tsx +75 -0
- package/src/serve/public/components/ai-elements/suggestion.tsx +51 -0
- package/src/serve/public/components/preset-selector.tsx +403 -0
- package/src/serve/public/components/ui/badge.tsx +46 -0
- package/src/serve/public/components/ui/button-group.tsx +82 -0
- package/src/serve/public/components/ui/button.tsx +62 -0
- package/src/serve/public/components/ui/card.tsx +92 -0
- package/src/serve/public/components/ui/carousel.tsx +244 -0
- package/src/serve/public/components/ui/collapsible.tsx +31 -0
- package/src/serve/public/components/ui/command.tsx +181 -0
- package/src/serve/public/components/ui/dialog.tsx +141 -0
- package/src/serve/public/components/ui/dropdown-menu.tsx +255 -0
- package/src/serve/public/components/ui/hover-card.tsx +42 -0
- package/src/serve/public/components/ui/input-group.tsx +167 -0
- package/src/serve/public/components/ui/input.tsx +21 -0
- package/src/serve/public/components/ui/progress.tsx +28 -0
- package/src/serve/public/components/ui/scroll-area.tsx +56 -0
- package/src/serve/public/components/ui/select.tsx +188 -0
- package/src/serve/public/components/ui/separator.tsx +26 -0
- package/src/serve/public/components/ui/table.tsx +114 -0
- package/src/serve/public/components/ui/textarea.tsx +18 -0
- package/src/serve/public/components/ui/tooltip.tsx +59 -0
- package/src/serve/public/globals.css +226 -0
- package/src/serve/public/hooks/use-api.ts +112 -0
- package/src/serve/public/index.html +13 -0
- package/src/serve/public/pages/Ask.tsx +442 -0
- package/src/serve/public/pages/Browse.tsx +270 -0
- package/src/serve/public/pages/Dashboard.tsx +202 -0
- package/src/serve/public/pages/DocView.tsx +302 -0
- package/src/serve/public/pages/Search.tsx +335 -0
- package/src/serve/routes/api.ts +763 -0
- package/src/serve/server.ts +249 -0
- package/src/store/migrations/002-documents-fts.ts +40 -0
- package/src/store/migrations/index.ts +2 -1
- package/src/store/sqlite/adapter.ts +216 -33
- package/src/store/sqlite/fts5-snowball.ts +144 -0
- package/src/store/types.ts +33 -3
- package/src/store/vector/stats.ts +3 -0
- package/src/store/vector/types.ts +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import Ask from './pages/Ask';
|
|
4
|
+
import Browse from './pages/Browse';
|
|
5
|
+
import Dashboard from './pages/Dashboard';
|
|
6
|
+
import DocView from './pages/DocView';
|
|
7
|
+
import Search from './pages/Search';
|
|
8
|
+
import './globals.css';
|
|
9
|
+
|
|
10
|
+
type Route = '/' | '/search' | '/browse' | '/doc' | '/ask';
|
|
11
|
+
type Navigate = (to: string | number) => void;
|
|
12
|
+
|
|
13
|
+
const routes: Record<Route, React.ComponentType<{ navigate: Navigate }>> = {
|
|
14
|
+
'/': Dashboard,
|
|
15
|
+
'/search': Search,
|
|
16
|
+
'/browse': Browse,
|
|
17
|
+
'/doc': DocView,
|
|
18
|
+
'/ask': Ask,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function App() {
|
|
22
|
+
// Track full location (pathname + search) for proper query param handling
|
|
23
|
+
const [location, setLocation] = useState<string>(
|
|
24
|
+
window.location.pathname + window.location.search
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handlePopState = () =>
|
|
29
|
+
setLocation(window.location.pathname + window.location.search);
|
|
30
|
+
window.addEventListener('popstate', handlePopState);
|
|
31
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
const navigate = (to: string | number) => {
|
|
35
|
+
if (typeof to === 'number') {
|
|
36
|
+
// Handle history.go(-1) style navigation
|
|
37
|
+
window.history.go(to);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
window.history.pushState({}, '', to);
|
|
41
|
+
setLocation(to);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Extract base path for routing (ignore query params)
|
|
45
|
+
const basePath = location.split('?')[0] as Route;
|
|
46
|
+
const Page = routes[basePath] || Dashboard;
|
|
47
|
+
|
|
48
|
+
return <Page navigate={navigate} />;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rootElement = document.getElementById('root');
|
|
52
|
+
if (!rootElement) {
|
|
53
|
+
throw new Error('Root element not found');
|
|
54
|
+
}
|
|
55
|
+
const root = createRoot(rootElement);
|
|
56
|
+
root.render(<App />);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { CheckIcon, CopyIcon } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
type ComponentProps,
|
|
4
|
+
createContext,
|
|
5
|
+
type HTMLAttributes,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from 'react';
|
|
11
|
+
import { type BundledLanguage, codeToHtml, type ShikiTransformer } from 'shiki';
|
|
12
|
+
import { cn } from '../../lib/utils';
|
|
13
|
+
import { Button } from '../ui/button';
|
|
14
|
+
|
|
15
|
+
type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
|
|
16
|
+
code: string;
|
|
17
|
+
language: BundledLanguage;
|
|
18
|
+
showLineNumbers?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface CodeBlockContextType {
|
|
22
|
+
code: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const CodeBlockContext = createContext<CodeBlockContextType>({
|
|
26
|
+
code: '',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const lineNumberTransformer: ShikiTransformer = {
|
|
30
|
+
name: 'line-numbers',
|
|
31
|
+
line(node, line) {
|
|
32
|
+
node.children.unshift({
|
|
33
|
+
type: 'element',
|
|
34
|
+
tagName: 'span',
|
|
35
|
+
properties: {
|
|
36
|
+
className: [
|
|
37
|
+
'inline-block',
|
|
38
|
+
'min-w-10',
|
|
39
|
+
'mr-4',
|
|
40
|
+
'text-right',
|
|
41
|
+
'select-none',
|
|
42
|
+
'text-muted-foreground',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
children: [{ type: 'text', value: String(line) }],
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export async function highlightCode(
|
|
51
|
+
code: string,
|
|
52
|
+
language: BundledLanguage,
|
|
53
|
+
showLineNumbers = false
|
|
54
|
+
) {
|
|
55
|
+
const transformers: ShikiTransformer[] = showLineNumbers
|
|
56
|
+
? [lineNumberTransformer]
|
|
57
|
+
: [];
|
|
58
|
+
|
|
59
|
+
return await Promise.all([
|
|
60
|
+
codeToHtml(code, {
|
|
61
|
+
lang: language,
|
|
62
|
+
theme: 'one-light',
|
|
63
|
+
transformers,
|
|
64
|
+
}),
|
|
65
|
+
codeToHtml(code, {
|
|
66
|
+
lang: language,
|
|
67
|
+
theme: 'one-dark-pro',
|
|
68
|
+
transformers,
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const CodeBlock = ({
|
|
74
|
+
code,
|
|
75
|
+
language,
|
|
76
|
+
showLineNumbers = false,
|
|
77
|
+
className,
|
|
78
|
+
children,
|
|
79
|
+
...props
|
|
80
|
+
}: CodeBlockProps) => {
|
|
81
|
+
const [html, setHtml] = useState<string>('');
|
|
82
|
+
const [darkHtml, setDarkHtml] = useState<string>('');
|
|
83
|
+
const mounted = useRef(false);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
highlightCode(code, language, showLineNumbers).then(([light, dark]) => {
|
|
87
|
+
if (!mounted.current) {
|
|
88
|
+
setHtml(light);
|
|
89
|
+
setDarkHtml(dark);
|
|
90
|
+
mounted.current = true;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
mounted.current = false;
|
|
96
|
+
};
|
|
97
|
+
}, [code, language, showLineNumbers]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<CodeBlockContext.Provider value={{ code }}>
|
|
101
|
+
<div
|
|
102
|
+
className={cn(
|
|
103
|
+
'group relative w-full overflow-hidden rounded-md border bg-background text-foreground',
|
|
104
|
+
className
|
|
105
|
+
)}
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
<div className="relative">
|
|
109
|
+
<div
|
|
110
|
+
className="overflow-auto dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
|
|
111
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
|
112
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
113
|
+
/>
|
|
114
|
+
<div
|
|
115
|
+
className="hidden overflow-auto dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"
|
|
116
|
+
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
|
|
117
|
+
dangerouslySetInnerHTML={{ __html: darkHtml }}
|
|
118
|
+
/>
|
|
119
|
+
{children && (
|
|
120
|
+
<div className="absolute top-2 right-2 flex items-center gap-2">
|
|
121
|
+
{children}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</CodeBlockContext.Provider>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
|
|
131
|
+
onCopy?: () => void;
|
|
132
|
+
onError?: (error: Error) => void;
|
|
133
|
+
timeout?: number;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const CodeBlockCopyButton = ({
|
|
137
|
+
onCopy,
|
|
138
|
+
onError,
|
|
139
|
+
timeout = 2000,
|
|
140
|
+
children,
|
|
141
|
+
className,
|
|
142
|
+
...props
|
|
143
|
+
}: CodeBlockCopyButtonProps) => {
|
|
144
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
145
|
+
const { code } = useContext(CodeBlockContext);
|
|
146
|
+
|
|
147
|
+
const copyToClipboard = async () => {
|
|
148
|
+
if (typeof window === 'undefined' || !navigator?.clipboard?.writeText) {
|
|
149
|
+
onError?.(new Error('Clipboard API not available'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
await navigator.clipboard.writeText(code);
|
|
155
|
+
setIsCopied(true);
|
|
156
|
+
onCopy?.();
|
|
157
|
+
setTimeout(() => setIsCopied(false), timeout);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
onError?.(error as Error);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const Icon = isCopied ? CheckIcon : CopyIcon;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Button
|
|
167
|
+
className={cn('shrink-0', className)}
|
|
168
|
+
onClick={copyToClipboard}
|
|
169
|
+
size="icon"
|
|
170
|
+
variant="ghost"
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
{children ?? <Icon size={14} />}
|
|
174
|
+
</Button>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ArrowDownIcon } from 'lucide-react';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
import { StickToBottom, useStickToBottomContext } from 'use-stick-to-bottom';
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
|
|
8
|
+
export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
|
9
|
+
|
|
10
|
+
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
|
11
|
+
<StickToBottom
|
|
12
|
+
className={cn('relative flex-1 overflow-y-hidden', className)}
|
|
13
|
+
initial="smooth"
|
|
14
|
+
resize="smooth"
|
|
15
|
+
role="log"
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export type ConversationContentProps = ComponentProps<
|
|
21
|
+
typeof StickToBottom.Content
|
|
22
|
+
>;
|
|
23
|
+
|
|
24
|
+
export const ConversationContent = ({
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}: ConversationContentProps) => (
|
|
28
|
+
<StickToBottom.Content
|
|
29
|
+
className={cn('flex flex-col gap-8 p-4', className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export type ConversationEmptyStateProps = ComponentProps<'div'> & {
|
|
35
|
+
title?: string;
|
|
36
|
+
description?: string;
|
|
37
|
+
icon?: React.ReactNode;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const ConversationEmptyState = ({
|
|
41
|
+
className,
|
|
42
|
+
title = 'No messages yet',
|
|
43
|
+
description = 'Start a conversation to see messages here',
|
|
44
|
+
icon,
|
|
45
|
+
children,
|
|
46
|
+
...props
|
|
47
|
+
}: ConversationEmptyStateProps) => (
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
'flex size-full flex-col items-center justify-center gap-3 p-8 text-center',
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children ?? (
|
|
56
|
+
<>
|
|
57
|
+
{icon && <div className="text-muted-foreground">{icon}</div>}
|
|
58
|
+
<div className="space-y-1">
|
|
59
|
+
<h3 className="font-medium text-sm">{title}</h3>
|
|
60
|
+
{description && (
|
|
61
|
+
<p className="text-muted-foreground text-sm">{description}</p>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export type ConversationScrollButtonProps = ComponentProps<typeof Button>;
|
|
70
|
+
|
|
71
|
+
export const ConversationScrollButton = ({
|
|
72
|
+
className,
|
|
73
|
+
...props
|
|
74
|
+
}: ConversationScrollButtonProps) => {
|
|
75
|
+
const { isAtBottom, scrollToBottom } = useStickToBottomContext();
|
|
76
|
+
|
|
77
|
+
const handleScrollToBottom = useCallback(() => {
|
|
78
|
+
scrollToBottom();
|
|
79
|
+
}, [scrollToBottom]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
!isAtBottom && (
|
|
83
|
+
<Button
|
|
84
|
+
className={cn(
|
|
85
|
+
'absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full',
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
onClick={handleScrollToBottom}
|
|
89
|
+
size="icon"
|
|
90
|
+
type="button"
|
|
91
|
+
variant="outline"
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
<ArrowDownIcon className="size-4" />
|
|
95
|
+
</Button>
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { ArrowLeftIcon, ArrowRightIcon } from 'lucide-react';
|
|
2
|
+
import {
|
|
3
|
+
type ComponentProps,
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useState,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import { cn } from '../../lib/utils';
|
|
11
|
+
import { Badge } from '../ui/badge';
|
|
12
|
+
import {
|
|
13
|
+
Carousel,
|
|
14
|
+
type CarouselApi,
|
|
15
|
+
CarouselContent,
|
|
16
|
+
CarouselItem,
|
|
17
|
+
} from '../ui/carousel';
|
|
18
|
+
import {
|
|
19
|
+
HoverCard,
|
|
20
|
+
HoverCardContent,
|
|
21
|
+
HoverCardTrigger,
|
|
22
|
+
} from '../ui/hover-card';
|
|
23
|
+
|
|
24
|
+
export type InlineCitationProps = ComponentProps<'span'>;
|
|
25
|
+
|
|
26
|
+
export const InlineCitation = ({
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: InlineCitationProps) => (
|
|
30
|
+
<span
|
|
31
|
+
className={cn('group inline items-center gap-1', className)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export type InlineCitationTextProps = ComponentProps<'span'>;
|
|
37
|
+
|
|
38
|
+
export const InlineCitationText = ({
|
|
39
|
+
className,
|
|
40
|
+
...props
|
|
41
|
+
}: InlineCitationTextProps) => (
|
|
42
|
+
<span
|
|
43
|
+
className={cn('transition-colors group-hover:bg-accent', className)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export type InlineCitationCardProps = ComponentProps<typeof HoverCard>;
|
|
49
|
+
|
|
50
|
+
export const InlineCitationCard = (props: InlineCitationCardProps) => (
|
|
51
|
+
<HoverCard closeDelay={0} openDelay={0} {...props} />
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export type InlineCitationCardTriggerProps = ComponentProps<typeof Badge> & {
|
|
55
|
+
sources: string[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const InlineCitationCardTrigger = ({
|
|
59
|
+
sources,
|
|
60
|
+
className,
|
|
61
|
+
...props
|
|
62
|
+
}: InlineCitationCardTriggerProps) => (
|
|
63
|
+
<HoverCardTrigger asChild>
|
|
64
|
+
<Badge
|
|
65
|
+
className={cn('ml-1 rounded-full', className)}
|
|
66
|
+
variant="secondary"
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{sources[0] ? (
|
|
70
|
+
<>
|
|
71
|
+
{new URL(sources[0]).hostname}{' '}
|
|
72
|
+
{sources.length > 1 && `+${sources.length - 1}`}
|
|
73
|
+
</>
|
|
74
|
+
) : (
|
|
75
|
+
'unknown'
|
|
76
|
+
)}
|
|
77
|
+
</Badge>
|
|
78
|
+
</HoverCardTrigger>
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export type InlineCitationCardBodyProps = ComponentProps<'div'>;
|
|
82
|
+
|
|
83
|
+
export const InlineCitationCardBody = ({
|
|
84
|
+
className,
|
|
85
|
+
...props
|
|
86
|
+
}: InlineCitationCardBodyProps) => (
|
|
87
|
+
<HoverCardContent className={cn('relative w-80 p-0', className)} {...props} />
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const CarouselApiContext = createContext<CarouselApi | undefined>(undefined);
|
|
91
|
+
|
|
92
|
+
const useCarouselApi = () => {
|
|
93
|
+
const context = useContext(CarouselApiContext);
|
|
94
|
+
return context;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type InlineCitationCarouselProps = ComponentProps<typeof Carousel>;
|
|
98
|
+
|
|
99
|
+
export const InlineCitationCarousel = ({
|
|
100
|
+
className,
|
|
101
|
+
children,
|
|
102
|
+
...props
|
|
103
|
+
}: InlineCitationCarouselProps) => {
|
|
104
|
+
const [api, setApi] = useState<CarouselApi>();
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<CarouselApiContext.Provider value={api}>
|
|
108
|
+
<Carousel className={cn('w-full', className)} setApi={setApi} {...props}>
|
|
109
|
+
{children}
|
|
110
|
+
</Carousel>
|
|
111
|
+
</CarouselApiContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export type InlineCitationCarouselContentProps = ComponentProps<'div'>;
|
|
116
|
+
|
|
117
|
+
export const InlineCitationCarouselContent = (
|
|
118
|
+
props: InlineCitationCarouselContentProps
|
|
119
|
+
) => <CarouselContent {...props} />;
|
|
120
|
+
|
|
121
|
+
export type InlineCitationCarouselItemProps = ComponentProps<'div'>;
|
|
122
|
+
|
|
123
|
+
export const InlineCitationCarouselItem = ({
|
|
124
|
+
className,
|
|
125
|
+
...props
|
|
126
|
+
}: InlineCitationCarouselItemProps) => (
|
|
127
|
+
<CarouselItem
|
|
128
|
+
className={cn('w-full space-y-2 p-4 pl-8', className)}
|
|
129
|
+
{...props}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
export type InlineCitationCarouselHeaderProps = ComponentProps<'div'>;
|
|
134
|
+
|
|
135
|
+
export const InlineCitationCarouselHeader = ({
|
|
136
|
+
className,
|
|
137
|
+
...props
|
|
138
|
+
}: InlineCitationCarouselHeaderProps) => (
|
|
139
|
+
<div
|
|
140
|
+
className={cn(
|
|
141
|
+
'flex items-center justify-between gap-2 rounded-t-md bg-secondary p-2',
|
|
142
|
+
className
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
export type InlineCitationCarouselIndexProps = ComponentProps<'div'>;
|
|
149
|
+
|
|
150
|
+
export const InlineCitationCarouselIndex = ({
|
|
151
|
+
children,
|
|
152
|
+
className,
|
|
153
|
+
...props
|
|
154
|
+
}: InlineCitationCarouselIndexProps) => {
|
|
155
|
+
const api = useCarouselApi();
|
|
156
|
+
const [current, setCurrent] = useState(0);
|
|
157
|
+
const [count, setCount] = useState(0);
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!api) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setCount(api.scrollSnapList().length);
|
|
165
|
+
setCurrent(api.selectedScrollSnap() + 1);
|
|
166
|
+
|
|
167
|
+
api.on('select', () => {
|
|
168
|
+
setCurrent(api.selectedScrollSnap() + 1);
|
|
169
|
+
});
|
|
170
|
+
}, [api]);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div
|
|
174
|
+
className={cn(
|
|
175
|
+
'flex flex-1 items-center justify-end px-3 py-1 text-muted-foreground text-xs',
|
|
176
|
+
className
|
|
177
|
+
)}
|
|
178
|
+
{...props}
|
|
179
|
+
>
|
|
180
|
+
{children ?? `${current}/${count}`}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export type InlineCitationCarouselPrevProps = ComponentProps<'button'>;
|
|
186
|
+
|
|
187
|
+
export const InlineCitationCarouselPrev = ({
|
|
188
|
+
className,
|
|
189
|
+
...props
|
|
190
|
+
}: InlineCitationCarouselPrevProps) => {
|
|
191
|
+
const api = useCarouselApi();
|
|
192
|
+
|
|
193
|
+
const handleClick = useCallback(() => {
|
|
194
|
+
if (api) {
|
|
195
|
+
api.scrollPrev();
|
|
196
|
+
}
|
|
197
|
+
}, [api]);
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<button
|
|
201
|
+
aria-label="Previous"
|
|
202
|
+
className={cn('shrink-0', className)}
|
|
203
|
+
onClick={handleClick}
|
|
204
|
+
type="button"
|
|
205
|
+
{...props}
|
|
206
|
+
>
|
|
207
|
+
<ArrowLeftIcon className="size-4 text-muted-foreground" />
|
|
208
|
+
</button>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export type InlineCitationCarouselNextProps = ComponentProps<'button'>;
|
|
213
|
+
|
|
214
|
+
export const InlineCitationCarouselNext = ({
|
|
215
|
+
className,
|
|
216
|
+
...props
|
|
217
|
+
}: InlineCitationCarouselNextProps) => {
|
|
218
|
+
const api = useCarouselApi();
|
|
219
|
+
|
|
220
|
+
const handleClick = useCallback(() => {
|
|
221
|
+
if (api) {
|
|
222
|
+
api.scrollNext();
|
|
223
|
+
}
|
|
224
|
+
}, [api]);
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<button
|
|
228
|
+
aria-label="Next"
|
|
229
|
+
className={cn('shrink-0', className)}
|
|
230
|
+
onClick={handleClick}
|
|
231
|
+
type="button"
|
|
232
|
+
{...props}
|
|
233
|
+
>
|
|
234
|
+
<ArrowRightIcon className="size-4 text-muted-foreground" />
|
|
235
|
+
</button>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export type InlineCitationSourceProps = ComponentProps<'div'> & {
|
|
240
|
+
title?: string;
|
|
241
|
+
url?: string;
|
|
242
|
+
description?: string;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
export const InlineCitationSource = ({
|
|
246
|
+
title,
|
|
247
|
+
url,
|
|
248
|
+
description,
|
|
249
|
+
className,
|
|
250
|
+
children,
|
|
251
|
+
...props
|
|
252
|
+
}: InlineCitationSourceProps) => (
|
|
253
|
+
<div className={cn('space-y-1', className)} {...props}>
|
|
254
|
+
{title && (
|
|
255
|
+
<h4 className="truncate font-medium text-sm leading-tight">{title}</h4>
|
|
256
|
+
)}
|
|
257
|
+
{url && (
|
|
258
|
+
<p className="truncate break-all text-muted-foreground text-xs">{url}</p>
|
|
259
|
+
)}
|
|
260
|
+
{description && (
|
|
261
|
+
<p className="line-clamp-3 text-muted-foreground text-sm leading-relaxed">
|
|
262
|
+
{description}
|
|
263
|
+
</p>
|
|
264
|
+
)}
|
|
265
|
+
{children}
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
export type InlineCitationQuoteProps = ComponentProps<'blockquote'>;
|
|
270
|
+
|
|
271
|
+
export const InlineCitationQuote = ({
|
|
272
|
+
children,
|
|
273
|
+
className,
|
|
274
|
+
...props
|
|
275
|
+
}: InlineCitationQuoteProps) => (
|
|
276
|
+
<blockquote
|
|
277
|
+
className={cn(
|
|
278
|
+
'border-muted border-l-2 pl-3 text-muted-foreground text-sm italic',
|
|
279
|
+
className
|
|
280
|
+
)}
|
|
281
|
+
{...props}
|
|
282
|
+
>
|
|
283
|
+
{children}
|
|
284
|
+
</blockquote>
|
|
285
|
+
);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
|
|
4
|
+
interface LoaderIconProps {
|
|
5
|
+
size?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
|
|
9
|
+
<svg
|
|
10
|
+
height={size}
|
|
11
|
+
strokeLinejoin="round"
|
|
12
|
+
style={{ color: 'currentcolor' }}
|
|
13
|
+
viewBox="0 0 16 16"
|
|
14
|
+
width={size}
|
|
15
|
+
>
|
|
16
|
+
<title>Loader</title>
|
|
17
|
+
<g clipPath="url(#clip0_2393_1490)">
|
|
18
|
+
<path d="M8 0V4" stroke="currentColor" strokeWidth="1.5" />
|
|
19
|
+
<path
|
|
20
|
+
d="M8 16V12"
|
|
21
|
+
opacity="0.5"
|
|
22
|
+
stroke="currentColor"
|
|
23
|
+
strokeWidth="1.5"
|
|
24
|
+
/>
|
|
25
|
+
<path
|
|
26
|
+
d="M3.29773 1.52783L5.64887 4.7639"
|
|
27
|
+
opacity="0.9"
|
|
28
|
+
stroke="currentColor"
|
|
29
|
+
strokeWidth="1.5"
|
|
30
|
+
/>
|
|
31
|
+
<path
|
|
32
|
+
d="M12.7023 1.52783L10.3511 4.7639"
|
|
33
|
+
opacity="0.1"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
strokeWidth="1.5"
|
|
36
|
+
/>
|
|
37
|
+
<path
|
|
38
|
+
d="M12.7023 14.472L10.3511 11.236"
|
|
39
|
+
opacity="0.4"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
strokeWidth="1.5"
|
|
42
|
+
/>
|
|
43
|
+
<path
|
|
44
|
+
d="M3.29773 14.472L5.64887 11.236"
|
|
45
|
+
opacity="0.6"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
strokeWidth="1.5"
|
|
48
|
+
/>
|
|
49
|
+
<path
|
|
50
|
+
d="M15.6085 5.52783L11.8043 6.7639"
|
|
51
|
+
opacity="0.2"
|
|
52
|
+
stroke="currentColor"
|
|
53
|
+
strokeWidth="1.5"
|
|
54
|
+
/>
|
|
55
|
+
<path
|
|
56
|
+
d="M0.391602 10.472L4.19583 9.23598"
|
|
57
|
+
opacity="0.7"
|
|
58
|
+
stroke="currentColor"
|
|
59
|
+
strokeWidth="1.5"
|
|
60
|
+
/>
|
|
61
|
+
<path
|
|
62
|
+
d="M15.6085 10.4722L11.8043 9.2361"
|
|
63
|
+
opacity="0.3"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
strokeWidth="1.5"
|
|
66
|
+
/>
|
|
67
|
+
<path
|
|
68
|
+
d="M0.391602 5.52783L4.19583 6.7639"
|
|
69
|
+
opacity="0.8"
|
|
70
|
+
stroke="currentColor"
|
|
71
|
+
strokeWidth="1.5"
|
|
72
|
+
/>
|
|
73
|
+
</g>
|
|
74
|
+
<defs>
|
|
75
|
+
<clipPath id="clip0_2393_1490">
|
|
76
|
+
<rect fill="white" height="16" width="16" />
|
|
77
|
+
</clipPath>
|
|
78
|
+
</defs>
|
|
79
|
+
</svg>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
export type LoaderProps = HTMLAttributes<HTMLDivElement> & {
|
|
83
|
+
size?: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
|
|
87
|
+
<div
|
|
88
|
+
className={cn(
|
|
89
|
+
'inline-flex animate-spin items-center justify-center',
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
<LoaderIcon size={size} />
|
|
95
|
+
</div>
|
|
96
|
+
);
|