@gmickel/gno 0.3.4 → 0.4.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 +194 -53
- package/assets/badges/license.svg +12 -0
- package/assets/badges/npm.svg +13 -0
- package/assets/badges/twitter.svg +22 -0
- package/assets/badges/website.svg +22 -0
- package/package.json +30 -1
- package/src/cli/commands/ask.ts +11 -186
- package/src/cli/commands/models/pull.ts +9 -4
- package/src/cli/commands/serve.ts +19 -0
- package/src/cli/program.ts +28 -0
- package/src/llm/registry.ts +3 -1
- package/src/pipeline/answer.ts +191 -0
- 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/sqlite/adapter.ts +47 -0
- package/src/store/types.ts +10 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { BookIcon, ChevronDownIcon } from 'lucide-react';
|
|
2
|
+
import type { ComponentProps } from 'react';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import {
|
|
5
|
+
Collapsible,
|
|
6
|
+
CollapsibleContent,
|
|
7
|
+
CollapsibleTrigger,
|
|
8
|
+
} from '../ui/collapsible';
|
|
9
|
+
|
|
10
|
+
export type SourcesProps = ComponentProps<'div'>;
|
|
11
|
+
|
|
12
|
+
export const Sources = ({ className, ...props }: SourcesProps) => (
|
|
13
|
+
<Collapsible
|
|
14
|
+
className={cn('not-prose mb-4 text-primary text-xs', className)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type SourcesTriggerProps = ComponentProps<typeof CollapsibleTrigger> & {
|
|
20
|
+
count: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const SourcesTrigger = ({
|
|
24
|
+
className,
|
|
25
|
+
count,
|
|
26
|
+
children,
|
|
27
|
+
...props
|
|
28
|
+
}: SourcesTriggerProps) => (
|
|
29
|
+
<CollapsibleTrigger
|
|
30
|
+
className={cn('flex items-center gap-2', className)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{children ?? (
|
|
34
|
+
<>
|
|
35
|
+
<p className="font-medium">Used {count} sources</p>
|
|
36
|
+
<ChevronDownIcon className="h-4 w-4" />
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
</CollapsibleTrigger>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export type SourcesContentProps = ComponentProps<typeof CollapsibleContent>;
|
|
43
|
+
|
|
44
|
+
export const SourcesContent = ({
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: SourcesContentProps) => (
|
|
48
|
+
<CollapsibleContent
|
|
49
|
+
className={cn(
|
|
50
|
+
'mt-3 flex w-fit flex-col gap-2',
|
|
51
|
+
'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
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export type SourceProps = ComponentProps<'a'>;
|
|
59
|
+
|
|
60
|
+
export const Source = ({ href, title, children, ...props }: SourceProps) => (
|
|
61
|
+
<a
|
|
62
|
+
className="flex items-center gap-2"
|
|
63
|
+
href={href}
|
|
64
|
+
rel="noreferrer"
|
|
65
|
+
target="_blank"
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{children ?? (
|
|
69
|
+
<>
|
|
70
|
+
<BookIcon className="h-4 w-4" />
|
|
71
|
+
<span className="block font-medium">{title}</span>
|
|
72
|
+
</>
|
|
73
|
+
)}
|
|
74
|
+
</a>
|
|
75
|
+
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { cn } from '../../lib/utils';
|
|
3
|
+
import { Button } from '../ui/button';
|
|
4
|
+
import { ScrollArea, ScrollBar } from '../ui/scroll-area';
|
|
5
|
+
|
|
6
|
+
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
|
|
7
|
+
|
|
8
|
+
export const Suggestions = ({
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}: SuggestionsProps) => (
|
|
13
|
+
<ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
|
|
14
|
+
<div className={cn('flex w-max flex-nowrap items-center gap-2', className)}>
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
<ScrollBar className="hidden" orientation="horizontal" />
|
|
18
|
+
</ScrollArea>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export type SuggestionProps = Omit<ComponentProps<typeof Button>, 'onClick'> & {
|
|
22
|
+
suggestion: string;
|
|
23
|
+
onClick?: (suggestion: string) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const Suggestion = ({
|
|
27
|
+
suggestion,
|
|
28
|
+
onClick,
|
|
29
|
+
className,
|
|
30
|
+
variant = 'outline',
|
|
31
|
+
size = 'sm',
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: SuggestionProps) => {
|
|
35
|
+
const handleClick = () => {
|
|
36
|
+
onClick?.(suggestion);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Button
|
|
41
|
+
className={cn('cursor-pointer rounded-full px-4', className)}
|
|
42
|
+
onClick={handleClick}
|
|
43
|
+
size={size}
|
|
44
|
+
type="button"
|
|
45
|
+
variant={variant}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{children || suggestion}
|
|
49
|
+
</Button>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertCircle,
|
|
3
|
+
CheckIcon,
|
|
4
|
+
ChevronDownIcon,
|
|
5
|
+
Download,
|
|
6
|
+
Loader2,
|
|
7
|
+
SlidersHorizontal,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
|
+
import { apiFetch } from '../hooks/use-api';
|
|
11
|
+
import { Button } from './ui/button';
|
|
12
|
+
import {
|
|
13
|
+
DropdownMenu,
|
|
14
|
+
DropdownMenuContent,
|
|
15
|
+
DropdownMenuLabel,
|
|
16
|
+
DropdownMenuRadioGroup,
|
|
17
|
+
DropdownMenuRadioItem,
|
|
18
|
+
DropdownMenuSeparator,
|
|
19
|
+
DropdownMenuTrigger,
|
|
20
|
+
} from './ui/dropdown-menu';
|
|
21
|
+
import { Progress } from './ui/progress';
|
|
22
|
+
|
|
23
|
+
interface Preset {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
embed: string;
|
|
27
|
+
rerank: string;
|
|
28
|
+
gen: string;
|
|
29
|
+
active: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PresetsResponse {
|
|
33
|
+
presets: Preset[];
|
|
34
|
+
activePreset: string;
|
|
35
|
+
capabilities: Capabilities;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface Capabilities {
|
|
39
|
+
bm25: boolean;
|
|
40
|
+
vector: boolean;
|
|
41
|
+
hybrid: boolean;
|
|
42
|
+
answer: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SetPresetResponse {
|
|
46
|
+
success: boolean;
|
|
47
|
+
activePreset: string;
|
|
48
|
+
capabilities: Capabilities;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface DownloadProgress {
|
|
52
|
+
downloadedBytes: number;
|
|
53
|
+
totalBytes: number;
|
|
54
|
+
percent: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface DownloadStatus {
|
|
58
|
+
active: boolean;
|
|
59
|
+
currentType: string | null;
|
|
60
|
+
progress: DownloadProgress | null;
|
|
61
|
+
completed: string[];
|
|
62
|
+
failed: Array<{ type: string; error: string }>;
|
|
63
|
+
startedAt: number | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Top-level regex patterns for performance
|
|
67
|
+
const SIZE_REGEX = /~[\d.]+GB/;
|
|
68
|
+
const DESC_REGEX = /\(([^,]+)/;
|
|
69
|
+
|
|
70
|
+
const KB = 1024;
|
|
71
|
+
const MB = KB * 1024;
|
|
72
|
+
const GB = MB * 1024;
|
|
73
|
+
|
|
74
|
+
function extractSize(name: string): string | null {
|
|
75
|
+
const match = name.match(SIZE_REGEX);
|
|
76
|
+
return match ? match[0] : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractDesc(name: string): string | null {
|
|
80
|
+
const match = name.match(DESC_REGEX);
|
|
81
|
+
return match ? match[1].trim() : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractBaseName(name: string): string {
|
|
85
|
+
return name.split('(')[0].trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatBytes(bytes: number): string {
|
|
89
|
+
if (bytes < KB) {
|
|
90
|
+
return `${bytes} B`;
|
|
91
|
+
}
|
|
92
|
+
if (bytes < MB) {
|
|
93
|
+
return `${(bytes / KB).toFixed(1)} KB`;
|
|
94
|
+
}
|
|
95
|
+
if (bytes < GB) {
|
|
96
|
+
return `${(bytes / MB).toFixed(1)} MB`;
|
|
97
|
+
}
|
|
98
|
+
return `${(bytes / GB).toFixed(2)} GB`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getButtonIcon(
|
|
102
|
+
switching: boolean,
|
|
103
|
+
downloading: boolean,
|
|
104
|
+
hasError: boolean
|
|
105
|
+
) {
|
|
106
|
+
if (switching) {
|
|
107
|
+
return <Loader2 className="size-3.5 animate-spin text-primary" />;
|
|
108
|
+
}
|
|
109
|
+
if (downloading) {
|
|
110
|
+
return <Loader2 className="size-3.5 animate-spin text-blue-500" />;
|
|
111
|
+
}
|
|
112
|
+
if (hasError) {
|
|
113
|
+
return <AlertCircle className="size-3.5 text-amber-500" />;
|
|
114
|
+
}
|
|
115
|
+
return (
|
|
116
|
+
<SlidersHorizontal className="size-3.5 text-muted-foreground/70 transition-colors group-hover:text-primary" />
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getButtonLabel(
|
|
121
|
+
switching: boolean,
|
|
122
|
+
downloading: boolean,
|
|
123
|
+
activePreset: Preset | undefined
|
|
124
|
+
): string {
|
|
125
|
+
if (switching) {
|
|
126
|
+
return 'Switching...';
|
|
127
|
+
}
|
|
128
|
+
if (downloading) {
|
|
129
|
+
return 'Downloading...';
|
|
130
|
+
}
|
|
131
|
+
return activePreset ? extractBaseName(activePreset.name) : 'Preset';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function PresetSelector() {
|
|
135
|
+
const [presets, setPresets] = useState<Preset[]>([]);
|
|
136
|
+
const [activeId, setActiveId] = useState<string>('');
|
|
137
|
+
const [loading, setLoading] = useState(true);
|
|
138
|
+
const [switching, setSwitching] = useState(false);
|
|
139
|
+
const [error, setError] = useState<string | null>(null);
|
|
140
|
+
const [modelsNeeded, setModelsNeeded] = useState(false);
|
|
141
|
+
|
|
142
|
+
// Download state
|
|
143
|
+
const [downloading, setDownloading] = useState(false);
|
|
144
|
+
const [downloadStatus, setDownloadStatus] = useState<DownloadStatus | null>(
|
|
145
|
+
null
|
|
146
|
+
);
|
|
147
|
+
const pollInterval = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
148
|
+
|
|
149
|
+
// Check capabilities and set modelsNeeded flag
|
|
150
|
+
const checkCapabilities = useCallback((caps: Capabilities) => {
|
|
151
|
+
const missing: string[] = [];
|
|
152
|
+
if (!caps.vector) {
|
|
153
|
+
missing.push('vector search');
|
|
154
|
+
}
|
|
155
|
+
if (!caps.answer) {
|
|
156
|
+
missing.push('AI answers');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (missing.length > 0) {
|
|
160
|
+
setError(`Missing: ${missing.join(', ')}`);
|
|
161
|
+
setModelsNeeded(true);
|
|
162
|
+
} else {
|
|
163
|
+
setError(null);
|
|
164
|
+
setModelsNeeded(false);
|
|
165
|
+
}
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
// Poll download status
|
|
169
|
+
const pollStatus = useCallback(async () => {
|
|
170
|
+
const { data } = await apiFetch<DownloadStatus>('/api/models/status');
|
|
171
|
+
if (data) {
|
|
172
|
+
setDownloadStatus(data);
|
|
173
|
+
|
|
174
|
+
// Download finished
|
|
175
|
+
if (!data.active && downloading) {
|
|
176
|
+
setDownloading(false);
|
|
177
|
+
if (pollInterval.current) {
|
|
178
|
+
clearInterval(pollInterval.current);
|
|
179
|
+
pollInterval.current = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Refresh presets to get updated capabilities
|
|
183
|
+
const { data: presetsData } =
|
|
184
|
+
await apiFetch<PresetsResponse>('/api/presets');
|
|
185
|
+
if (presetsData) {
|
|
186
|
+
checkCapabilities(presetsData.capabilities);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Show any failures
|
|
190
|
+
if (data.failed.length > 0) {
|
|
191
|
+
setError(`Failed: ${data.failed.map((f) => f.type).join(', ')}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, [downloading, checkCapabilities]);
|
|
196
|
+
|
|
197
|
+
// Initial load
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
apiFetch<PresetsResponse>('/api/presets').then(({ data }) => {
|
|
200
|
+
if (data) {
|
|
201
|
+
setPresets(data.presets);
|
|
202
|
+
setActiveId(data.activePreset);
|
|
203
|
+
checkCapabilities(data.capabilities);
|
|
204
|
+
}
|
|
205
|
+
setLoading(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Check if download already in progress
|
|
209
|
+
apiFetch<DownloadStatus>('/api/models/status').then(({ data }) => {
|
|
210
|
+
if (data?.active) {
|
|
211
|
+
setDownloading(true);
|
|
212
|
+
setDownloadStatus(data);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}, [checkCapabilities]);
|
|
216
|
+
|
|
217
|
+
// Start/stop polling
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (downloading && !pollInterval.current) {
|
|
220
|
+
pollInterval.current = setInterval(pollStatus, 1000);
|
|
221
|
+
}
|
|
222
|
+
return () => {
|
|
223
|
+
if (pollInterval.current) {
|
|
224
|
+
clearInterval(pollInterval.current);
|
|
225
|
+
pollInterval.current = null;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}, [downloading, pollStatus]);
|
|
229
|
+
|
|
230
|
+
const activePreset = presets.find((p) => p.id === activeId);
|
|
231
|
+
|
|
232
|
+
if (loading || presets.length === 0) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const handleSelect = async (id: string) => {
|
|
237
|
+
if (id === activeId || switching || downloading) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
setSwitching(true);
|
|
242
|
+
setError(null);
|
|
243
|
+
|
|
244
|
+
const { data, error: fetchError } = await apiFetch<SetPresetResponse>(
|
|
245
|
+
'/api/presets',
|
|
246
|
+
{
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: { 'Content-Type': 'application/json' },
|
|
249
|
+
body: JSON.stringify({ presetId: id }),
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
setSwitching(false);
|
|
254
|
+
|
|
255
|
+
if (fetchError) {
|
|
256
|
+
setError(fetchError);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (data?.success) {
|
|
261
|
+
setActiveId(data.activePreset);
|
|
262
|
+
checkCapabilities(data.capabilities);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const handleDownload = async () => {
|
|
267
|
+
if (downloading) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
setDownloading(true);
|
|
272
|
+
setError(null);
|
|
273
|
+
|
|
274
|
+
const { error: fetchError } = await apiFetch('/api/models/pull', {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (fetchError) {
|
|
279
|
+
setError(fetchError);
|
|
280
|
+
setDownloading(false);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Start polling
|
|
285
|
+
pollStatus();
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<DropdownMenu>
|
|
290
|
+
<DropdownMenuTrigger asChild>
|
|
291
|
+
<Button
|
|
292
|
+
className="group gap-2 font-normal text-muted-foreground hover:text-foreground"
|
|
293
|
+
disabled={switching}
|
|
294
|
+
size="sm"
|
|
295
|
+
variant="ghost"
|
|
296
|
+
>
|
|
297
|
+
{getButtonIcon(
|
|
298
|
+
switching,
|
|
299
|
+
downloading,
|
|
300
|
+
Boolean(error || modelsNeeded)
|
|
301
|
+
)}
|
|
302
|
+
<span className="hidden sm:inline">
|
|
303
|
+
{getButtonLabel(switching, downloading, activePreset)}
|
|
304
|
+
</span>
|
|
305
|
+
<ChevronDownIcon className="size-3 opacity-50" />
|
|
306
|
+
</Button>
|
|
307
|
+
</DropdownMenuTrigger>
|
|
308
|
+
<DropdownMenuContent align="start" className="w-64 border-border bg-card">
|
|
309
|
+
<DropdownMenuLabel className="font-normal text-muted-foreground text-xs uppercase tracking-wider">
|
|
310
|
+
Model Preset
|
|
311
|
+
</DropdownMenuLabel>
|
|
312
|
+
<DropdownMenuSeparator />
|
|
313
|
+
|
|
314
|
+
{/* Download progress */}
|
|
315
|
+
{downloading && downloadStatus && (
|
|
316
|
+
<>
|
|
317
|
+
<div className="space-y-2 px-2 py-2">
|
|
318
|
+
<div className="flex items-center justify-between text-xs">
|
|
319
|
+
<span className="text-muted-foreground">
|
|
320
|
+
{downloadStatus.currentType || 'Starting...'}
|
|
321
|
+
</span>
|
|
322
|
+
{downloadStatus.progress && (
|
|
323
|
+
<span className="font-mono text-[10px] text-muted-foreground">
|
|
324
|
+
{formatBytes(downloadStatus.progress.downloadedBytes)} /{' '}
|
|
325
|
+
{formatBytes(downloadStatus.progress.totalBytes)}
|
|
326
|
+
</span>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
<Progress value={downloadStatus.progress?.percent ?? 0} />
|
|
330
|
+
{downloadStatus.completed.length > 0 && (
|
|
331
|
+
<div className="text-[10px] text-muted-foreground">
|
|
332
|
+
Done: {downloadStatus.completed.join(', ')}
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
336
|
+
<DropdownMenuSeparator />
|
|
337
|
+
</>
|
|
338
|
+
)}
|
|
339
|
+
|
|
340
|
+
<DropdownMenuRadioGroup onValueChange={handleSelect} value={activeId}>
|
|
341
|
+
{presets.map((preset) => {
|
|
342
|
+
const baseName = extractBaseName(preset.name);
|
|
343
|
+
const desc = extractDesc(preset.name);
|
|
344
|
+
const size = extractSize(preset.name);
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<DropdownMenuRadioItem
|
|
348
|
+
className="cursor-pointer py-2.5"
|
|
349
|
+
disabled={switching || downloading}
|
|
350
|
+
key={preset.id}
|
|
351
|
+
value={preset.id}
|
|
352
|
+
>
|
|
353
|
+
<div className="flex w-full items-center justify-between gap-3">
|
|
354
|
+
<div className="flex flex-col gap-0.5">
|
|
355
|
+
<span className="font-medium">{baseName}</span>
|
|
356
|
+
{desc && (
|
|
357
|
+
<span className="text-muted-foreground text-xs">
|
|
358
|
+
{desc}
|
|
359
|
+
</span>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
<div className="flex items-center gap-2">
|
|
363
|
+
{size && (
|
|
364
|
+
<span className="rounded bg-muted px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
|
365
|
+
{size}
|
|
366
|
+
</span>
|
|
367
|
+
)}
|
|
368
|
+
{preset.id === activeId && (
|
|
369
|
+
<CheckIcon className="size-4 text-primary" />
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
</DropdownMenuRadioItem>
|
|
374
|
+
);
|
|
375
|
+
})}
|
|
376
|
+
</DropdownMenuRadioGroup>
|
|
377
|
+
|
|
378
|
+
{/* Error or download prompt */}
|
|
379
|
+
{(error || modelsNeeded) && !downloading && (
|
|
380
|
+
<>
|
|
381
|
+
<DropdownMenuSeparator />
|
|
382
|
+
<div className="space-y-2 px-2 py-2">
|
|
383
|
+
{error && (
|
|
384
|
+
<div className="text-[10px] text-destructive">{error}</div>
|
|
385
|
+
)}
|
|
386
|
+
{modelsNeeded && (
|
|
387
|
+
<Button
|
|
388
|
+
className="w-full gap-2"
|
|
389
|
+
onClick={handleDownload}
|
|
390
|
+
size="sm"
|
|
391
|
+
variant="outline"
|
|
392
|
+
>
|
|
393
|
+
<Download className="size-3.5" />
|
|
394
|
+
Download Models
|
|
395
|
+
</Button>
|
|
396
|
+
)}
|
|
397
|
+
</div>
|
|
398
|
+
</>
|
|
399
|
+
)}
|
|
400
|
+
</DropdownMenuContent>
|
|
401
|
+
</DropdownMenu>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import { cn } from '../../lib/utils';
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded-full border px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
|
14
|
+
secondary:
|
|
15
|
+
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
|
16
|
+
destructive:
|
|
17
|
+
'border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90',
|
|
18
|
+
outline:
|
|
19
|
+
'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant,
|
|
31
|
+
asChild = false,
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<'span'> &
|
|
34
|
+
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
35
|
+
const Comp = asChild ? Slot : 'span';
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Comp
|
|
39
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
40
|
+
data-slot="badge"
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { cn } from '../../lib/utils';
|
|
4
|
+
import { Separator } from './separator';
|
|
5
|
+
|
|
6
|
+
const buttonGroupVariants = cva(
|
|
7
|
+
"flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
orientation: {
|
|
11
|
+
horizontal:
|
|
12
|
+
'[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
|
|
13
|
+
vertical:
|
|
14
|
+
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
orientation: 'horizontal',
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
function ButtonGroup({
|
|
24
|
+
className,
|
|
25
|
+
orientation,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(buttonGroupVariants({ orientation }), className)}
|
|
31
|
+
data-orientation={orientation}
|
|
32
|
+
data-slot="button-group"
|
|
33
|
+
role="group"
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ButtonGroupText({
|
|
40
|
+
className,
|
|
41
|
+
asChild = false,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<'div'> & {
|
|
44
|
+
asChild?: boolean;
|
|
45
|
+
}) {
|
|
46
|
+
const Comp = asChild ? Slot : 'div';
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Comp
|
|
50
|
+
className={cn(
|
|
51
|
+
"flex items-center gap-2 rounded-md border bg-muted px-4 font-medium text-sm shadow-xs [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ButtonGroupSeparator({
|
|
60
|
+
className,
|
|
61
|
+
orientation = 'vertical',
|
|
62
|
+
...props
|
|
63
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
64
|
+
return (
|
|
65
|
+
<Separator
|
|
66
|
+
className={cn(
|
|
67
|
+
'!m-0 relative self-stretch bg-input data-[orientation=vertical]:h-auto',
|
|
68
|
+
className
|
|
69
|
+
)}
|
|
70
|
+
data-slot="button-group-separator"
|
|
71
|
+
orientation={orientation}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
ButtonGroup,
|
|
79
|
+
ButtonGroupSeparator,
|
|
80
|
+
ButtonGroupText,
|
|
81
|
+
buttonGroupVariants,
|
|
82
|
+
};
|