@gmickel/gno 0.28.2 → 0.29.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 +10 -2
- package/package.json +1 -1
- package/src/app/constants.ts +4 -2
- package/src/cli/commands/mcp/install.ts +4 -4
- package/src/cli/commands/mcp/status.ts +7 -7
- package/src/cli/commands/skill/install.ts +5 -5
- package/src/cli/program.ts +2 -2
- package/src/collection/add.ts +10 -0
- package/src/collection/types.ts +1 -0
- package/src/config/types.ts +12 -2
- package/src/core/depth-policy.ts +1 -1
- package/src/core/file-ops.ts +38 -0
- package/src/llm/registry.ts +20 -4
- package/src/serve/AGENTS.md +16 -16
- package/src/serve/CLAUDE.md +16 -16
- package/src/serve/config-sync.ts +32 -1
- package/src/serve/connectors.ts +243 -0
- package/src/serve/context.ts +9 -0
- package/src/serve/doc-events.ts +31 -1
- package/src/serve/embed-scheduler.ts +12 -0
- package/src/serve/import-preview.ts +173 -0
- package/src/serve/public/app.tsx +101 -7
- package/src/serve/public/components/AIModelSelector.tsx +383 -145
- package/src/serve/public/components/AddCollectionDialog.tsx +123 -7
- package/src/serve/public/components/BootstrapStatus.tsx +133 -0
- package/src/serve/public/components/CaptureModal.tsx +5 -2
- package/src/serve/public/components/CollectionsEmptyState.tsx +63 -0
- package/src/serve/public/components/FirstRunWizard.tsx +622 -0
- package/src/serve/public/components/HealthCenter.tsx +128 -0
- package/src/serve/public/components/IndexingProgress.tsx +21 -2
- package/src/serve/public/components/QuickSwitcher.tsx +62 -36
- package/src/serve/public/components/TagInput.tsx +5 -1
- package/src/serve/public/components/WikiLinkAutocomplete.tsx +15 -6
- package/src/serve/public/components/WorkspaceTabs.tsx +60 -0
- package/src/serve/public/hooks/use-doc-events.ts +48 -4
- package/src/serve/public/lib/local-history.ts +40 -7
- package/src/serve/public/lib/navigation-state.ts +156 -0
- package/src/serve/public/lib/workspace-tabs.ts +235 -0
- package/src/serve/public/pages/Ask.tsx +11 -1
- package/src/serve/public/pages/Browse.tsx +73 -0
- package/src/serve/public/pages/Collections.tsx +29 -13
- package/src/serve/public/pages/Connectors.tsx +178 -0
- package/src/serve/public/pages/Dashboard.tsx +493 -67
- package/src/serve/public/pages/DocView.tsx +192 -34
- package/src/serve/public/pages/DocumentEditor.tsx +127 -5
- package/src/serve/public/pages/Search.tsx +12 -1
- package/src/serve/routes/api.ts +532 -62
- package/src/serve/server.ts +79 -2
- package/src/serve/status-model.ts +149 -0
- package/src/serve/status.ts +706 -0
- package/src/serve/watch-service.ts +73 -8
- package/src/types/electrobun-shell.d.ts +43 -0
|
@@ -11,16 +11,22 @@
|
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
AlertCircle,
|
|
14
|
+
BadgeCheck,
|
|
14
15
|
Check,
|
|
15
16
|
ChevronDown,
|
|
16
17
|
Download,
|
|
17
18
|
Loader2,
|
|
19
|
+
ScanSearch,
|
|
18
20
|
Sparkles,
|
|
19
21
|
} from "lucide-react";
|
|
20
22
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
23
|
+
import { createPortal } from "react-dom";
|
|
24
|
+
|
|
25
|
+
import type { AppStatusResponse } from "../../status-model";
|
|
21
26
|
|
|
22
27
|
import { apiFetch } from "../hooks/use-api";
|
|
23
28
|
import { cn } from "../lib/utils";
|
|
29
|
+
import { Badge } from "./ui/badge";
|
|
24
30
|
|
|
25
31
|
interface Preset {
|
|
26
32
|
id: string;
|
|
@@ -66,12 +72,26 @@ interface DownloadStatus {
|
|
|
66
72
|
startedAt: number | null;
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
const PRESET_EXPLANATIONS: Record<string, string> = {
|
|
76
|
+
slim: "Fastest setup. Lowest disk use.",
|
|
77
|
+
balanced: "Better answers. Good default.",
|
|
78
|
+
quality: "Best local answers. Highest disk use.",
|
|
79
|
+
"slim-tuned": "Fine-tuned retrieval in a compact footprint.",
|
|
80
|
+
};
|
|
81
|
+
const BUILTIN_PRESET_IDS = new Set([
|
|
82
|
+
"slim-tuned",
|
|
83
|
+
"slim",
|
|
84
|
+
"balanced",
|
|
85
|
+
"quality",
|
|
86
|
+
]);
|
|
87
|
+
|
|
69
88
|
// Extract readable model name from preset name
|
|
70
89
|
const SIZE_REGEX = /~[\d.]+GB/;
|
|
71
90
|
const MODEL_URI_SEGMENT_RE = /\/([^/#]+?)(?:\.(?:gguf|bin|safetensors))?$/i;
|
|
72
91
|
|
|
73
92
|
function extractBaseName(name: string): string {
|
|
74
|
-
|
|
93
|
+
const [firstPart] = name.split("(");
|
|
94
|
+
return firstPart?.trim() ?? name.trim();
|
|
75
95
|
}
|
|
76
96
|
|
|
77
97
|
function extractSize(name: string): string | null {
|
|
@@ -107,16 +127,34 @@ function formatModelRole(uri: string | undefined): string {
|
|
|
107
127
|
|
|
108
128
|
export interface AIModelSelectorProps {
|
|
109
129
|
onPresetChange?: (presetId: string) => void;
|
|
130
|
+
showDetails?: boolean;
|
|
131
|
+
showDownloadAction?: boolean;
|
|
132
|
+
showLabel?: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isCustomPreset(preset: Preset): boolean {
|
|
136
|
+
return !BUILTIN_PRESET_IDS.has(preset.id);
|
|
110
137
|
}
|
|
111
138
|
|
|
112
|
-
export function AIModelSelector({
|
|
139
|
+
export function AIModelSelector({
|
|
140
|
+
onPresetChange,
|
|
141
|
+
showDetails = false,
|
|
142
|
+
showDownloadAction = true,
|
|
143
|
+
showLabel = true,
|
|
144
|
+
}: AIModelSelectorProps = {}) {
|
|
113
145
|
const [presets, setPresets] = useState<Preset[]>([]);
|
|
114
146
|
const [activeId, setActiveId] = useState<string>("");
|
|
115
147
|
const [loading, setLoading] = useState(true);
|
|
116
148
|
const [switching, setSwitching] = useState(false);
|
|
117
149
|
const [error, setError] = useState<string | null>(null);
|
|
118
150
|
const [modelsNeeded, setModelsNeeded] = useState(false);
|
|
151
|
+
const [notice, setNotice] = useState<string | null>(null);
|
|
119
152
|
const [open, setOpen] = useState(false);
|
|
153
|
+
const [menuPosition, setMenuPosition] = useState<{
|
|
154
|
+
left: number;
|
|
155
|
+
top: number;
|
|
156
|
+
width: number;
|
|
157
|
+
} | null>(null);
|
|
120
158
|
|
|
121
159
|
// Download state
|
|
122
160
|
const [downloading, setDownloading] = useState(false);
|
|
@@ -125,21 +163,57 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
125
163
|
);
|
|
126
164
|
const pollInterval = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
127
165
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
166
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
167
|
+
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
128
168
|
|
|
129
169
|
// Click outside to close
|
|
130
170
|
useEffect(() => {
|
|
131
171
|
function handleClickOutside(e: MouseEvent) {
|
|
172
|
+
const target = e.target as Node;
|
|
132
173
|
if (
|
|
133
|
-
containerRef.current &&
|
|
134
|
-
|
|
174
|
+
(containerRef.current && containerRef.current.contains(target)) ||
|
|
175
|
+
(menuRef.current && menuRef.current.contains(target))
|
|
135
176
|
) {
|
|
136
|
-
|
|
177
|
+
return;
|
|
137
178
|
}
|
|
179
|
+
setOpen(false);
|
|
138
180
|
}
|
|
139
181
|
document.addEventListener("mousedown", handleClickOutside);
|
|
140
182
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
141
183
|
}, []);
|
|
142
184
|
|
|
185
|
+
const updateMenuPosition = useCallback(() => {
|
|
186
|
+
if (!triggerRef.current || typeof window === "undefined") {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rect = triggerRef.current.getBoundingClientRect();
|
|
191
|
+
const width = Math.min(360, window.innerWidth - 32);
|
|
192
|
+
const left = Math.max(
|
|
193
|
+
16,
|
|
194
|
+
Math.min(rect.left, window.innerWidth - width - 16)
|
|
195
|
+
);
|
|
196
|
+
const top = rect.bottom + 8;
|
|
197
|
+
|
|
198
|
+
setMenuPosition({ left, top, width });
|
|
199
|
+
}, []);
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!open) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
updateMenuPosition();
|
|
207
|
+
|
|
208
|
+
const handlePosition = () => updateMenuPosition();
|
|
209
|
+
window.addEventListener("resize", handlePosition);
|
|
210
|
+
window.addEventListener("scroll", handlePosition, true);
|
|
211
|
+
return () => {
|
|
212
|
+
window.removeEventListener("resize", handlePosition);
|
|
213
|
+
window.removeEventListener("scroll", handlePosition, true);
|
|
214
|
+
};
|
|
215
|
+
}, [open, updateMenuPosition]);
|
|
216
|
+
|
|
143
217
|
// Check capabilities
|
|
144
218
|
const checkCapabilities = useCallback((caps: Capabilities) => {
|
|
145
219
|
if (!caps.answer) {
|
|
@@ -170,6 +244,14 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
170
244
|
if (presetsData) {
|
|
171
245
|
checkCapabilities(presetsData.capabilities);
|
|
172
246
|
}
|
|
247
|
+
const { data: statusData } =
|
|
248
|
+
await apiFetch<AppStatusResponse>("/api/status");
|
|
249
|
+
if (statusData) {
|
|
250
|
+
setModelsNeeded(
|
|
251
|
+
statusData.bootstrap.models.cachedCount <
|
|
252
|
+
statusData.bootstrap.models.totalCount
|
|
253
|
+
);
|
|
254
|
+
}
|
|
173
255
|
|
|
174
256
|
if (data.failed.length > 0) {
|
|
175
257
|
setError(`Failed: ${data.failed.map((f) => f.type).join(", ")}`);
|
|
@@ -190,6 +272,14 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
190
272
|
setLoading(false);
|
|
191
273
|
});
|
|
192
274
|
|
|
275
|
+
void apiFetch<AppStatusResponse>("/api/status").then(({ data }) => {
|
|
276
|
+
if (data) {
|
|
277
|
+
setModelsNeeded(
|
|
278
|
+
data.bootstrap.models.cachedCount < data.bootstrap.models.totalCount
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
193
283
|
void apiFetch<DownloadStatus>("/api/models/status").then(({ data }) => {
|
|
194
284
|
if (data?.active) {
|
|
195
285
|
setDownloading(true);
|
|
@@ -212,6 +302,30 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
212
302
|
}, [downloading, pollStatus]);
|
|
213
303
|
|
|
214
304
|
const activePreset = presets.find((p) => p.id === activeId);
|
|
305
|
+
const activeExplanation = activePreset
|
|
306
|
+
? (PRESET_EXPLANATIONS[activePreset.id] ??
|
|
307
|
+
"Switch between presets without redoing setup.")
|
|
308
|
+
: "Select a preset";
|
|
309
|
+
|
|
310
|
+
const syncFromStatus = useCallback(
|
|
311
|
+
async (status: AppStatusResponse | null) => {
|
|
312
|
+
if (!status) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const readyModels =
|
|
317
|
+
status.bootstrap.models.cachedCount >=
|
|
318
|
+
status.bootstrap.models.totalCount;
|
|
319
|
+
setModelsNeeded(!readyModels);
|
|
320
|
+
checkCapabilities(status.capabilities);
|
|
321
|
+
|
|
322
|
+
if (!readyModels) {
|
|
323
|
+
setNotice("Switched preset. Downloading required models...");
|
|
324
|
+
await handleDownload();
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
[checkCapabilities]
|
|
328
|
+
);
|
|
215
329
|
|
|
216
330
|
const handleSelect = async (id: string) => {
|
|
217
331
|
if (id === activeId || switching || downloading) return;
|
|
@@ -240,6 +354,11 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
240
354
|
onPresetChange?.(data.activePreset);
|
|
241
355
|
checkCapabilities(data.capabilities);
|
|
242
356
|
setOpen(false);
|
|
357
|
+
const presetName = presets.find((preset) => preset.id === id)?.name ?? id;
|
|
358
|
+
setNotice(`Switched to ${presetName}`);
|
|
359
|
+
const { data: statusData } =
|
|
360
|
+
await apiFetch<AppStatusResponse>("/api/status");
|
|
361
|
+
await syncFromStatus(statusData);
|
|
243
362
|
}
|
|
244
363
|
};
|
|
245
364
|
|
|
@@ -262,13 +381,23 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
262
381
|
void pollStatus();
|
|
263
382
|
};
|
|
264
383
|
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (!notice) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const timer = window.setTimeout(() => setNotice(null), 3000);
|
|
389
|
+
return () => window.clearTimeout(timer);
|
|
390
|
+
}, [notice]);
|
|
391
|
+
|
|
265
392
|
// Loading skeleton
|
|
266
393
|
if (loading) {
|
|
267
394
|
return (
|
|
268
395
|
<div className="flex items-center gap-2">
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
396
|
+
{showLabel && (
|
|
397
|
+
<span className="font-mono text-[9px] uppercase tracking-widest text-muted-foreground/40">
|
|
398
|
+
Preset
|
|
399
|
+
</span>
|
|
400
|
+
)}
|
|
272
401
|
<div
|
|
273
402
|
className={cn(
|
|
274
403
|
"h-7 w-24 rounded",
|
|
@@ -287,10 +416,17 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
287
416
|
return (
|
|
288
417
|
<div className="relative" ref={containerRef}>
|
|
289
418
|
{/* Label */}
|
|
290
|
-
<div
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
419
|
+
<div
|
|
420
|
+
className={cn(
|
|
421
|
+
"flex items-start",
|
|
422
|
+
showLabel ? "flex-col gap-2" : "flex-row"
|
|
423
|
+
)}
|
|
424
|
+
>
|
|
425
|
+
{showLabel && (
|
|
426
|
+
<span className="font-mono text-[9px] uppercase tracking-widest text-muted-foreground/40">
|
|
427
|
+
Preset
|
|
428
|
+
</span>
|
|
429
|
+
)}
|
|
294
430
|
|
|
295
431
|
{/* Tube Display Button */}
|
|
296
432
|
<button
|
|
@@ -317,6 +453,7 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
317
453
|
)}
|
|
318
454
|
disabled={switching}
|
|
319
455
|
onClick={() => setOpen(!open)}
|
|
456
|
+
ref={triggerRef}
|
|
320
457
|
type="button"
|
|
321
458
|
>
|
|
322
459
|
{/* Status indicator */}
|
|
@@ -359,155 +496,256 @@ export function AIModelSelector({ onPresetChange }: AIModelSelectorProps = {}) {
|
|
|
359
496
|
</div>
|
|
360
497
|
|
|
361
498
|
{/* Dropdown Panel */}
|
|
362
|
-
{open &&
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
className=
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
499
|
+
{open &&
|
|
500
|
+
menuPosition &&
|
|
501
|
+
createPortal(
|
|
502
|
+
<div
|
|
503
|
+
className={cn(
|
|
504
|
+
"fixed z-[120]",
|
|
505
|
+
"rounded-md border p-1",
|
|
506
|
+
"max-h-[min(420px,calc(100vh-6rem))] overflow-y-auto",
|
|
507
|
+
"border-[hsl(var(--secondary)/0.2)]",
|
|
508
|
+
"bg-card/95 backdrop-blur-sm",
|
|
509
|
+
"shadow-[0_8px_32px_-8px_hsl(var(--secondary)/0.2),0_0_1px_hsl(var(--secondary)/0.1)]",
|
|
510
|
+
"animate-in fade-in-0 zoom-in-95 slide-in-from-top-2",
|
|
511
|
+
"duration-200"
|
|
512
|
+
)}
|
|
513
|
+
ref={menuRef}
|
|
514
|
+
style={{
|
|
515
|
+
left: `${menuPosition.left}px`,
|
|
516
|
+
top: `${menuPosition.top}px`,
|
|
517
|
+
width: `${menuPosition.width}px`,
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
{/* Download progress */}
|
|
521
|
+
{downloading && downloadStatus && (
|
|
522
|
+
<div className="mb-2 space-y-2 rounded bg-[hsl(var(--secondary)/0.05)] p-2">
|
|
523
|
+
<div className="flex items-center justify-between">
|
|
524
|
+
<span className="font-mono text-[10px] text-muted-foreground">
|
|
525
|
+
{downloadStatus.currentType || "Preparing..."}
|
|
526
|
+
</span>
|
|
527
|
+
<span className="font-mono text-[10px] text-[hsl(var(--secondary)/0.7)]">
|
|
528
|
+
{downloadStatus.progress?.percent.toFixed(0) ?? 0}%
|
|
529
|
+
</span>
|
|
530
|
+
</div>
|
|
531
|
+
<div className="relative h-1.5 overflow-hidden rounded-full bg-muted/50">
|
|
532
|
+
<div
|
|
533
|
+
className={cn(
|
|
534
|
+
"absolute inset-y-0 left-0 rounded-full",
|
|
535
|
+
"bg-gradient-to-r from-[hsl(var(--secondary)/0.6)] to-[hsl(var(--secondary))]",
|
|
536
|
+
"shadow-[0_0_8px_hsl(var(--secondary)/0.5)]",
|
|
537
|
+
"transition-all duration-300"
|
|
538
|
+
)}
|
|
539
|
+
style={{
|
|
540
|
+
width: `${downloadStatus.progress?.percent ?? 0}%`,
|
|
541
|
+
}}
|
|
542
|
+
/>
|
|
543
|
+
</div>
|
|
544
|
+
{downloadStatus.completed.length > 0 && (
|
|
545
|
+
<p className="font-mono text-[9px] text-muted-foreground/60">
|
|
546
|
+
Done: {downloadStatus.completed.join(", ")}
|
|
547
|
+
</p>
|
|
548
|
+
)}
|
|
398
549
|
</div>
|
|
399
|
-
|
|
400
|
-
<p className="font-mono text-[9px] text-muted-foreground/60">
|
|
401
|
-
Done: {downloadStatus.completed.join(", ")}
|
|
402
|
-
</p>
|
|
403
|
-
)}
|
|
404
|
-
</div>
|
|
405
|
-
)}
|
|
550
|
+
)}
|
|
406
551
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
552
|
+
<div className="space-y-0.5">
|
|
553
|
+
{presets.map((preset) => {
|
|
554
|
+
const isActive = preset.id === activeId;
|
|
555
|
+
const baseName = extractBaseName(preset.name);
|
|
556
|
+
const size = extractSize(preset.name);
|
|
557
|
+
const explanation =
|
|
558
|
+
PRESET_EXPLANATIONS[preset.id] ??
|
|
559
|
+
"Pick this if the trade-off fits your machine.";
|
|
413
560
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
561
|
+
return (
|
|
562
|
+
<button
|
|
563
|
+
className={cn(
|
|
564
|
+
"group/item flex w-full items-center justify-between gap-3",
|
|
565
|
+
"rounded px-3 py-2.5 text-left",
|
|
566
|
+
"transition-all duration-150",
|
|
567
|
+
"text-muted-foreground",
|
|
568
|
+
!isActive &&
|
|
569
|
+
"hover:bg-[hsl(var(--secondary)/0.08)] hover:text-foreground",
|
|
570
|
+
isActive && [
|
|
571
|
+
"bg-[hsl(var(--secondary)/0.1)]",
|
|
572
|
+
"text-[hsl(var(--secondary))]",
|
|
573
|
+
],
|
|
574
|
+
(switching || downloading) &&
|
|
575
|
+
"pointer-events-none opacity-50"
|
|
576
|
+
)}
|
|
577
|
+
disabled={switching || downloading}
|
|
578
|
+
key={preset.id}
|
|
579
|
+
onClick={() => handleSelect(preset.id)}
|
|
580
|
+
type="button"
|
|
581
|
+
>
|
|
582
|
+
<div className="flex flex-col items-start gap-0.5">
|
|
583
|
+
<span
|
|
584
|
+
className={cn(
|
|
585
|
+
"font-medium text-sm",
|
|
586
|
+
isActive && "text-[hsl(var(--secondary))]"
|
|
587
|
+
)}
|
|
588
|
+
>
|
|
589
|
+
{baseName}
|
|
590
|
+
</span>
|
|
591
|
+
{size && (
|
|
592
|
+
<span className="font-mono text-[10px] text-muted-foreground/60">
|
|
593
|
+
{size}
|
|
594
|
+
</span>
|
|
595
|
+
)}
|
|
596
|
+
<span className="text-[10px] text-muted-foreground/80">
|
|
597
|
+
{explanation}
|
|
598
|
+
</span>
|
|
599
|
+
<span className="font-mono text-[9px] text-muted-foreground/50">
|
|
600
|
+
{`expand: ${formatModelRole(preset.expand ?? preset.gen)}`}
|
|
601
|
+
</span>
|
|
602
|
+
<span className="font-mono text-[9px] text-muted-foreground/50">
|
|
603
|
+
{`answer: ${formatModelRole(preset.gen)}`}
|
|
604
|
+
</span>
|
|
605
|
+
</div>
|
|
606
|
+
|
|
607
|
+
{isActive && (
|
|
608
|
+
<Check className="size-4 text-[hsl(var(--secondary))]" />
|
|
609
|
+
)}
|
|
610
|
+
</button>
|
|
611
|
+
);
|
|
612
|
+
})}
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
{(error || modelsNeeded) && !downloading && (
|
|
616
|
+
<>
|
|
617
|
+
<div className="my-1 border-t border-border/50" />
|
|
618
|
+
<div className="space-y-2 p-2">
|
|
619
|
+
{error && (
|
|
620
|
+
<p className="font-mono text-[10px] text-amber-500">
|
|
621
|
+
{error}
|
|
622
|
+
</p>
|
|
433
623
|
)}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
onClick={() => handleSelect(preset.id)}
|
|
437
|
-
type="button"
|
|
438
|
-
>
|
|
439
|
-
<div className="flex flex-col items-start gap-0.5">
|
|
440
|
-
<span
|
|
624
|
+
{modelsNeeded && (
|
|
625
|
+
<button
|
|
441
626
|
className={cn(
|
|
442
|
-
"
|
|
443
|
-
|
|
627
|
+
"flex w-full items-center justify-center gap-2",
|
|
628
|
+
"rounded border px-3 py-2",
|
|
629
|
+
"border-[hsl(var(--secondary)/0.3)]",
|
|
630
|
+
"bg-[hsl(var(--secondary)/0.05)]",
|
|
631
|
+
"font-medium text-[hsl(var(--secondary))] text-xs",
|
|
632
|
+
"transition-all duration-200",
|
|
633
|
+
"hover:border-[hsl(var(--secondary)/0.5)]",
|
|
634
|
+
"hover:bg-[hsl(var(--secondary)/0.1)]",
|
|
635
|
+
"hover:shadow-[0_0_12px_-4px_hsl(var(--secondary)/0.3)]"
|
|
444
636
|
)}
|
|
637
|
+
onClick={handleDownload}
|
|
638
|
+
type="button"
|
|
445
639
|
>
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
<span className="font-mono text-[9px] text-muted-foreground/50">
|
|
454
|
-
{`expand: ${formatModelRole(preset.expand ?? preset.gen)}`}
|
|
455
|
-
</span>
|
|
456
|
-
<span className="font-mono text-[9px] text-muted-foreground/50">
|
|
457
|
-
{`answer: ${formatModelRole(preset.gen)}`}
|
|
458
|
-
</span>
|
|
459
|
-
</div>
|
|
640
|
+
<Download className="size-3.5" />
|
|
641
|
+
Download Preset Models
|
|
642
|
+
</button>
|
|
643
|
+
)}
|
|
644
|
+
</div>
|
|
645
|
+
</>
|
|
646
|
+
)}
|
|
460
647
|
|
|
461
|
-
|
|
462
|
-
|
|
648
|
+
<div className="mt-1 border-t border-border/30 px-3 py-2">
|
|
649
|
+
<p className="font-mono text-[9px] text-muted-foreground/50">
|
|
650
|
+
Controls retrieval expansion and AI answers
|
|
651
|
+
</p>
|
|
652
|
+
</div>
|
|
653
|
+
</div>,
|
|
654
|
+
document.body
|
|
655
|
+
)}
|
|
656
|
+
|
|
657
|
+
{showDetails && activePreset && (
|
|
658
|
+
<div className="mt-4 rounded-2xl border border-secondary/20 bg-background/80 shadow-sm">
|
|
659
|
+
<div
|
|
660
|
+
className="border-border/50 border-b"
|
|
661
|
+
style={{ padding: "20px 24px" }}
|
|
662
|
+
>
|
|
663
|
+
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
664
|
+
<div>
|
|
665
|
+
<div className="mb-3 flex flex-wrap items-center gap-2">
|
|
666
|
+
<div className="font-semibold text-base">
|
|
667
|
+
{activePreset.name}
|
|
668
|
+
</div>
|
|
669
|
+
{extractSize(activePreset.name) && (
|
|
670
|
+
<Badge variant="outline">
|
|
671
|
+
{extractSize(activePreset.name)}
|
|
672
|
+
</Badge>
|
|
463
673
|
)}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
674
|
+
{isCustomPreset(activePreset) && (
|
|
675
|
+
<Badge className="border-secondary/30 bg-secondary/10 text-secondary hover:bg-secondary/10">
|
|
676
|
+
Tuned
|
|
677
|
+
</Badge>
|
|
678
|
+
)}
|
|
679
|
+
</div>
|
|
680
|
+
<p className="text-muted-foreground text-sm">
|
|
681
|
+
{activeExplanation}
|
|
682
|
+
</p>
|
|
683
|
+
</div>
|
|
684
|
+
<Badge variant="outline">{`${presets.length} presets available`}</Badge>
|
|
685
|
+
</div>
|
|
467
686
|
</div>
|
|
468
687
|
|
|
469
|
-
{
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
"
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
"hover:border-[hsl(var(--secondary)/0.5)]",
|
|
489
|
-
"hover:bg-[hsl(var(--secondary)/0.1)]",
|
|
490
|
-
"hover:shadow-[0_0_12px_-4px_hsl(var(--secondary)/0.3)]"
|
|
491
|
-
)}
|
|
492
|
-
onClick={handleDownload}
|
|
493
|
-
type="button"
|
|
494
|
-
>
|
|
495
|
-
<Download className="size-3.5" />
|
|
496
|
-
Download Preset Models
|
|
497
|
-
</button>
|
|
498
|
-
)}
|
|
688
|
+
<div style={{ padding: "20px 24px" }}>
|
|
689
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
690
|
+
<div
|
|
691
|
+
className="rounded-xl border border-border/60 bg-card/70 shadow-none"
|
|
692
|
+
style={{ padding: "16px" }}
|
|
693
|
+
>
|
|
694
|
+
<div className="mb-2 flex items-center gap-2 font-medium text-sm">
|
|
695
|
+
<ScanSearch className="size-4 text-secondary" />
|
|
696
|
+
Retrieval profile
|
|
697
|
+
</div>
|
|
698
|
+
<p className="text-muted-foreground text-sm">
|
|
699
|
+
{activePreset.id === "slim"
|
|
700
|
+
? "Quickest local setup with the lightest footprint."
|
|
701
|
+
: activePreset.id === "balanced"
|
|
702
|
+
? "Good general-purpose trade-off for most projects."
|
|
703
|
+
: activePreset.id === "quality"
|
|
704
|
+
? "Highest local answer quality with heavier resource use."
|
|
705
|
+
: "Custom tuned profile layered on top of the built-in options."}
|
|
706
|
+
</p>
|
|
499
707
|
</div>
|
|
500
|
-
|
|
501
|
-
|
|
708
|
+
<div
|
|
709
|
+
className="rounded-xl border border-border/60 bg-card/70 shadow-none"
|
|
710
|
+
style={{ padding: "16px" }}
|
|
711
|
+
>
|
|
712
|
+
<div className="mb-2 flex items-center gap-2 font-medium text-sm">
|
|
713
|
+
<BadgeCheck className="size-4 text-secondary" />
|
|
714
|
+
Active models
|
|
715
|
+
</div>
|
|
716
|
+
<div className="space-y-1 font-mono text-[11px] text-muted-foreground">
|
|
717
|
+
<div>{`expand: ${formatModelRole(activePreset.expand ?? activePreset.gen)}`}</div>
|
|
718
|
+
<div>{`answer: ${formatModelRole(activePreset.gen)}`}</div>
|
|
719
|
+
<div>{`rerank: ${formatModelRole(activePreset.rerank)}`}</div>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
502
723
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
724
|
+
{showDownloadAction && (error || modelsNeeded) && !downloading && (
|
|
725
|
+
<div className="mt-4 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-500/25 bg-amber-500/10 px-4 py-4">
|
|
726
|
+
<p className="text-amber-500 text-sm">
|
|
727
|
+
{error ?? "This preset still needs local model files."}
|
|
728
|
+
</p>
|
|
729
|
+
<button
|
|
730
|
+
className={cn(
|
|
731
|
+
"flex items-center justify-center gap-2 rounded border px-3 py-2",
|
|
732
|
+
"border-[hsl(var(--secondary)/0.3)] bg-[hsl(var(--secondary)/0.05)]",
|
|
733
|
+
"font-medium text-[hsl(var(--secondary))] text-xs transition-all duration-200",
|
|
734
|
+
"hover:border-[hsl(var(--secondary)/0.5)] hover:bg-[hsl(var(--secondary)/0.1)]"
|
|
735
|
+
)}
|
|
736
|
+
onClick={handleDownload}
|
|
737
|
+
type="button"
|
|
738
|
+
>
|
|
739
|
+
<Download className="size-3.5" />
|
|
740
|
+
Download preset models
|
|
741
|
+
</button>
|
|
742
|
+
</div>
|
|
743
|
+
)}
|
|
508
744
|
</div>
|
|
509
745
|
</div>
|
|
510
746
|
)}
|
|
747
|
+
|
|
748
|
+
{notice && <div className="mt-2 text-primary text-xs">{notice}</div>}
|
|
511
749
|
</div>
|
|
512
750
|
);
|
|
513
751
|
}
|