@gmickel/gno 0.37.0 → 0.39.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 +35 -96
- package/assets/skill/SKILL.md +29 -0
- package/assets/skill/cli-reference.md +11 -1
- package/package.json +3 -1
- package/src/cli/commands/collection/add.ts +6 -0
- package/src/cli/commands/collection/clear-embeddings.ts +83 -0
- package/src/cli/commands/collection/index.ts +1 -0
- package/src/cli/commands/models/use.ts +19 -4
- package/src/cli/commands/status.ts +4 -1
- package/src/cli/program.ts +16 -0
- package/src/collection/add.ts +1 -0
- package/src/collection/index.ts +2 -0
- package/src/collection/types.ts +13 -0
- package/src/collection/update.ts +93 -0
- package/src/config/types.ts +4 -4
- package/src/llm/registry.ts +53 -0
- package/src/mcp/tools/status.ts +4 -1
- package/src/sdk/client.ts +5 -1
- package/src/serve/public/components/AIModelSelector.tsx +8 -1
- package/src/serve/public/components/CollectionModelDialog.tsx +399 -0
- package/src/serve/public/pages/Collections.tsx +234 -7
- package/src/serve/routes/api.ts +221 -17
- package/src/serve/server.ts +38 -1
- package/src/serve/status.ts +4 -2
- package/src/store/sqlite/adapter.ts +148 -9
- package/src/store/types.ts +19 -1
- package/src/store/vector/sqlite-vec.ts +1 -1
package/src/llm/registry.ts
CHANGED
|
@@ -15,6 +15,10 @@ import type { ModelType } from "./types";
|
|
|
15
15
|
|
|
16
16
|
import { DEFAULT_MODEL_PRESETS } from "../config/types";
|
|
17
17
|
|
|
18
|
+
export type ModelResolutionSource = "override" | "preset" | "default";
|
|
19
|
+
export type ModelResolutionMap = Record<ModelType, string>;
|
|
20
|
+
export type ModelResolutionSourceMap = Record<ModelType, ModelResolutionSource>;
|
|
21
|
+
|
|
18
22
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
23
|
// Registry Functions
|
|
20
24
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -130,6 +134,55 @@ export function resolveModelUri(
|
|
|
130
134
|
return preset[type];
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
export function resolveModelSource(
|
|
138
|
+
config: Config,
|
|
139
|
+
type: ModelType,
|
|
140
|
+
override?: string,
|
|
141
|
+
collection?: string
|
|
142
|
+
): ModelResolutionSource {
|
|
143
|
+
if (override) {
|
|
144
|
+
return "override";
|
|
145
|
+
}
|
|
146
|
+
const collectionModels = getCollectionModelOverrides(config, collection);
|
|
147
|
+
if (collectionModels?.[type]) {
|
|
148
|
+
return "override";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const modelConfig = getModelConfig(config);
|
|
152
|
+
const preset = modelConfig.presets.find(
|
|
153
|
+
(p) => p.id === modelConfig.activePreset
|
|
154
|
+
);
|
|
155
|
+
if (preset) {
|
|
156
|
+
return "preset";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return "default";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getCollectionEffectiveModels(
|
|
163
|
+
config: Config,
|
|
164
|
+
collection?: string
|
|
165
|
+
): ModelResolutionMap {
|
|
166
|
+
return {
|
|
167
|
+
embed: resolveModelUri(config, "embed", undefined, collection),
|
|
168
|
+
rerank: resolveModelUri(config, "rerank", undefined, collection),
|
|
169
|
+
expand: resolveModelUri(config, "expand", undefined, collection),
|
|
170
|
+
gen: resolveModelUri(config, "gen", undefined, collection),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function getCollectionModelSources(
|
|
175
|
+
config: Config,
|
|
176
|
+
collection?: string
|
|
177
|
+
): ModelResolutionSourceMap {
|
|
178
|
+
return {
|
|
179
|
+
embed: resolveModelSource(config, "embed", undefined, collection),
|
|
180
|
+
rerank: resolveModelSource(config, "rerank", undefined, collection),
|
|
181
|
+
expand: resolveModelSource(config, "expand", undefined, collection),
|
|
182
|
+
gen: resolveModelSource(config, "gen", undefined, collection),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
133
186
|
/**
|
|
134
187
|
* List all available presets.
|
|
135
188
|
*/
|
package/src/mcp/tools/status.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { IndexStatus } from "../../store/types";
|
|
8
8
|
import type { ToolContext } from "../server";
|
|
9
9
|
|
|
10
|
+
import { resolveModelUri } from "../../llm/registry";
|
|
10
11
|
import { runTool, type ToolResult } from "./index";
|
|
11
12
|
|
|
12
13
|
type StatusInput = Record<string, never>;
|
|
@@ -66,7 +67,9 @@ export function handleStatus(
|
|
|
66
67
|
ctx,
|
|
67
68
|
"gno_status",
|
|
68
69
|
async () => {
|
|
69
|
-
const result = await ctx.store.getStatus(
|
|
70
|
+
const result = await ctx.store.getStatus({
|
|
71
|
+
embedModel: resolveModelUri(ctx.config, "embed"),
|
|
72
|
+
});
|
|
70
73
|
if (!result.ok) {
|
|
71
74
|
throw new Error(result.error.message);
|
|
72
75
|
}
|
package/src/sdk/client.ts
CHANGED
|
@@ -621,7 +621,11 @@ class GnoClientImpl implements GnoClient {
|
|
|
621
621
|
|
|
622
622
|
async status(): Promise<IndexStatus> {
|
|
623
623
|
this.assertOpen();
|
|
624
|
-
return unwrapStore(
|
|
624
|
+
return unwrapStore(
|
|
625
|
+
await this.store.getStatus({
|
|
626
|
+
embedModel: resolveModelUri(this.config, "embed"),
|
|
627
|
+
})
|
|
628
|
+
);
|
|
625
629
|
}
|
|
626
630
|
|
|
627
631
|
async update(options: GnoUpdateOptions = {}): Promise<SyncResult> {
|
|
@@ -55,6 +55,8 @@ interface SetPresetResponse {
|
|
|
55
55
|
success: boolean;
|
|
56
56
|
activePreset: string;
|
|
57
57
|
capabilities: Capabilities;
|
|
58
|
+
embedModelChanged?: boolean;
|
|
59
|
+
note?: string;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
interface DownloadProgress {
|
|
@@ -355,7 +357,12 @@ export function AIModelSelector({
|
|
|
355
357
|
checkCapabilities(data.capabilities);
|
|
356
358
|
setOpen(false);
|
|
357
359
|
const presetName = presets.find((preset) => preset.id === id)?.name ?? id;
|
|
358
|
-
setNotice(
|
|
360
|
+
setNotice(
|
|
361
|
+
data.embedModelChanged
|
|
362
|
+
? (data.note ??
|
|
363
|
+
`Switched to ${presetName}. Run embeddings again so vector results catch up.`)
|
|
364
|
+
: `Switched to ${presetName}`
|
|
365
|
+
);
|
|
359
366
|
const { data: statusData } =
|
|
360
367
|
await apiFetch<AppStatusResponse>("/api/status");
|
|
361
368
|
await syncFromStatus(statusData);
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertTriangleIcon,
|
|
3
|
+
CpuIcon,
|
|
4
|
+
Loader2Icon,
|
|
5
|
+
RotateCcwIcon,
|
|
6
|
+
SparklesIcon,
|
|
7
|
+
} from "lucide-react";
|
|
8
|
+
import { useEffect, useMemo, useState } from "react";
|
|
9
|
+
|
|
10
|
+
import { apiFetch } from "../hooks/use-api";
|
|
11
|
+
import { Button } from "./ui/button";
|
|
12
|
+
import {
|
|
13
|
+
Dialog,
|
|
14
|
+
DialogContent,
|
|
15
|
+
DialogDescription,
|
|
16
|
+
DialogFooter,
|
|
17
|
+
DialogHeader,
|
|
18
|
+
DialogTitle,
|
|
19
|
+
} from "./ui/dialog";
|
|
20
|
+
import { Input } from "./ui/input";
|
|
21
|
+
|
|
22
|
+
const MODEL_ROLES = ["embed", "rerank", "expand", "gen"] as const;
|
|
23
|
+
|
|
24
|
+
type ModelRole = (typeof MODEL_ROLES)[number];
|
|
25
|
+
type ModelSource = "override" | "preset" | "default";
|
|
26
|
+
|
|
27
|
+
const ROLE_LABELS: Record<ModelRole, string> = {
|
|
28
|
+
embed: "Embedding",
|
|
29
|
+
rerank: "Reranker",
|
|
30
|
+
expand: "Query Expansion",
|
|
31
|
+
gen: "Answer Model",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ROLE_NOTES: Record<ModelRole, string> = {
|
|
35
|
+
embed: "Drives vector search and embedding backlog for this collection.",
|
|
36
|
+
rerank: "Scores candidate passages/documents after retrieval.",
|
|
37
|
+
expand:
|
|
38
|
+
"Generates lexical and semantic expansion variants for harder queries.",
|
|
39
|
+
gen: "Used for collection-targeted answer generation flows.",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface CollectionModelDetails {
|
|
43
|
+
activePresetId?: string;
|
|
44
|
+
chunkCount: number;
|
|
45
|
+
documentCount: number;
|
|
46
|
+
effectiveModels?: Record<ModelRole, string>;
|
|
47
|
+
include?: string[];
|
|
48
|
+
modelSources?: Record<ModelRole, ModelSource>;
|
|
49
|
+
models?: Partial<Record<ModelRole, string>>;
|
|
50
|
+
name: string;
|
|
51
|
+
path: string;
|
|
52
|
+
pattern?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface UpdateCollectionResponse {
|
|
56
|
+
collection: CollectionModelDetails;
|
|
57
|
+
success: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface CollectionModelDialogProps {
|
|
61
|
+
collection: CollectionModelDetails | null;
|
|
62
|
+
onOpenChange: (open: boolean) => void;
|
|
63
|
+
onSaved: () => void;
|
|
64
|
+
open: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeValue(value: string | undefined): string {
|
|
68
|
+
return value?.trim() ?? "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const CODE_EMBED_RECOMMENDATION =
|
|
72
|
+
"hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf";
|
|
73
|
+
|
|
74
|
+
const CODE_PATH_HINTS = [
|
|
75
|
+
"/src",
|
|
76
|
+
"/lib",
|
|
77
|
+
"/app",
|
|
78
|
+
"/apps",
|
|
79
|
+
"/packages",
|
|
80
|
+
"/server",
|
|
81
|
+
"/services",
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
const CODE_EXT_HINTS = [
|
|
85
|
+
".ts",
|
|
86
|
+
".tsx",
|
|
87
|
+
".js",
|
|
88
|
+
".jsx",
|
|
89
|
+
".go",
|
|
90
|
+
".rs",
|
|
91
|
+
".py",
|
|
92
|
+
".swift",
|
|
93
|
+
".c",
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
function collectionLooksCodeHeavy(collection: CollectionModelDetails): boolean {
|
|
97
|
+
const path = collection.path.toLowerCase();
|
|
98
|
+
if (
|
|
99
|
+
CODE_PATH_HINTS.some(
|
|
100
|
+
(hint) => path.endsWith(hint) || path.includes(`${hint}/`)
|
|
101
|
+
)
|
|
102
|
+
) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const includeValues = collection.include ?? [];
|
|
107
|
+
if (
|
|
108
|
+
includeValues.some((value) =>
|
|
109
|
+
CODE_EXT_HINTS.some((ext) => value.includes(ext))
|
|
110
|
+
)
|
|
111
|
+
) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pattern = collection.pattern?.toLowerCase() ?? "";
|
|
116
|
+
return CODE_EXT_HINTS.some(
|
|
117
|
+
(ext) => pattern.includes(ext) || pattern.includes(ext.replace(".", ""))
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function CollectionModelDialog({
|
|
122
|
+
collection,
|
|
123
|
+
onOpenChange,
|
|
124
|
+
onSaved,
|
|
125
|
+
open,
|
|
126
|
+
}: CollectionModelDialogProps) {
|
|
127
|
+
const [draft, setDraft] = useState<Record<ModelRole, string>>({
|
|
128
|
+
embed: "",
|
|
129
|
+
rerank: "",
|
|
130
|
+
expand: "",
|
|
131
|
+
gen: "",
|
|
132
|
+
});
|
|
133
|
+
const [error, setError] = useState<string | null>(null);
|
|
134
|
+
const [saving, setSaving] = useState(false);
|
|
135
|
+
const showCodeRecommendation =
|
|
136
|
+
collection !== null &&
|
|
137
|
+
collectionLooksCodeHeavy(collection) &&
|
|
138
|
+
collection.effectiveModels?.embed !== CODE_EMBED_RECOMMENDATION;
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!open || !collection) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setDraft({
|
|
146
|
+
embed: collection.models?.embed ?? "",
|
|
147
|
+
rerank: collection.models?.rerank ?? "",
|
|
148
|
+
expand: collection.models?.expand ?? "",
|
|
149
|
+
gen: collection.models?.gen ?? "",
|
|
150
|
+
});
|
|
151
|
+
setError(null);
|
|
152
|
+
setSaving(false);
|
|
153
|
+
}, [collection, open]);
|
|
154
|
+
|
|
155
|
+
const patch = useMemo(() => {
|
|
156
|
+
if (!collection) {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const nextPatch: Partial<Record<ModelRole, string | null>> = {};
|
|
161
|
+
for (const role of MODEL_ROLES) {
|
|
162
|
+
const original = normalizeValue(collection.models?.[role]);
|
|
163
|
+
const current = normalizeValue(draft[role]);
|
|
164
|
+
if (original === current) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
nextPatch[role] = current.length === 0 ? null : current;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return nextPatch;
|
|
171
|
+
}, [collection, draft]);
|
|
172
|
+
|
|
173
|
+
const hasChanges = Object.keys(patch).length > 0;
|
|
174
|
+
const embedChanged = Object.hasOwn(patch, "embed");
|
|
175
|
+
|
|
176
|
+
const handleSave = async () => {
|
|
177
|
+
if (!collection || !hasChanges) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setSaving(true);
|
|
182
|
+
setError(null);
|
|
183
|
+
|
|
184
|
+
const { error: requestError } = await apiFetch<UpdateCollectionResponse>(
|
|
185
|
+
`/api/collections/${encodeURIComponent(collection.name)}`,
|
|
186
|
+
{
|
|
187
|
+
method: "PATCH",
|
|
188
|
+
body: JSON.stringify({ models: patch }),
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
setSaving(false);
|
|
193
|
+
|
|
194
|
+
if (requestError) {
|
|
195
|
+
setError(requestError);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onSaved();
|
|
200
|
+
onOpenChange(false);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Dialog onOpenChange={onOpenChange} open={open}>
|
|
205
|
+
<DialogContent className="flex max-h-[85vh] max-w-3xl flex-col gap-0 overflow-x-hidden border-none bg-[#0f1115] p-0 shadow-[0_30px_90px_-35px_rgba(0,0,0,0.8)]">
|
|
206
|
+
{/* Header */}
|
|
207
|
+
<DialogHeader className="shrink-0 border-border/20 border-b px-6 pt-5 pb-4 text-left">
|
|
208
|
+
<div className="flex items-start justify-between gap-4">
|
|
209
|
+
<div className="min-w-0 space-y-1.5">
|
|
210
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
|
211
|
+
<span className="rounded bg-muted/40 px-2 py-0.5 font-mono text-[10px] text-muted-foreground/70 uppercase tracking-[0.12em]">
|
|
212
|
+
Collection models
|
|
213
|
+
</span>
|
|
214
|
+
<span className="rounded bg-muted/30 px-2 py-0.5 font-mono text-[10px] text-muted-foreground/50 tracking-[0.05em]">
|
|
215
|
+
preset: {collection?.activePresetId ?? "unknown"}
|
|
216
|
+
</span>
|
|
217
|
+
</div>
|
|
218
|
+
<DialogTitle className="font-[Iowan_Old_Style,Palatino_Linotype,Palatino,Book_Antiqua,Georgia,serif] text-2xl leading-tight">
|
|
219
|
+
{collection?.name ?? "Collection"}
|
|
220
|
+
</DialogTitle>
|
|
221
|
+
<DialogDescription className="text-muted-foreground/70 text-[13px]">
|
|
222
|
+
Override model roles for one collection without changing the
|
|
223
|
+
active preset for the rest of the workspace.
|
|
224
|
+
</DialogDescription>
|
|
225
|
+
</div>
|
|
226
|
+
<div className="hidden shrink-0 max-w-[200px] space-y-1 border-border/15 border-l pl-4 lg:block">
|
|
227
|
+
<p className="font-mono text-[10px] text-muted-foreground/50 uppercase tracking-[0.15em]">
|
|
228
|
+
Path
|
|
229
|
+
</p>
|
|
230
|
+
<p
|
|
231
|
+
className="break-all font-mono text-[10px] leading-relaxed text-muted-foreground/50"
|
|
232
|
+
title={collection?.path}
|
|
233
|
+
>
|
|
234
|
+
{collection?.path}
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</DialogHeader>
|
|
239
|
+
|
|
240
|
+
{/* Scrollable model roles */}
|
|
241
|
+
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-hidden">
|
|
242
|
+
<div className="divide-y divide-border/15">
|
|
243
|
+
{MODEL_ROLES.map((role) => {
|
|
244
|
+
const source = collection?.modelSources?.[role] ?? "preset";
|
|
245
|
+
const effectiveValue = collection?.effectiveModels?.[role] ?? "";
|
|
246
|
+
const draftValue = draft[role];
|
|
247
|
+
const isOverride = source === "override";
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
className="grid gap-x-5 gap-y-3 px-6 py-4 lg:grid-cols-[180px_minmax(0,1fr)]"
|
|
252
|
+
key={role}
|
|
253
|
+
>
|
|
254
|
+
{/* Left: role info */}
|
|
255
|
+
<div className="space-y-1">
|
|
256
|
+
<div className="flex items-center gap-2">
|
|
257
|
+
<CpuIcon className="size-3.5 text-secondary/70" />
|
|
258
|
+
<h3 className="font-medium text-[13px]">
|
|
259
|
+
{ROLE_LABELS[role]}
|
|
260
|
+
</h3>
|
|
261
|
+
</div>
|
|
262
|
+
<p className="text-muted-foreground/50 text-xs leading-relaxed">
|
|
263
|
+
{ROLE_NOTES[role]}
|
|
264
|
+
</p>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Right: controls */}
|
|
268
|
+
<div className="space-y-2.5">
|
|
269
|
+
{/* Source + effective model */}
|
|
270
|
+
<div className="flex items-baseline gap-2">
|
|
271
|
+
<span
|
|
272
|
+
className={`shrink-0 rounded px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-[0.1em] ${
|
|
273
|
+
isOverride
|
|
274
|
+
? "bg-secondary/15 text-secondary/80"
|
|
275
|
+
: "bg-muted/40 text-muted-foreground/50"
|
|
276
|
+
}`}
|
|
277
|
+
>
|
|
278
|
+
{isOverride ? "override" : "inherits"}
|
|
279
|
+
</span>
|
|
280
|
+
<span className="font-mono text-[9px] text-muted-foreground/35 uppercase tracking-[0.12em]">
|
|
281
|
+
effective
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
<p
|
|
285
|
+
className="break-all font-mono text-[11px] leading-relaxed text-foreground/70"
|
|
286
|
+
title={effectiveValue}
|
|
287
|
+
>
|
|
288
|
+
{effectiveValue}
|
|
289
|
+
</p>
|
|
290
|
+
|
|
291
|
+
{/* Code embed recommendation */}
|
|
292
|
+
{role === "embed" && showCodeRecommendation ? (
|
|
293
|
+
<button
|
|
294
|
+
className="flex w-full cursor-pointer items-center gap-2.5 rounded-md border border-secondary/15 bg-secondary/5 px-3 py-2 text-left transition-colors hover:border-secondary/25 hover:bg-secondary/8"
|
|
295
|
+
onClick={() =>
|
|
296
|
+
setDraft((current) => ({
|
|
297
|
+
...current,
|
|
298
|
+
embed: CODE_EMBED_RECOMMENDATION,
|
|
299
|
+
}))
|
|
300
|
+
}
|
|
301
|
+
type="button"
|
|
302
|
+
>
|
|
303
|
+
<SparklesIcon className="size-3.5 shrink-0 text-secondary/60" />
|
|
304
|
+
<div className="min-w-0">
|
|
305
|
+
<p className="text-[11px] text-foreground/70">
|
|
306
|
+
Apply code-optimized embedding
|
|
307
|
+
</p>
|
|
308
|
+
<p className="truncate font-mono text-[10px] text-muted-foreground/40">
|
|
309
|
+
{CODE_EMBED_RECOMMENDATION}
|
|
310
|
+
</p>
|
|
311
|
+
</div>
|
|
312
|
+
</button>
|
|
313
|
+
) : null}
|
|
314
|
+
|
|
315
|
+
{/* Input + reset */}
|
|
316
|
+
<div className="flex items-center gap-1.5">
|
|
317
|
+
<Input
|
|
318
|
+
className="h-8 border-border/20 bg-muted/10 font-mono text-[11px] placeholder:text-muted-foreground/25"
|
|
319
|
+
onChange={(event) =>
|
|
320
|
+
setDraft((current) => ({
|
|
321
|
+
...current,
|
|
322
|
+
[role]: event.target.value,
|
|
323
|
+
}))
|
|
324
|
+
}
|
|
325
|
+
placeholder="Leave empty to inherit from preset"
|
|
326
|
+
value={draftValue}
|
|
327
|
+
/>
|
|
328
|
+
<Button
|
|
329
|
+
className="size-8 shrink-0 border-border/20 text-muted-foreground/30 hover:text-muted-foreground/60"
|
|
330
|
+
disabled={draftValue.trim().length === 0}
|
|
331
|
+
onClick={() =>
|
|
332
|
+
setDraft((current) => ({
|
|
333
|
+
...current,
|
|
334
|
+
[role]: "",
|
|
335
|
+
}))
|
|
336
|
+
}
|
|
337
|
+
size="icon-sm"
|
|
338
|
+
variant="outline"
|
|
339
|
+
>
|
|
340
|
+
<RotateCcwIcon className="size-3" />
|
|
341
|
+
</Button>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
})}
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
{/* Warnings */}
|
|
350
|
+
{collection && embedChanged && collection.documentCount > 0 ? (
|
|
351
|
+
<div className="border-border/15 border-t px-6 py-3">
|
|
352
|
+
<div className="flex items-start gap-2.5 rounded-md border border-secondary/15 bg-secondary/5 px-3 py-2.5">
|
|
353
|
+
<AlertTriangleIcon className="mt-0.5 size-3.5 shrink-0 text-secondary/60" />
|
|
354
|
+
<div>
|
|
355
|
+
<p className="font-medium text-secondary/80 text-xs">
|
|
356
|
+
Re-index needed after save
|
|
357
|
+
</p>
|
|
358
|
+
<p className="mt-0.5 text-muted-foreground/50 text-xs">
|
|
359
|
+
{collection.documentCount} docs / {collection.chunkCount}{" "}
|
|
360
|
+
chunks will need re-embedding for the new model.
|
|
361
|
+
</p>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
) : null}
|
|
366
|
+
|
|
367
|
+
{error ? (
|
|
368
|
+
<div className="border-border/15 border-t px-6 py-3">
|
|
369
|
+
<div className="rounded-md border border-destructive/25 bg-destructive/8 px-3 py-2.5 text-destructive text-xs">
|
|
370
|
+
{error}
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
) : null}
|
|
374
|
+
</div>
|
|
375
|
+
|
|
376
|
+
{/* Footer — always visible */}
|
|
377
|
+
<DialogFooter className="shrink-0 border-border/20 border-t px-6 py-3">
|
|
378
|
+
<Button
|
|
379
|
+
className="border-border/20 text-xs"
|
|
380
|
+
onClick={() => onOpenChange(false)}
|
|
381
|
+
size="sm"
|
|
382
|
+
variant="outline"
|
|
383
|
+
>
|
|
384
|
+
Cancel
|
|
385
|
+
</Button>
|
|
386
|
+
<Button
|
|
387
|
+
className="text-xs"
|
|
388
|
+
disabled={!hasChanges || saving}
|
|
389
|
+
onClick={() => void handleSave()}
|
|
390
|
+
size="sm"
|
|
391
|
+
>
|
|
392
|
+
{saving ? <Loader2Icon className="size-3.5 animate-spin" /> : null}
|
|
393
|
+
Save model settings
|
|
394
|
+
</Button>
|
|
395
|
+
</DialogFooter>
|
|
396
|
+
</DialogContent>
|
|
397
|
+
</Dialog>
|
|
398
|
+
);
|
|
399
|
+
}
|