@klarkxy/dsh-model-picker 0.0.0-stage → 0.1.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/LICENSE +42 -0
- package/README.md +26 -2
- package/cordis.patch.yml +5 -0
- package/docs/README.zh-CN.md +27 -0
- package/icon.svg +13 -0
- package/lib/client.inner.cjs +1720 -0
- package/lib/client.inner.cjs.map +1 -0
- package/lib/client.js +1729 -0
- package/lib/index.d.ts +12 -0
- package/lib/index.js +13 -0
- package/lib/index.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +115 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,1729 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@klarkxy/dsh-model-picker",
|
|
3
|
+
factory: (dshRequire) => {
|
|
4
|
+
var require = dshRequire;
|
|
5
|
+
var module = { exports: {} };
|
|
6
|
+
var exports = module.exports;
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
8
|
+
let react = require("react");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
let react_dom = require("react-dom");
|
|
11
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
12
|
+
//#region ../dsh-model-route/src/menu.ts
|
|
13
|
+
/** Localize standard effort ids only when the provider has not supplied a display name. */
|
|
14
|
+
function modelMenuEffortLabel(effort, locale) {
|
|
15
|
+
if (effort.name !== effort.id) return effort.name;
|
|
16
|
+
return {
|
|
17
|
+
off: ["关闭", "Off"],
|
|
18
|
+
none: ["无", "None"],
|
|
19
|
+
minimal: ["最低", "Minimal"],
|
|
20
|
+
low: ["低", "Low"],
|
|
21
|
+
medium: ["中", "Medium"],
|
|
22
|
+
high: ["高", "High"],
|
|
23
|
+
xhigh: ["极高", "Extra high"],
|
|
24
|
+
max: ["最高", "Maximum"],
|
|
25
|
+
ultra: ["超高", "Ultra"]
|
|
26
|
+
}[effort.id]?.[locale?.toLowerCase().startsWith("zh") ? 0 : 1] ?? effort.name;
|
|
27
|
+
}
|
|
28
|
+
/** The short model label shown inside a provider group. */
|
|
29
|
+
function modelMenuShortName(choice) {
|
|
30
|
+
const sep = choice.label.indexOf(" / ");
|
|
31
|
+
return sep > 0 ? choice.label.slice(sep + 3) : choice.model;
|
|
32
|
+
}
|
|
33
|
+
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
34
|
+
function groupModelMenuChoices(choices, query) {
|
|
35
|
+
const needle = query.trim().toLowerCase();
|
|
36
|
+
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle) || (choice.providerLabel?.toLowerCase().includes(needle) ?? false)) : choices;
|
|
37
|
+
const groups = [];
|
|
38
|
+
for (const choice of matched) {
|
|
39
|
+
const sep = choice.label.indexOf(" / ");
|
|
40
|
+
const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
|
|
41
|
+
const last = groups.at(-1);
|
|
42
|
+
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
43
|
+
else groups.push({
|
|
44
|
+
provider: choice.provider,
|
|
45
|
+
name,
|
|
46
|
+
items: [choice]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return groups;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region ../dsh-model-route/src/ui.tsx
|
|
53
|
+
/**
|
|
54
|
+
* ModelMenu: the standard editor of one model-route field — the visible half
|
|
55
|
+
* of the x-model-route contract. A feature plugin's settings page renders it
|
|
56
|
+
* for its own marked field; the model hub renders one per detected field.
|
|
57
|
+
*
|
|
58
|
+
* It is a plugin-side copy of the host's composer model picker
|
|
59
|
+
* (dsh-client-ui-model-selection's ModelSelect), rebuilt from the official
|
|
60
|
+
* primitives and the published bundle's behavior: one card that opens on the
|
|
61
|
+
* root pane (Model / Effort cells), drills into the provider-grouped,
|
|
62
|
+
* searchable model list or the effort list, and Escapes back before closing.
|
|
63
|
+
* Sticky group headings, subsequence ranking, highlight walking and focus
|
|
64
|
+
* hand-back all match the host. SYNC POINT: when the host's ModelSelect or
|
|
65
|
+
* ModelSelect.module.css changes, this copy must be re-transcribed; styles
|
|
66
|
+
* below may only reference allowlisted theme tokens (the --dsw-* set plus the
|
|
67
|
+
* host's --ds-* motion tokens), never literal colors.
|
|
68
|
+
*
|
|
69
|
+
* It statically imports the primitives, so only browser clients should load
|
|
70
|
+
* this entry; host code and node tests use the core entry (`./index.ts`).
|
|
71
|
+
*/
|
|
72
|
+
/** Unplaced portal card: hidden but laid out at a fixed origin so measurements are real. */
|
|
73
|
+
const MEASURE_STYLE = {
|
|
74
|
+
visibility: "hidden",
|
|
75
|
+
left: 0,
|
|
76
|
+
top: 0
|
|
77
|
+
};
|
|
78
|
+
function ModelMenu(props) {
|
|
79
|
+
const language = props.locale?.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
80
|
+
const copy = language === "zh" ? {
|
|
81
|
+
model: "模型",
|
|
82
|
+
effort: "思考强度",
|
|
83
|
+
default: "默认",
|
|
84
|
+
search: "搜索模型…",
|
|
85
|
+
empty: "没有匹配的模型",
|
|
86
|
+
clear: "清除搜索"
|
|
87
|
+
} : {
|
|
88
|
+
model: "Model",
|
|
89
|
+
effort: "Reasoning",
|
|
90
|
+
default: "Default",
|
|
91
|
+
search: "Search models…",
|
|
92
|
+
empty: "No matching models",
|
|
93
|
+
clear: "Clear search"
|
|
94
|
+
};
|
|
95
|
+
const { open, onOpenChange, anchor, choices, selected, onPick, leading = [], onPickLeading, efforts = [], selectedEffort, onPickEffort, modelLabel = copy.model, modelValue = "—", effortLabel = copy.effort, effortValue, defaultEffortLabel = copy.default, searchPlaceholder = copy.search, emptyLabel = copy.empty, clearSearchLabel = copy.clear, initialPane = "root", alwaysShowSearch = false, reasoningSlider = false, menuHeader, disabled = false, busy = false, closeOnPick = true, closeOnDisable = true, className, listClassName } = props;
|
|
96
|
+
const rootRef = (0, react.useRef)(null);
|
|
97
|
+
const openRef = (0, react.useRef)(open);
|
|
98
|
+
openRef.current = open;
|
|
99
|
+
const pendingOpenRef = (0, react.useRef)(false);
|
|
100
|
+
const pendingRestoreFocus = (0, react.useRef)(false);
|
|
101
|
+
const [requestedOpen, setRequestedOpen] = (0, react.useState)(false);
|
|
102
|
+
const menuRef = (0, react.useRef)(null);
|
|
103
|
+
const groupsRef = (0, react.useRef)(null);
|
|
104
|
+
const searchRef = (0, react.useRef)(null);
|
|
105
|
+
const itemRefs = (0, react.useRef)([]);
|
|
106
|
+
const paneFocus = (0, react.useRef)(null);
|
|
107
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
108
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
109
|
+
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
110
|
+
const [keyNav, setKeyNav] = (0, react.useState)(0);
|
|
111
|
+
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
112
|
+
const id = (0, react.useId)();
|
|
113
|
+
const showSearch = alwaysShowSearch || choices.length > 4;
|
|
114
|
+
const effortRows = (0, react.useMemo)(() => [{
|
|
115
|
+
id: "",
|
|
116
|
+
name: defaultEffortLabel
|
|
117
|
+
}, ...efforts.map((effort) => ({
|
|
118
|
+
...effort,
|
|
119
|
+
name: modelMenuEffortLabel(effort, language)
|
|
120
|
+
}))], [
|
|
121
|
+
defaultEffortLabel,
|
|
122
|
+
efforts,
|
|
123
|
+
language
|
|
124
|
+
]);
|
|
125
|
+
const filteredGroups = (0, react.useMemo)(() => groupModelMenuChoices(choices, "").map((group) => ({
|
|
126
|
+
...group,
|
|
127
|
+
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
128
|
+
choice,
|
|
129
|
+
name: modelMenuShortName(choice),
|
|
130
|
+
label: [
|
|
131
|
+
choice.provider,
|
|
132
|
+
choice.providerLabel,
|
|
133
|
+
choice.model,
|
|
134
|
+
choice.label
|
|
135
|
+
].filter(Boolean).join(" ")
|
|
136
|
+
})), showSearch ? query.trim() : "")
|
|
137
|
+
})).filter((group) => group.items.length > 0), [
|
|
138
|
+
choices,
|
|
139
|
+
query,
|
|
140
|
+
showSearch
|
|
141
|
+
]);
|
|
142
|
+
const visibleModels = (0, react.useMemo)(() => filteredGroups.flatMap((group) => group.items.map((item) => item.choice)), [filteredGroups]);
|
|
143
|
+
const currentVisibleIndex = visibleModels.findIndex((choice) => choice.provider === selected?.provider && choice.model === selected.model);
|
|
144
|
+
const activeModelIndex = Math.min(highlightedIndex ?? Math.max(0, currentVisibleIndex), visibleModels.length - 1);
|
|
145
|
+
const changeOpen = (next) => {
|
|
146
|
+
if (openRef.current === next) return;
|
|
147
|
+
openRef.current = next;
|
|
148
|
+
onOpenChange(next);
|
|
149
|
+
};
|
|
150
|
+
const requestOpen = () => {
|
|
151
|
+
if (open || pendingOpenRef.current) return;
|
|
152
|
+
pendingOpenRef.current = true;
|
|
153
|
+
pendingRestoreFocus.current = false;
|
|
154
|
+
setRequestedOpen(true);
|
|
155
|
+
};
|
|
156
|
+
const close = (restoreFocus = false) => {
|
|
157
|
+
const active = document.activeElement;
|
|
158
|
+
pendingRestoreFocus.current = restoreFocus && (active === document.body || rootRef.current?.contains(active) === true || menuRef.current?.contains(active) === true);
|
|
159
|
+
changeOpen(false);
|
|
160
|
+
};
|
|
161
|
+
const drill = (next) => {
|
|
162
|
+
setQuery("");
|
|
163
|
+
setHighlightedIndex(null);
|
|
164
|
+
paneFocus.current = "drill";
|
|
165
|
+
setPane(next);
|
|
166
|
+
};
|
|
167
|
+
/** Leave a drilled pane for the entry pane, handing the keyboard back to it.
|
|
168
|
+
* A root entry refocuses the cell that opened the pane; a search-first
|
|
169
|
+
* (model) entry refocuses that pane's own intent. */
|
|
170
|
+
const back = (from) => {
|
|
171
|
+
paneFocus.current = initialPane === "root" ? from : "drill";
|
|
172
|
+
setPane(initialPane);
|
|
173
|
+
};
|
|
174
|
+
const moveFocus = (offset) => {
|
|
175
|
+
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
176
|
+
if (!items.length) return;
|
|
177
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
178
|
+
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
179
|
+
};
|
|
180
|
+
(0, react.useLayoutEffect)(() => {
|
|
181
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
182
|
+
const active = document.activeElement;
|
|
183
|
+
if (active !== document.body && !rootRef.current?.contains(active)) {
|
|
184
|
+
pendingRestoreFocus.current = false;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const trigger = rootRef.current?.querySelector("button");
|
|
188
|
+
if (!trigger || trigger.disabled) return;
|
|
189
|
+
trigger.focus();
|
|
190
|
+
if (document.activeElement === trigger) pendingRestoreFocus.current = false;
|
|
191
|
+
});
|
|
192
|
+
(0, react.useEffect)(() => {
|
|
193
|
+
if (open || !pendingRestoreFocus.current) return;
|
|
194
|
+
const cancelOutside = (event) => {
|
|
195
|
+
if (event.target instanceof Node && !rootRef.current?.contains(event.target)) pendingRestoreFocus.current = false;
|
|
196
|
+
};
|
|
197
|
+
document.addEventListener("focusin", cancelOutside);
|
|
198
|
+
document.addEventListener("pointerdown", cancelOutside);
|
|
199
|
+
return () => {
|
|
200
|
+
document.removeEventListener("focusin", cancelOutside);
|
|
201
|
+
document.removeEventListener("pointerdown", cancelOutside);
|
|
202
|
+
};
|
|
203
|
+
});
|
|
204
|
+
(0, react.useEffect)(() => {
|
|
205
|
+
if (!requestedOpen) return;
|
|
206
|
+
pendingOpenRef.current = false;
|
|
207
|
+
setRequestedOpen(false);
|
|
208
|
+
if (!open) changeOpen(true);
|
|
209
|
+
}, [requestedOpen, open]);
|
|
210
|
+
(0, react.useEffect)(() => {
|
|
211
|
+
if (!open) return;
|
|
212
|
+
setPane(initialPane);
|
|
213
|
+
if (initialPane === "model") paneFocus.current = "drill";
|
|
214
|
+
setQuery("");
|
|
215
|
+
setHighlightedIndex(null);
|
|
216
|
+
}, [open, initialPane]);
|
|
217
|
+
(0, react.useEffect)(() => {
|
|
218
|
+
if (disabled && open && closeOnDisable) changeOpen(false);
|
|
219
|
+
}, [
|
|
220
|
+
disabled,
|
|
221
|
+
open,
|
|
222
|
+
closeOnDisable
|
|
223
|
+
]);
|
|
224
|
+
(0, react.useEffect)(() => {
|
|
225
|
+
if (!open) return;
|
|
226
|
+
const closeOutside = (event) => {
|
|
227
|
+
const target = event.target;
|
|
228
|
+
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
229
|
+
changeOpen(false);
|
|
230
|
+
};
|
|
231
|
+
const escapeOutside = (event) => {
|
|
232
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
233
|
+
if (!openRef.current) return;
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
if (pane !== initialPane) back(pane);
|
|
236
|
+
else close(true);
|
|
237
|
+
};
|
|
238
|
+
document.addEventListener("mousedown", closeOutside);
|
|
239
|
+
document.addEventListener("keydown", escapeOutside);
|
|
240
|
+
return () => {
|
|
241
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
242
|
+
document.removeEventListener("keydown", escapeOutside);
|
|
243
|
+
};
|
|
244
|
+
}, [
|
|
245
|
+
open,
|
|
246
|
+
onOpenChange,
|
|
247
|
+
pane,
|
|
248
|
+
initialPane
|
|
249
|
+
]);
|
|
250
|
+
(0, react.useEffect)(() => {
|
|
251
|
+
const intent = paneFocus.current;
|
|
252
|
+
if (!open || menuPos === null || intent === null) return;
|
|
253
|
+
if (intent !== "drill" && pane !== "root") return;
|
|
254
|
+
paneFocus.current = null;
|
|
255
|
+
if (intent === "drill") {
|
|
256
|
+
if (pane === "model" && showSearch) {
|
|
257
|
+
searchRef.current?.focus();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? itemRefs.current.find((item) => item !== null && !item.disabled))?.focus();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
itemRefs.current[intent === "effort" ? 1 : 0]?.focus();
|
|
264
|
+
}, [
|
|
265
|
+
open,
|
|
266
|
+
pane,
|
|
267
|
+
showSearch,
|
|
268
|
+
menuPos
|
|
269
|
+
]);
|
|
270
|
+
(0, react.useEffect)(() => {
|
|
271
|
+
const viewport = groupsRef.current;
|
|
272
|
+
if (viewport === null) return;
|
|
273
|
+
return (0, _deepseek_ai_dsh_client_ui_primitives.observeStickyMenuGroups)(viewport);
|
|
274
|
+
}, [
|
|
275
|
+
open,
|
|
276
|
+
pane,
|
|
277
|
+
filteredGroups
|
|
278
|
+
]);
|
|
279
|
+
const modelPaneOpen = open && pane === "model";
|
|
280
|
+
const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
|
|
281
|
+
const scrollSignalRef = (0, react.useRef)(null);
|
|
282
|
+
(0, react.useLayoutEffect)(() => {
|
|
283
|
+
if (!modelPaneOpen || activeModelIndex < 0) {
|
|
284
|
+
if (!modelPaneOpen) scrollSignalRef.current = null;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (scrollSignalRef.current === scrollSignal) return;
|
|
288
|
+
scrollSignalRef.current = scrollSignal;
|
|
289
|
+
itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
290
|
+
}, [
|
|
291
|
+
modelPaneOpen,
|
|
292
|
+
scrollSignal,
|
|
293
|
+
activeModelIndex,
|
|
294
|
+
leading.length
|
|
295
|
+
]);
|
|
296
|
+
(0, react.useLayoutEffect)(() => {
|
|
297
|
+
if (!open) {
|
|
298
|
+
setMenuPos(null);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const place = () => {
|
|
302
|
+
const rect = rootRef.current?.getBoundingClientRect();
|
|
303
|
+
if (!rect) return;
|
|
304
|
+
const margin = 12, lw = menuRef.current?.offsetWidth ?? 0, lh = menuRef.current?.offsetHeight ?? 0;
|
|
305
|
+
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
306
|
+
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
307
|
+
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
308
|
+
setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
|
|
309
|
+
left: x,
|
|
310
|
+
top: y
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
place();
|
|
314
|
+
window.addEventListener("scroll", place, true);
|
|
315
|
+
window.addEventListener("resize", place);
|
|
316
|
+
return () => {
|
|
317
|
+
window.removeEventListener("scroll", place, true);
|
|
318
|
+
window.removeEventListener("resize", place);
|
|
319
|
+
};
|
|
320
|
+
}, [
|
|
321
|
+
open,
|
|
322
|
+
pane,
|
|
323
|
+
choices,
|
|
324
|
+
query
|
|
325
|
+
]);
|
|
326
|
+
function onKeyDown(event) {
|
|
327
|
+
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
328
|
+
if (event.key === "Escape" && open) {
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
if (pane !== initialPane) back(pane);
|
|
331
|
+
else close(true);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (disabled) return;
|
|
335
|
+
if (!open && [
|
|
336
|
+
"Enter",
|
|
337
|
+
" ",
|
|
338
|
+
"ArrowDown",
|
|
339
|
+
"ArrowUp"
|
|
340
|
+
].includes(event.key)) {
|
|
341
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
342
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
paneFocus.current = "model";
|
|
345
|
+
requestOpen();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!open) return;
|
|
349
|
+
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
350
|
+
event.preventDefault();
|
|
351
|
+
if (visibleModels.length > 0) {
|
|
352
|
+
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
353
|
+
setKeyNav((n) => n + 1);
|
|
354
|
+
searchRef.current?.focus();
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (pane === "model" && showSearch && event.target instanceof HTMLInputElement && (event.key === "Enter" || event.key === "Tab" && !event.shiftKey)) {
|
|
359
|
+
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
360
|
+
event.preventDefault();
|
|
361
|
+
const highlighted = visibleModels[activeModelIndex];
|
|
362
|
+
if (highlighted && !busy) {
|
|
363
|
+
onPick({
|
|
364
|
+
provider: highlighted.provider,
|
|
365
|
+
model: highlighted.model
|
|
366
|
+
});
|
|
367
|
+
if (closeOnPick) close(true);
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (event.key === "Tab") {
|
|
372
|
+
if (event.shiftKey) {
|
|
373
|
+
event.preventDefault();
|
|
374
|
+
if (pane !== initialPane) back(pane);
|
|
375
|
+
else close(true);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const focused = document.activeElement;
|
|
379
|
+
const rows = itemRefs.current.filter((item) => item !== null);
|
|
380
|
+
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
focused.click();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (pane === "model" && showSearch) {
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
setHighlightedIndex(null);
|
|
388
|
+
searchRef.current?.focus();
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function onBlur(event) {
|
|
398
|
+
const related = event.relatedTarget;
|
|
399
|
+
if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
|
|
400
|
+
if (!closeOnPick && !(related instanceof Node)) return;
|
|
401
|
+
if (busy && related == null) return;
|
|
402
|
+
if (open) changeOpen(false);
|
|
403
|
+
}
|
|
404
|
+
itemRefs.current = [];
|
|
405
|
+
let itemIndex = 0;
|
|
406
|
+
let modelIndex = 0;
|
|
407
|
+
const itemRef = () => {
|
|
408
|
+
const at = itemIndex++;
|
|
409
|
+
return (node) => {
|
|
410
|
+
itemRefs.current[at] = node;
|
|
411
|
+
};
|
|
412
|
+
};
|
|
413
|
+
const menu = open && typeof document !== "undefined" ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
414
|
+
ref: menuRef,
|
|
415
|
+
id: `${id}-menu`,
|
|
416
|
+
className: `dsh-model-menu-menu${listClassName ? ` ${listClassName}` : ""}`,
|
|
417
|
+
style: menuPos ?? MEASURE_STYLE,
|
|
418
|
+
role: pane === "model" ? "group" : "menu",
|
|
419
|
+
"aria-label": modelLabel,
|
|
420
|
+
lang: language,
|
|
421
|
+
children: [
|
|
422
|
+
menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
423
|
+
className: "dsh-model-menu-header",
|
|
424
|
+
children: menuHeader
|
|
425
|
+
}) : null,
|
|
426
|
+
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
427
|
+
ref: itemRef(),
|
|
428
|
+
type: "button",
|
|
429
|
+
role: "menuitem",
|
|
430
|
+
className: "dsh-model-menu-cell",
|
|
431
|
+
disabled,
|
|
432
|
+
onClick: () => drill("model"),
|
|
433
|
+
children: [
|
|
434
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
435
|
+
className: "dsh-model-menu-cellLabel",
|
|
436
|
+
children: modelLabel
|
|
437
|
+
}),
|
|
438
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
439
|
+
className: "dsh-model-menu-cellValue",
|
|
440
|
+
children: modelValue
|
|
441
|
+
}),
|
|
442
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
443
|
+
]
|
|
444
|
+
}), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
445
|
+
ref: itemRef(),
|
|
446
|
+
type: "button",
|
|
447
|
+
role: "menuitem",
|
|
448
|
+
className: "dsh-model-menu-cell",
|
|
449
|
+
disabled,
|
|
450
|
+
onClick: () => drill("effort"),
|
|
451
|
+
children: [
|
|
452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
453
|
+
className: "dsh-model-menu-cellLabel",
|
|
454
|
+
children: effortLabel
|
|
455
|
+
}),
|
|
456
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
457
|
+
className: "dsh-model-menu-cellValue",
|
|
458
|
+
children: effortValue ?? effortRows.find((row) => row.id === (selectedEffort ?? ""))?.name ?? defaultEffortLabel
|
|
459
|
+
}),
|
|
460
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
461
|
+
]
|
|
462
|
+
}) : null] }) : null,
|
|
463
|
+
pane === "model" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
464
|
+
showSearch ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
465
|
+
className: "dsh-model-menu-searchRow",
|
|
466
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
467
|
+
ref: searchRef,
|
|
468
|
+
disabled,
|
|
469
|
+
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
470
|
+
type: "text",
|
|
471
|
+
role: "searchbox",
|
|
472
|
+
"aria-label": searchPlaceholder,
|
|
473
|
+
"aria-controls": `${id}-models`,
|
|
474
|
+
"aria-activedescendant": activeModelIndex < 0 ? void 0 : `${id}-model-${activeModelIndex}`,
|
|
475
|
+
placeholder: searchPlaceholder,
|
|
476
|
+
value: query,
|
|
477
|
+
onChange: (event) => {
|
|
478
|
+
setQuery(event.target.value);
|
|
479
|
+
setHighlightedIndex(0);
|
|
480
|
+
}
|
|
481
|
+
}), query !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
482
|
+
type: "button",
|
|
483
|
+
className: "dsh-model-menu-searchClear",
|
|
484
|
+
"aria-label": clearSearchLabel,
|
|
485
|
+
disabled,
|
|
486
|
+
onClick: () => {
|
|
487
|
+
setQuery("");
|
|
488
|
+
setHighlightedIndex(null);
|
|
489
|
+
searchRef.current?.focus();
|
|
490
|
+
},
|
|
491
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
|
|
492
|
+
}) : null]
|
|
493
|
+
}) : null,
|
|
494
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
495
|
+
ref: groupsRef,
|
|
496
|
+
id: `${id}-models`,
|
|
497
|
+
className: "dsh-model-menu-groups scrollable",
|
|
498
|
+
role: "menu",
|
|
499
|
+
"aria-label": modelLabel,
|
|
500
|
+
hidden: filteredGroups.length === 0,
|
|
501
|
+
children: [leading.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
502
|
+
className: "dsh-model-menu-leading",
|
|
503
|
+
children: leading.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
504
|
+
ref: itemRef(),
|
|
505
|
+
type: "button",
|
|
506
|
+
role: "menuitemradio",
|
|
507
|
+
"aria-checked": item.selected === true,
|
|
508
|
+
disabled: disabled || busy,
|
|
509
|
+
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
510
|
+
onClick: () => {
|
|
511
|
+
if (disabled || busy) return;
|
|
512
|
+
onPickLeading?.(item.id);
|
|
513
|
+
if (closeOnPick) close(true);
|
|
514
|
+
},
|
|
515
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
516
|
+
className: "dsh-model-menu-optionCopy",
|
|
517
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
518
|
+
className: "dsh-model-menu-modelName",
|
|
519
|
+
children: item.label
|
|
520
|
+
})
|
|
521
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
522
|
+
className: "dsh-model-menu-check",
|
|
523
|
+
children: item.selected === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
524
|
+
})]
|
|
525
|
+
}, item.id))
|
|
526
|
+
}) : null, filteredGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuGroup, {
|
|
527
|
+
label: group.name,
|
|
528
|
+
children: group.items.map((item) => {
|
|
529
|
+
const index = modelIndex++;
|
|
530
|
+
const choice = item.choice;
|
|
531
|
+
const checked = selected?.provider === choice.provider && selected.model === choice.model;
|
|
532
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
533
|
+
ref: itemRef(),
|
|
534
|
+
type: "button",
|
|
535
|
+
role: "menuitemradio",
|
|
536
|
+
"aria-checked": checked,
|
|
537
|
+
id: `${id}-model-${index}`,
|
|
538
|
+
tabIndex: showSearch ? -1 : 0,
|
|
539
|
+
disabled: disabled || busy,
|
|
540
|
+
onFocus: () => setHighlightedIndex(index),
|
|
541
|
+
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
542
|
+
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
543
|
+
onMouseMove: index === activeModelIndex ? void 0 : () => {
|
|
544
|
+
if (showSearch) setHighlightedIndex(index);
|
|
545
|
+
else itemRefs.current[leading.length + index]?.focus();
|
|
546
|
+
},
|
|
547
|
+
title: choice.label,
|
|
548
|
+
onClick: () => {
|
|
549
|
+
if (disabled || busy) return;
|
|
550
|
+
onPick({
|
|
551
|
+
provider: choice.provider,
|
|
552
|
+
model: choice.model
|
|
553
|
+
});
|
|
554
|
+
if (closeOnPick) close(true);
|
|
555
|
+
},
|
|
556
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
557
|
+
className: "dsh-model-menu-optionCopy",
|
|
558
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
559
|
+
className: "dsh-model-menu-modelName",
|
|
560
|
+
children: modelMenuShortName(choice)
|
|
561
|
+
})
|
|
562
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
563
|
+
className: "dsh-model-menu-check",
|
|
564
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
565
|
+
})]
|
|
566
|
+
}, `${choice.provider}/${choice.model}`);
|
|
567
|
+
})
|
|
568
|
+
}, group.provider))]
|
|
569
|
+
}),
|
|
570
|
+
reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
571
|
+
label: effortLabel,
|
|
572
|
+
rows: effortRows,
|
|
573
|
+
selected: selectedEffort ?? "",
|
|
574
|
+
disabled,
|
|
575
|
+
busy,
|
|
576
|
+
onPick: (value) => onPickEffort?.(value)
|
|
577
|
+
}) : null,
|
|
578
|
+
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
579
|
+
className: "dsh-model-menu-empty",
|
|
580
|
+
role: "status",
|
|
581
|
+
children: emptyLabel
|
|
582
|
+
}) : null
|
|
583
|
+
] }) : null,
|
|
584
|
+
pane === "effort" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: effortRows.map((row) => {
|
|
585
|
+
const checked = (selectedEffort ?? "") === row.id;
|
|
586
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
587
|
+
ref: itemRef(),
|
|
588
|
+
type: "button",
|
|
589
|
+
role: "menuitemradio",
|
|
590
|
+
"aria-checked": checked,
|
|
591
|
+
disabled: disabled || busy,
|
|
592
|
+
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
593
|
+
onClick: () => {
|
|
594
|
+
if (disabled || busy) return;
|
|
595
|
+
onPickEffort?.(row.id || void 0);
|
|
596
|
+
if (closeOnPick) close(true);
|
|
597
|
+
},
|
|
598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
599
|
+
className: "dsh-model-menu-optionCopy",
|
|
600
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
601
|
+
className: "dsh-model-menu-modelName",
|
|
602
|
+
children: row.name
|
|
603
|
+
})
|
|
604
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
605
|
+
className: "dsh-model-menu-check",
|
|
606
|
+
children: checked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
607
|
+
})]
|
|
608
|
+
}, row.id || "__default");
|
|
609
|
+
}) }) : null
|
|
610
|
+
]
|
|
611
|
+
}), document.body) : null;
|
|
612
|
+
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
613
|
+
"aria-haspopup": "menu",
|
|
614
|
+
"aria-expanded": open,
|
|
615
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
616
|
+
"aria-disabled": disabled || void 0
|
|
617
|
+
}) : anchor;
|
|
618
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
619
|
+
ref: rootRef,
|
|
620
|
+
className: `dsh-model-menu-root${className ? ` ${className}` : ""}`,
|
|
621
|
+
onKeyDown,
|
|
622
|
+
onBlur,
|
|
623
|
+
onClick: (event) => {
|
|
624
|
+
if (disabled || event.defaultPrevented) return;
|
|
625
|
+
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
626
|
+
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
627
|
+
requestOpen();
|
|
628
|
+
},
|
|
629
|
+
onMouseDown: (event) => {
|
|
630
|
+
if (event.target instanceof Element && menuRef.current?.contains(event.target) && event.target.closest("button") !== null) event.preventDefault();
|
|
631
|
+
},
|
|
632
|
+
children: [trigger, menu]
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Discrete reasoning-effort slider shown under the model list in enhanced mode.
|
|
637
|
+
*
|
|
638
|
+
* A native range input (`step=1`) over exactly the rows the caller supplies —
|
|
639
|
+
* the default row plus each effort the selected model advertises — so it can
|
|
640
|
+
* never invent a level. It previews locally while dragging or arrowing and
|
|
641
|
+
* commits only when the gesture ends (pointer up, key up, or blur), so a drag
|
|
642
|
+
* does not write on every step. The menu stays open, so adjusting the effort
|
|
643
|
+
* never loses the model selection or the search query.
|
|
644
|
+
*
|
|
645
|
+
* The input itself stays the entire interaction and accessibility surface —
|
|
646
|
+
* keyboard, ARIA, disabled and the native step snapping all come from it —
|
|
647
|
+
* while the track, gradient fill and thumb below are decorative layers painted
|
|
648
|
+
* from the `--_p` percentage the preview position sets on the rail. Value
|
|
649
|
+
* changes only update that custom property, so the CSS transitions animate
|
|
650
|
+
* between levels without remounting nodes or restarting an animation.
|
|
651
|
+
*/
|
|
652
|
+
function EffortSlider(props) {
|
|
653
|
+
const { label, rows, selected, disabled = false, busy = false, onPick } = props;
|
|
654
|
+
const active = Math.max(0, rows.findIndex((row) => row.id === selected));
|
|
655
|
+
const [position, setPosition] = (0, react.useState)(active);
|
|
656
|
+
const positionRef = (0, react.useRef)(position);
|
|
657
|
+
(0, react.useEffect)(() => {
|
|
658
|
+
positionRef.current = active;
|
|
659
|
+
setPosition(active);
|
|
660
|
+
}, [active]);
|
|
661
|
+
const preview = (next) => {
|
|
662
|
+
positionRef.current = next;
|
|
663
|
+
setPosition(next);
|
|
664
|
+
};
|
|
665
|
+
const commit = () => {
|
|
666
|
+
if (disabled || busy) return;
|
|
667
|
+
const row = rows[positionRef.current];
|
|
668
|
+
if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
|
|
669
|
+
};
|
|
670
|
+
const max = Math.max(0, rows.length - 1);
|
|
671
|
+
const percent = max > 0 ? Math.round(position / max * 100) : 0;
|
|
672
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
673
|
+
className: "dsh-model-menu-sliderGroup",
|
|
674
|
+
role: "group",
|
|
675
|
+
"aria-label": label,
|
|
676
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
677
|
+
className: "dsh-model-menu-sliderHead",
|
|
678
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
679
|
+
className: "dsh-model-menu-sliderTitle",
|
|
680
|
+
children: label
|
|
681
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
682
|
+
className: "dsh-model-menu-sliderValue",
|
|
683
|
+
children: rows[position]?.name
|
|
684
|
+
})]
|
|
685
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
686
|
+
className: "dsh-model-menu-sliderRail",
|
|
687
|
+
style: { "--_p": percent },
|
|
688
|
+
children: [
|
|
689
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
690
|
+
type: "range",
|
|
691
|
+
className: "dsh-model-menu-sliderInput",
|
|
692
|
+
min: 0,
|
|
693
|
+
max,
|
|
694
|
+
step: 1,
|
|
695
|
+
value: position,
|
|
696
|
+
disabled,
|
|
697
|
+
"aria-label": label,
|
|
698
|
+
"aria-disabled": disabled || busy || void 0,
|
|
699
|
+
"aria-valuetext": rows[position]?.name,
|
|
700
|
+
onChange: (event) => {
|
|
701
|
+
const value = Number(event.target.value);
|
|
702
|
+
if (Number.isFinite(value)) preview(value);
|
|
703
|
+
},
|
|
704
|
+
onPointerUp: commit,
|
|
705
|
+
onKeyUp: commit,
|
|
706
|
+
onBlur: commit
|
|
707
|
+
}),
|
|
708
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
709
|
+
className: "dsh-model-menu-sliderTrack",
|
|
710
|
+
"aria-hidden": "true",
|
|
711
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
712
|
+
className: "dsh-model-menu-sliderFill",
|
|
713
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
|
|
714
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
715
|
+
className: "dsh-model-menu-sliderNodes",
|
|
716
|
+
children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
717
|
+
"data-covered": index <= position ? "" : void 0,
|
|
718
|
+
className: "dsh-model-menu-sliderNode",
|
|
719
|
+
style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
|
|
720
|
+
}, row.id || "__default"))
|
|
721
|
+
})]
|
|
722
|
+
}),
|
|
723
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
724
|
+
className: "dsh-model-menu-sliderThumb",
|
|
725
|
+
"aria-hidden": "true"
|
|
726
|
+
})
|
|
727
|
+
]
|
|
728
|
+
})]
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
733
|
+
* published dsh-client-ui-model-selection bundle): card capped at
|
|
734
|
+
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
735
|
+
* 34px root cells at 13/20, a borderless 12px search row with a clear button,
|
|
736
|
+
* sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
|
|
737
|
+
* elevation at portal z-index 1100. The trigger mirrors the composer's 28px
|
|
738
|
+
* model pill, chevron rotating open. MenuSurface paints fill and backdrop.
|
|
739
|
+
* Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
|
|
740
|
+
* color is a bug.
|
|
741
|
+
*/
|
|
742
|
+
const modelMenuCss = `
|
|
743
|
+
.dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
|
|
744
|
+
.dsh-model-menu-trigger {
|
|
745
|
+
border-radius: var(--dsw-radius-sm);
|
|
746
|
+
min-width: 0;
|
|
747
|
+
max-width: min(360px, 45cqw);
|
|
748
|
+
height: 28px;
|
|
749
|
+
color: var(--dsw-alias-label-secondary);
|
|
750
|
+
cursor: pointer;
|
|
751
|
+
background: none;
|
|
752
|
+
border: none;
|
|
753
|
+
outline: none;
|
|
754
|
+
align-items: center;
|
|
755
|
+
gap: 4px;
|
|
756
|
+
padding: 0 4px 0 8px;
|
|
757
|
+
font-size: 13px;
|
|
758
|
+
font-weight: 400;
|
|
759
|
+
line-height: 20px;
|
|
760
|
+
display: flex;
|
|
761
|
+
}
|
|
762
|
+
.dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
763
|
+
.dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
|
|
764
|
+
.dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
765
|
+
.dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
|
|
766
|
+
.dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
|
|
767
|
+
.dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
|
|
768
|
+
.dsh-model-menu-chevronOpen { transform: rotate(180deg); }
|
|
769
|
+
/* Portal overlay: the host publishes no z-index token, so the card keeps the
|
|
770
|
+
* native menu layer's 1100 verbatim. */
|
|
771
|
+
.dsh-model-menu-menu {
|
|
772
|
+
z-index: 1100;
|
|
773
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
774
|
+
width: max-content;
|
|
775
|
+
min-width: min(240px, 100vw - 32px);
|
|
776
|
+
max-width: min(420px, 100vw - 32px);
|
|
777
|
+
max-height: min(360px, 100vh - 96px);
|
|
778
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
779
|
+
color: var(--dsw-alias-label-primary);
|
|
780
|
+
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
781
|
+
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
782
|
+
border: 0;
|
|
783
|
+
flex-direction: column;
|
|
784
|
+
padding: 4px;
|
|
785
|
+
display: flex;
|
|
786
|
+
position: fixed;
|
|
787
|
+
overflow: hidden;
|
|
788
|
+
}
|
|
789
|
+
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
790
|
+
.dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
|
|
791
|
+
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
792
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search { border-radius: var(--dsw-radius-md); background: none; border: 0 solid transparent; height: auto; padding: 5px 7px; display: flex; }
|
|
793
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
794
|
+
.dsh-model-menu-searchWithQuery { padding-right: 34px; }
|
|
795
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
|
|
796
|
+
.dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
|
|
797
|
+
/* The clear button is a circle by geometry (border-radius: 50%), a shape the
|
|
798
|
+
* radius token scale does not describe. */
|
|
799
|
+
.dsh-model-menu-searchClear {
|
|
800
|
+
width: 24px;
|
|
801
|
+
height: 24px;
|
|
802
|
+
color: var(--dsw-alias-label-secondary);
|
|
803
|
+
cursor: pointer;
|
|
804
|
+
background: none;
|
|
805
|
+
border: none;
|
|
806
|
+
border-radius: 50%;
|
|
807
|
+
justify-content: center;
|
|
808
|
+
align-items: center;
|
|
809
|
+
padding: 0;
|
|
810
|
+
display: inline-flex;
|
|
811
|
+
position: absolute;
|
|
812
|
+
top: 50%;
|
|
813
|
+
right: 4px;
|
|
814
|
+
transform: translateY(-50%);
|
|
815
|
+
}
|
|
816
|
+
.dsh-model-menu-searchClear:hover, .dsh-model-menu-searchClear:focus-visible { background: var(--dsw-alias-interactive-bg-hover); outline: none; }
|
|
817
|
+
.dsh-model-menu-groups { min-height: 0; overflow-y: auto; }
|
|
818
|
+
.dsh-model-menu-leading { border-bottom: 0.5px solid var(--dsw-alias-border-l2); margin-bottom: 3px; padding-bottom: 3px; }
|
|
819
|
+
.dsh-model-menu-option {
|
|
820
|
+
box-sizing: border-box;
|
|
821
|
+
border-radius: var(--dsw-radius-md);
|
|
822
|
+
width: auto;
|
|
823
|
+
min-width: 100%;
|
|
824
|
+
min-height: 34px;
|
|
825
|
+
color: inherit;
|
|
826
|
+
text-align: left;
|
|
827
|
+
cursor: pointer;
|
|
828
|
+
background: none;
|
|
829
|
+
border: none;
|
|
830
|
+
outline: none;
|
|
831
|
+
align-items: center;
|
|
832
|
+
gap: 6px;
|
|
833
|
+
padding: 5px 7px;
|
|
834
|
+
display: flex;
|
|
835
|
+
}
|
|
836
|
+
.dsh-model-menu-option:not(.dsh-model-menu-modelOption):hover:not(:disabled),
|
|
837
|
+
.dsh-model-menu-option:focus-visible,
|
|
838
|
+
.dsh-model-menu-optionActive:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
839
|
+
.dsh-model-menu-modelOption:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
840
|
+
.dsh-model-menu-selected { background: none; }
|
|
841
|
+
.dsh-model-menu-option:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
842
|
+
.dsh-model-menu-optionCopy { flex-direction: column; flex: 1; min-width: 0; display: flex; }
|
|
843
|
+
.dsh-model-menu-modelName { color: inherit; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; font-weight: 400; line-height: 18px; overflow: hidden; }
|
|
844
|
+
.dsh-model-menu-check { color: var(--dsw-alias-label-primary); flex: 0 0 14px; place-items: center; display: grid; }
|
|
845
|
+
.dsh-model-menu-check svg { width: 14px; height: 14px; }
|
|
846
|
+
.dsh-model-menu-cell {
|
|
847
|
+
box-sizing: border-box;
|
|
848
|
+
border-radius: var(--dsw-radius-md);
|
|
849
|
+
width: auto;
|
|
850
|
+
min-width: 100%;
|
|
851
|
+
height: 34px;
|
|
852
|
+
color: var(--dsw-alias-label-primary);
|
|
853
|
+
cursor: pointer;
|
|
854
|
+
text-align: left;
|
|
855
|
+
background: none;
|
|
856
|
+
border: none;
|
|
857
|
+
outline: none;
|
|
858
|
+
align-items: center;
|
|
859
|
+
gap: 6px;
|
|
860
|
+
padding: 0 8px;
|
|
861
|
+
font-size: 13px;
|
|
862
|
+
line-height: 20px;
|
|
863
|
+
display: flex;
|
|
864
|
+
}
|
|
865
|
+
.dsh-model-menu-cell:hover, .dsh-model-menu-cell:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
866
|
+
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
867
|
+
.dsh-model-menu-cellValue { text-overflow: ellipsis; white-space: nowrap; text-align: right; min-width: 0; color: var(--dsw-alias-label-tertiary); flex: auto; overflow: hidden; }
|
|
868
|
+
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
869
|
+
/* Discrete reasoning-effort slider under the model list (enhanced mode).
|
|
870
|
+
* The painted control is layered over a fully functional native range input:
|
|
871
|
+
* the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
|
|
872
|
+
* the decorative track, fill and thumb render beneath it at pointer-events:
|
|
873
|
+
* none. Position lives in one custom property (--_p, 0–100) on the rail, so
|
|
874
|
+
* the only animated work a level change triggers is a clip-path/left
|
|
875
|
+
* transition — no keyframes, no element churn. */
|
|
876
|
+
.dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
|
|
877
|
+
.dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
|
|
878
|
+
.dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
879
|
+
.dsh-model-menu-sliderValue { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; line-height: 20px; transition: color var(--ds-transition-duration) var(--ds-ease-in-out); }
|
|
880
|
+
.dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
|
|
881
|
+
.dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
|
|
882
|
+
.dsh-model-menu-sliderInput {
|
|
883
|
+
appearance: none;
|
|
884
|
+
-webkit-appearance: none;
|
|
885
|
+
box-sizing: border-box;
|
|
886
|
+
border: 0;
|
|
887
|
+
cursor: pointer;
|
|
888
|
+
direction: ltr;
|
|
889
|
+
opacity: 0;
|
|
890
|
+
margin: 0;
|
|
891
|
+
padding: 0;
|
|
892
|
+
width: 100%;
|
|
893
|
+
height: 100%;
|
|
894
|
+
position: absolute;
|
|
895
|
+
inset: 0;
|
|
896
|
+
z-index: 1;
|
|
897
|
+
touch-action: pan-y;
|
|
898
|
+
}
|
|
899
|
+
.dsh-model-menu-sliderInput:disabled { cursor: default; }
|
|
900
|
+
/* The transparent input must keep the painted thumb's geometry: an
|
|
901
|
+
* appearance:none webkit thumb defaults to 16px, which would shift the
|
|
902
|
+
* end-point positions 7px off the decorative 30px disc and misplace the
|
|
903
|
+
* native drag hit target. Track gets the full rail height so the whole band
|
|
904
|
+
* maps to values identically. */
|
|
905
|
+
.dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
|
|
906
|
+
.dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
|
|
907
|
+
.dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
|
|
908
|
+
.dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
|
|
909
|
+
.dsh-model-menu-sliderTrack {
|
|
910
|
+
pointer-events: none;
|
|
911
|
+
border-radius: 999px;
|
|
912
|
+
width: 100%;
|
|
913
|
+
height: 26px;
|
|
914
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
915
|
+
box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
|
|
916
|
+
position: absolute;
|
|
917
|
+
inset-inline: 0;
|
|
918
|
+
top: 50%;
|
|
919
|
+
transform: translateY(-50%);
|
|
920
|
+
/* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
|
|
921
|
+
* own clip-path already supplies the rounded reveal shape. */
|
|
922
|
+
}
|
|
923
|
+
/* The accent ramp paints the full track width once and is revealed by an
|
|
924
|
+
* animated clip-path, so raising the effort unwraps more of the spectrum
|
|
925
|
+
* instead of squashing the same gradient into a narrower bar. The glow lives
|
|
926
|
+
* on the un-clipped parent: a filter on the clipped element itself would be
|
|
927
|
+
* clipped away with it. */
|
|
928
|
+
.dsh-model-menu-sliderFill {
|
|
929
|
+
display: block;
|
|
930
|
+
height: 100%;
|
|
931
|
+
filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
|
|
932
|
+
transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
933
|
+
}
|
|
934
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
|
|
935
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
|
|
936
|
+
filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
|
|
937
|
+
}
|
|
938
|
+
/* The fill stops at the thumb's center, not at the track edge — at a 30px
|
|
939
|
+
* thumb the end stops sit 15px in from each side, matching the node dots. */
|
|
940
|
+
.dsh-model-menu-sliderFillBar {
|
|
941
|
+
display: block;
|
|
942
|
+
height: 100%;
|
|
943
|
+
background: linear-gradient(90deg,
|
|
944
|
+
var(--dsw-alias-state-business-primary),
|
|
945
|
+
var(--dsw-alias-state-success-primary),
|
|
946
|
+
var(--dsw-alias-state-warn-primary),
|
|
947
|
+
var(--dsw-alias-state-error-primary));
|
|
948
|
+
clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
|
|
949
|
+
transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
950
|
+
}
|
|
951
|
+
/* Stop dots: one per declared row (default + every advertised effort), evenly
|
|
952
|
+
* spread across the band between the two thumb-center extremes (inset 15px,
|
|
953
|
+
* i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
|
|
954
|
+
* reads as a pale pin through the gradient; an uncovered one stays a neutral
|
|
955
|
+
* notch on the empty track. */
|
|
956
|
+
.dsh-model-menu-sliderNodes {
|
|
957
|
+
pointer-events: none;
|
|
958
|
+
display: block;
|
|
959
|
+
position: absolute;
|
|
960
|
+
inset-inline: 15px;
|
|
961
|
+
inset-block: 0;
|
|
962
|
+
}
|
|
963
|
+
.dsh-model-menu-sliderNode {
|
|
964
|
+
pointer-events: none;
|
|
965
|
+
box-sizing: border-box;
|
|
966
|
+
width: 4px;
|
|
967
|
+
height: 4px;
|
|
968
|
+
border-radius: 50%;
|
|
969
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
|
|
970
|
+
position: absolute;
|
|
971
|
+
left: calc(var(--_n, 0) * 1%);
|
|
972
|
+
top: 50%;
|
|
973
|
+
transform: translate(-50%, -50%);
|
|
974
|
+
transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
975
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
976
|
+
}
|
|
977
|
+
.dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
|
|
978
|
+
.dsh-model-menu-sliderThumb {
|
|
979
|
+
pointer-events: none;
|
|
980
|
+
box-sizing: border-box;
|
|
981
|
+
border-radius: 50%;
|
|
982
|
+
width: 30px;
|
|
983
|
+
height: 30px;
|
|
984
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
985
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
986
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
|
|
987
|
+
position: absolute;
|
|
988
|
+
left: calc((100% - 30px) * var(--_p, 0) / 100);
|
|
989
|
+
top: 50%;
|
|
990
|
+
transform: translateY(-50%) scale(0.92);
|
|
991
|
+
transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
992
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
993
|
+
box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
994
|
+
}
|
|
995
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
|
|
996
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
|
|
997
|
+
transform: translateY(-50%) scale(1.1);
|
|
998
|
+
box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
|
|
999
|
+
}
|
|
1000
|
+
.dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
|
|
1001
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
|
|
1002
|
+
0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
1003
|
+
}
|
|
1004
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1005
|
+
.dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
|
|
1006
|
+
.dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
|
|
1007
|
+
}
|
|
1008
|
+
`;
|
|
1009
|
+
//#endregion
|
|
1010
|
+
//#region ../dsh-model-route/src/model-picker.ts
|
|
1011
|
+
/** Registered Factory name — the `renderFactorySlot`/`registerFactory` key. */
|
|
1012
|
+
const MODEL_PICKER_FACTORY = "klarkxy.model-picker";
|
|
1013
|
+
/** The one caller-selectable local position: the trigger rendered in place. */
|
|
1014
|
+
const MODEL_PICKER_ANCHOR_SLOT = "anchor";
|
|
1015
|
+
//#endregion
|
|
1016
|
+
//#region src/effort-preference.ts
|
|
1017
|
+
const keyOf = (route) => JSON.stringify([route.provider, route.model]);
|
|
1018
|
+
/**
|
|
1019
|
+
* In-memory per-route effort memory. One instance is shared by the composer
|
|
1020
|
+
* seat and any face that submits through `submit`, so a slider pick made in
|
|
1021
|
+
* one surface is what the next model pick re-attaches.
|
|
1022
|
+
*/
|
|
1023
|
+
var EffortPreference = class {
|
|
1024
|
+
/** route key → remembered explicit effort (a cleared route has no entry). */
|
|
1025
|
+
efforts = /* @__PURE__ */ new Map();
|
|
1026
|
+
/** The remembered explicit effort for one route, or undefined. */
|
|
1027
|
+
effortFor(route) {
|
|
1028
|
+
return this.efforts.get(keyOf(route));
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Record one confirmed pick. `undefined` clears the route's entry —
|
|
1032
|
+
* "Default" is an explicit choice too, and must not be overridden later.
|
|
1033
|
+
* Callers invoke this only after the Host accepted the selection.
|
|
1034
|
+
*/
|
|
1035
|
+
remember(route, effort) {
|
|
1036
|
+
const key = keyOf(route);
|
|
1037
|
+
if (effort === void 0) this.efforts.delete(key);
|
|
1038
|
+
else this.efforts.set(key, effort);
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Effort to attach to a fresh pick of `route`: the remembered value when
|
|
1042
|
+
* the catalog still declares it for that route, else undefined (the pick
|
|
1043
|
+
* then follows the adapter default). Validation is per-route — a model
|
|
1044
|
+
* without declared efforts can never inherit an effort remembered for it.
|
|
1045
|
+
*/
|
|
1046
|
+
resolve(route, reasoning) {
|
|
1047
|
+
const effort = this.efforts.get(keyOf(route));
|
|
1048
|
+
if (effort === void 0) return void 0;
|
|
1049
|
+
return reasoning?.efforts?.includes(effort) ? effort : void 0;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Submit helper for a MODEL pick: merge the remembered effort into
|
|
1053
|
+
* `selection` when no effort is already present and the target route still
|
|
1054
|
+
* supports it, call `select`, and remember the resolved effort only on
|
|
1055
|
+
* success. On failure memory is untouched and the result is returned
|
|
1056
|
+
* verbatim.
|
|
1057
|
+
*
|
|
1058
|
+
* This is NOT the right verb for the slider's "Default" row: a caller
|
|
1059
|
+
* clearing an effort must submit through `select` directly and call
|
|
1060
|
+
* `remember(route, undefined)` only after success — routing the clear
|
|
1061
|
+
* through `submit` would just re-attach the remembered effort.
|
|
1062
|
+
*/
|
|
1063
|
+
async submit(selection, reasoning, select) {
|
|
1064
|
+
const effort = selection.reasoningEffort ?? this.resolve(selection, reasoning);
|
|
1065
|
+
const intent = effort === void 0 ? {
|
|
1066
|
+
provider: selection.provider,
|
|
1067
|
+
model: selection.model
|
|
1068
|
+
} : {
|
|
1069
|
+
...selection,
|
|
1070
|
+
reasoningEffort: effort
|
|
1071
|
+
};
|
|
1072
|
+
const result = await select(intent);
|
|
1073
|
+
if (result?.ok !== true) return result;
|
|
1074
|
+
this.remember(intent, intent.reasoningEffort);
|
|
1075
|
+
return result;
|
|
1076
|
+
}
|
|
1077
|
+
};
|
|
1078
|
+
//#endregion
|
|
1079
|
+
//#region src/client.tsx
|
|
1080
|
+
/**
|
|
1081
|
+
* Model picker, browser half.
|
|
1082
|
+
*
|
|
1083
|
+
* Two surfaces ride one component:
|
|
1084
|
+
*
|
|
1085
|
+
* - `klarkxy.model-picker` (root Factory): the searchable model picker with a
|
|
1086
|
+
* reasoning-effort slider that feature plugins mount through
|
|
1087
|
+
* `renderFactorySlot`. Its props are pure data and callbacks so consumers
|
|
1088
|
+
* never take a runtime dependency on this package — while the definition is
|
|
1089
|
+
* absent the renderer resolves the caller's `options.fallback`.
|
|
1090
|
+
* - `conversation.input.model` (single/session, priority -2): the composer
|
|
1091
|
+
* model seat, shadowing the shipped entry (priority 0) and a reasoning-slider
|
|
1092
|
+
* entry (priority -1). The ledger keeps every occupant, so unmounting this
|
|
1093
|
+
* entry restores the previous winner — the native or reference seat returns
|
|
1094
|
+
* automatically on disable/uninstall. The label child slot stays declared by
|
|
1095
|
+
* the shadowed native entry, which means only it may render it; like
|
|
1096
|
+
* reasoning-slider this seat renders the model name verbatim.
|
|
1097
|
+
*
|
|
1098
|
+
* The seat drives the same per-session ModelDirectory the native seat and the
|
|
1099
|
+
* /model popup share (catalog load, selection submission, pending/error
|
|
1100
|
+
* projection), so selection semantics stay host-owned.
|
|
1101
|
+
*/
|
|
1102
|
+
const name = "dsh-model-picker-client";
|
|
1103
|
+
const inject = [
|
|
1104
|
+
"slots",
|
|
1105
|
+
"locale",
|
|
1106
|
+
"sessions"
|
|
1107
|
+
];
|
|
1108
|
+
const MODEL_SEAT_PRIORITY = -2;
|
|
1109
|
+
const NS = "model-picker";
|
|
1110
|
+
const dictZh = {
|
|
1111
|
+
"trigger.fallback": "请选择模型",
|
|
1112
|
+
"trigger.aria": "选择模型,当前 {model}",
|
|
1113
|
+
"trigger.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
|
|
1114
|
+
"menu.model": "模型",
|
|
1115
|
+
"menu.effort": "推理等级",
|
|
1116
|
+
"effort.providerDefault": "跟随模型默认",
|
|
1117
|
+
"search.placeholder": "搜索模型…",
|
|
1118
|
+
"search.clear": "清除搜索",
|
|
1119
|
+
"search.empty": "没有匹配的模型。",
|
|
1120
|
+
"empty.models": "暂无可用模型",
|
|
1121
|
+
"status.loading": "正在刷新模型列表…",
|
|
1122
|
+
"error.action": "模型操作失败:{message}",
|
|
1123
|
+
"action.retry": "重试",
|
|
1124
|
+
"warning.unroutable": "当前模型不在可用目录中"
|
|
1125
|
+
};
|
|
1126
|
+
const dictEn = {
|
|
1127
|
+
"trigger.fallback": "Select model",
|
|
1128
|
+
"trigger.aria": "Select model, current {model}",
|
|
1129
|
+
"trigger.ariaEffort": "Select model, current {model}, reasoning effort {effort}",
|
|
1130
|
+
"menu.model": "Model",
|
|
1131
|
+
"menu.effort": "Effort",
|
|
1132
|
+
"effort.providerDefault": "Default",
|
|
1133
|
+
"search.placeholder": "Search models…",
|
|
1134
|
+
"search.clear": "Clear search",
|
|
1135
|
+
"search.empty": "No matching models.",
|
|
1136
|
+
"empty.models": "No models available.",
|
|
1137
|
+
"status.loading": "Refreshing model list…",
|
|
1138
|
+
"error.action": "Model operation failed: {message}",
|
|
1139
|
+
"action.retry": "Retry",
|
|
1140
|
+
"warning.unroutable": "The selected model is not in the available catalog"
|
|
1141
|
+
};
|
|
1142
|
+
function template(text, params) {
|
|
1143
|
+
return params ? text.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? `{${k}}`)) : text;
|
|
1144
|
+
}
|
|
1145
|
+
/** Copy resolver for tests and for faces that never got a framework `t`. */
|
|
1146
|
+
function pickerCopy(locale) {
|
|
1147
|
+
const dict = locale?.toLowerCase().startsWith("zh") ? dictZh : dictEn;
|
|
1148
|
+
return (key, params) => template(dict[key], params);
|
|
1149
|
+
}
|
|
1150
|
+
const modelPickerCss = `${modelMenuCss}
|
|
1151
|
+
.dsh-model-picker-status {
|
|
1152
|
+
display: flex; flex-direction: column; gap: 4px; flex: none;
|
|
1153
|
+
}
|
|
1154
|
+
.dsh-model-picker-statusRow {
|
|
1155
|
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
1156
|
+
border-radius: var(--dsw-radius-md); padding: 5px 7px;
|
|
1157
|
+
color: var(--dsw-alias-state-error-primary);
|
|
1158
|
+
background: var(--dsw-alias-interactive-bg-hover-danger);
|
|
1159
|
+
font-size: 12px; line-height: 18px;
|
|
1160
|
+
}
|
|
1161
|
+
.dsh-model-picker-statusRow[data-tone="warn"] {
|
|
1162
|
+
color: var(--dsw-alias-label-secondary);
|
|
1163
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1164
|
+
}
|
|
1165
|
+
.dsh-model-picker-statusText { min-width: 0; flex: 1; overflow-wrap: anywhere; }
|
|
1166
|
+
.dsh-model-picker-retry {
|
|
1167
|
+
color: inherit; font: inherit; font-weight: 600; cursor: pointer;
|
|
1168
|
+
background: none; border: none; padding: 0 2px; flex: none;
|
|
1169
|
+
}
|
|
1170
|
+
.dsh-model-picker-retry:hover:not(:disabled) { text-decoration: underline; }
|
|
1171
|
+
.dsh-model-picker-retry:disabled { opacity: 0.6; cursor: default; }
|
|
1172
|
+
.dsh-model-picker-triggerIcon { display: var(--dsh-composer-model-icon-display, none); flex: none; }
|
|
1173
|
+
`;
|
|
1174
|
+
/** Directory provider groups → flat ModelMenu choices. */
|
|
1175
|
+
function directoryChoices(groups, bound) {
|
|
1176
|
+
const choices = [];
|
|
1177
|
+
for (const group of groups) {
|
|
1178
|
+
const providerName = group.name || group.id;
|
|
1179
|
+
for (const model of group.models) choices.push({
|
|
1180
|
+
provider: group.id,
|
|
1181
|
+
model: model.id,
|
|
1182
|
+
label: `${providerName} / ${model.name || model.id}`,
|
|
1183
|
+
providerLabel: providerName,
|
|
1184
|
+
efforts: (model.reasoning?.efforts ?? []).map((effort) => ({
|
|
1185
|
+
id: effort.id,
|
|
1186
|
+
name: effort.name || effort.id
|
|
1187
|
+
}))
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
if (bound?.provider && bound.model && !choices.some((item) => item.provider === bound.provider && item.model === bound.model)) choices.push({
|
|
1191
|
+
provider: bound.provider,
|
|
1192
|
+
model: bound.model,
|
|
1193
|
+
label: `${bound.provider} / ${bound.model}`,
|
|
1194
|
+
efforts: []
|
|
1195
|
+
});
|
|
1196
|
+
return choices;
|
|
1197
|
+
}
|
|
1198
|
+
function reasoningOf(groups, provider, model) {
|
|
1199
|
+
return groups.find((group) => group.id === provider)?.models.find((entry) => entry.id === model)?.reasoning;
|
|
1200
|
+
}
|
|
1201
|
+
/** Route key only when the saved model is absent from the catalog. Effort-only
|
|
1202
|
+
* snapshot updates must not change this — a new `current` object is not a new list. */
|
|
1203
|
+
function outsideCatalogKey(groups, current) {
|
|
1204
|
+
if (!current) return "";
|
|
1205
|
+
for (const group of groups) {
|
|
1206
|
+
if (group.id !== current.provider) continue;
|
|
1207
|
+
if (group.models.some((model) => model.id === current.model)) return "";
|
|
1208
|
+
}
|
|
1209
|
+
return `${current.provider}\0${current.model}`;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* DOM attributes the anchor forwards: framework-stamped ARIA, identity and
|
|
1213
|
+
* style fields, and event handlers. Everything else in the local props —
|
|
1214
|
+
* injected kit members and hook objects — never reaches the DOM.
|
|
1215
|
+
*/
|
|
1216
|
+
const TRIGGER_FORWARD = /^(aria-|data-|id$|className$|style$|title$|role$|tabIndex$|on[A-Z])/;
|
|
1217
|
+
/**
|
|
1218
|
+
* The trigger every occurrence gets unless the caller selects its own
|
|
1219
|
+
* `anchor` local component. It mirrors the composer's model pill: icon +
|
|
1220
|
+
* label + effort caption + chevron (spinner while busy).
|
|
1221
|
+
*/
|
|
1222
|
+
function DefaultTrigger(props) {
|
|
1223
|
+
const { open, disabled, busy, label, effortLabel, className, ...rest } = props;
|
|
1224
|
+
const forward = {};
|
|
1225
|
+
for (const [key, value] of Object.entries(rest)) if (TRIGGER_FORWARD.test(key)) forward[key] = value;
|
|
1226
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1227
|
+
type: "button",
|
|
1228
|
+
...forward,
|
|
1229
|
+
className: `dsh-model-menu-trigger${className ? ` ${className}` : ""}`,
|
|
1230
|
+
disabled: disabled || void 0,
|
|
1231
|
+
title: effortLabel ? `${label} · ${effortLabel}` : label,
|
|
1232
|
+
children: [
|
|
1233
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, {
|
|
1234
|
+
className: "dsh-model-picker-triggerIcon",
|
|
1235
|
+
size: 16
|
|
1236
|
+
}),
|
|
1237
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1238
|
+
className: "dsh-model-menu-triggerLabel",
|
|
1239
|
+
children: label
|
|
1240
|
+
}),
|
|
1241
|
+
effortLabel !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1242
|
+
className: "dsh-model-menu-triggerEffort",
|
|
1243
|
+
children: effortLabel
|
|
1244
|
+
}) : null,
|
|
1245
|
+
busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
1246
|
+
]
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* One picker occurrence. `props` is the consumer's ModelPickerProps plus the
|
|
1251
|
+
* framework seats (`t`, `useFactorySlot`, injected hooks). Open state is per
|
|
1252
|
+
* occurrence; a `disabled` rise closes the menu and every pick is gated so a
|
|
1253
|
+
* busy or disabled picker can never select.
|
|
1254
|
+
*/
|
|
1255
|
+
function ModelPicker(props) {
|
|
1256
|
+
const { t, useFactorySlot, useActiveLocale, ...input } = props;
|
|
1257
|
+
const activeLocale = useActiveLocale ? String(useActiveLocale((snapshot) => snapshot.active)) : "en";
|
|
1258
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1259
|
+
const disabled = input.disabled === true;
|
|
1260
|
+
const busy = input.busy === true;
|
|
1261
|
+
const gatedRef = (0, react.useRef)(disabled || busy);
|
|
1262
|
+
gatedRef.current = disabled || busy;
|
|
1263
|
+
const setMenuOpenRef = (0, react.useRef)((next) => {});
|
|
1264
|
+
setMenuOpenRef.current = (next) => {
|
|
1265
|
+
if (disabled && next) return;
|
|
1266
|
+
setOpen(next);
|
|
1267
|
+
input.onOpenChange?.(next);
|
|
1268
|
+
};
|
|
1269
|
+
const setMenuOpen = (next) => setMenuOpenRef.current(next);
|
|
1270
|
+
const language = input.locale ?? activeLocale;
|
|
1271
|
+
const selected = input.selected;
|
|
1272
|
+
const selectedChoice = (0, react.useMemo)(() => input.choices.find((choice) => choice.provider === selected?.provider && choice.model === selected.model), [
|
|
1273
|
+
input.choices,
|
|
1274
|
+
selected?.provider,
|
|
1275
|
+
selected?.model
|
|
1276
|
+
]);
|
|
1277
|
+
const triggerLabel = input.triggerLabel ?? selectedChoice?.label ?? (selected ? `${selected.provider} / ${selected.model}` : t("trigger.fallback"));
|
|
1278
|
+
const effortRows = input.efforts ?? [];
|
|
1279
|
+
const selectedEffort = input.selectedEffort ?? "";
|
|
1280
|
+
const triggerEffortLabel = input.triggerEffortLabel ?? (selectedEffort ? modelMenuEffortLabel(effortRows.find((row) => row.id === selectedEffort) ?? {
|
|
1281
|
+
id: selectedEffort,
|
|
1282
|
+
name: selectedEffort
|
|
1283
|
+
}, language) : effortRows.length > 0 ? input.defaultEffortLabel ?? t("effort.providerDefault") : void 0);
|
|
1284
|
+
const status = input.status;
|
|
1285
|
+
const loading = status?.busy === true && input.choices.length === 0;
|
|
1286
|
+
const menuHeader = status && (loading || status.error || status.warning || (status.failures?.length ?? 0) > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1287
|
+
className: "dsh-model-picker-status",
|
|
1288
|
+
role: status.error ? "alert" : "status",
|
|
1289
|
+
children: [
|
|
1290
|
+
loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1291
|
+
className: "dsh-model-picker-statusRow",
|
|
1292
|
+
"data-tone": "info",
|
|
1293
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1294
|
+
className: "dsh-model-picker-statusText",
|
|
1295
|
+
children: t("status.loading")
|
|
1296
|
+
})
|
|
1297
|
+
}) : null,
|
|
1298
|
+
status.warning ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1299
|
+
className: "dsh-model-picker-statusRow",
|
|
1300
|
+
"data-tone": "warn",
|
|
1301
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1302
|
+
className: "dsh-model-picker-statusText",
|
|
1303
|
+
children: status.warning
|
|
1304
|
+
})
|
|
1305
|
+
}) : null,
|
|
1306
|
+
status.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1307
|
+
className: "dsh-model-picker-statusRow",
|
|
1308
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1309
|
+
className: "dsh-model-picker-statusText",
|
|
1310
|
+
children: t("error.action", { message: status.error })
|
|
1311
|
+
}), status.retry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1312
|
+
type: "button",
|
|
1313
|
+
className: "dsh-model-picker-retry",
|
|
1314
|
+
disabled,
|
|
1315
|
+
onClick: () => status.retry?.(),
|
|
1316
|
+
children: input.retryLabel ?? t("action.retry")
|
|
1317
|
+
}) : null]
|
|
1318
|
+
}) : null,
|
|
1319
|
+
(status.failures ?? []).map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1320
|
+
className: "dsh-model-picker-statusRow",
|
|
1321
|
+
"data-tone": "warn",
|
|
1322
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1323
|
+
className: "dsh-model-picker-statusText",
|
|
1324
|
+
children: [
|
|
1325
|
+
failure.name || failure.id,
|
|
1326
|
+
": ",
|
|
1327
|
+
failure.message
|
|
1328
|
+
]
|
|
1329
|
+
}), status.retry ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1330
|
+
type: "button",
|
|
1331
|
+
className: "dsh-model-picker-retry",
|
|
1332
|
+
disabled,
|
|
1333
|
+
onClick: () => status.retry?.(),
|
|
1334
|
+
children: input.retryLabel ?? t("action.retry")
|
|
1335
|
+
}) : null]
|
|
1336
|
+
}, failure.id))
|
|
1337
|
+
]
|
|
1338
|
+
}) : void 0;
|
|
1339
|
+
const gate = (fn) => (...args) => {
|
|
1340
|
+
if (!gatedRef.current) fn?.(...args);
|
|
1341
|
+
};
|
|
1342
|
+
const Anchor = useFactorySlot(MODEL_PICKER_ANCHOR_SLOT, DefaultTrigger);
|
|
1343
|
+
const anchor = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Anchor, {
|
|
1344
|
+
open,
|
|
1345
|
+
disabled,
|
|
1346
|
+
busy,
|
|
1347
|
+
label: triggerLabel,
|
|
1348
|
+
effortLabel: triggerEffortLabel,
|
|
1349
|
+
className: input.triggerClassName,
|
|
1350
|
+
"aria-label": triggerEffortLabel ? t("trigger.ariaEffort", {
|
|
1351
|
+
model: triggerLabel,
|
|
1352
|
+
effort: triggerEffortLabel
|
|
1353
|
+
}) : t("trigger.aria", { model: triggerLabel })
|
|
1354
|
+
});
|
|
1355
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: modelPickerCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
1356
|
+
locale: language,
|
|
1357
|
+
open,
|
|
1358
|
+
onOpenChange: setMenuOpen,
|
|
1359
|
+
anchor,
|
|
1360
|
+
choices: input.choices,
|
|
1361
|
+
selected: input.selected,
|
|
1362
|
+
onPick: gate(input.onPick),
|
|
1363
|
+
leading: input.leading ?? [],
|
|
1364
|
+
onPickLeading: gate(input.onPickLeading),
|
|
1365
|
+
menuHeader,
|
|
1366
|
+
disabled,
|
|
1367
|
+
busy,
|
|
1368
|
+
efforts: effortRows,
|
|
1369
|
+
selectedEffort,
|
|
1370
|
+
onPickEffort: gate(input.onPickEffort),
|
|
1371
|
+
modelLabel: input.modelLabel ?? t("menu.model"),
|
|
1372
|
+
modelValue: input.modelValue ?? triggerLabel,
|
|
1373
|
+
effortLabel: input.effortLabel ?? t("menu.effort"),
|
|
1374
|
+
defaultEffortLabel: input.defaultEffortLabel ?? t("effort.providerDefault"),
|
|
1375
|
+
searchPlaceholder: input.searchPlaceholder ?? t("search.placeholder"),
|
|
1376
|
+
emptyLabel: input.emptyLabel ?? (input.choices.length === 0 ? t("empty.models") : t("search.empty")),
|
|
1377
|
+
clearSearchLabel: input.clearSearchLabel ?? t("search.clear"),
|
|
1378
|
+
initialPane: input.initialPane ?? "model",
|
|
1379
|
+
alwaysShowSearch: input.alwaysShowSearch ?? true,
|
|
1380
|
+
reasoningSlider: input.reasoningSlider ?? true,
|
|
1381
|
+
closeOnPick: false,
|
|
1382
|
+
closeOnDisable: false,
|
|
1383
|
+
listClassName: `dsh-model-picker${input.listClassName ? ` ${input.listClassName}` : ""}`,
|
|
1384
|
+
className: "dsh-model-picker"
|
|
1385
|
+
})] });
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* The composer model seat: the same injected face the native entry exposes,
|
|
1389
|
+
* rendered through the shared factory so the composer and every consumer
|
|
1390
|
+
* share one picker implementation. The bare ModelMenu below is the last-ditch
|
|
1391
|
+
* path for a missing factory — the seat never loses its control.
|
|
1392
|
+
*/
|
|
1393
|
+
function ComposerModelSeat(props) {
|
|
1394
|
+
const { t } = props;
|
|
1395
|
+
const directory = props.directory;
|
|
1396
|
+
const subscribeDirectory = (0, react.useCallback)((onStoreChange) => directory.subscribe(onStoreChange), [directory]);
|
|
1397
|
+
const snapshotDirectory = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
|
|
1398
|
+
const state = (0, react.useSyncExternalStore)(subscribeDirectory, snapshotDirectory, snapshotDirectory);
|
|
1399
|
+
const [toast, setToast] = (0, react.useState)(null);
|
|
1400
|
+
const toastSeq = (0, react.useRef)(0);
|
|
1401
|
+
const rootRef = (0, react.useRef)(null);
|
|
1402
|
+
const groups = state.groups;
|
|
1403
|
+
const current = state.current;
|
|
1404
|
+
const outsideKey = outsideCatalogKey(groups, current);
|
|
1405
|
+
const choices = (0, react.useMemo)(() => directoryChoices(groups, outsideKey === "" ? void 0 : {
|
|
1406
|
+
provider: outsideKey.slice(0, outsideKey.indexOf("\0")),
|
|
1407
|
+
model: outsideKey.slice(outsideKey.indexOf("\0") + 1)
|
|
1408
|
+
}), [groups, outsideKey]);
|
|
1409
|
+
const selectedChoice = choices.find((choice) => choice.provider === current?.provider && choice.model === current.model);
|
|
1410
|
+
const reasoning = current ? reasoningOf(groups, current.provider, current.model) : void 0;
|
|
1411
|
+
const efforts = (0, react.useMemo)(() => [...selectedChoice?.efforts ?? []], [selectedChoice]);
|
|
1412
|
+
const busy = state.status === "selecting" || state.pending !== null;
|
|
1413
|
+
(0, react.useEffect)(() => {
|
|
1414
|
+
props.load();
|
|
1415
|
+
}, [props.load]);
|
|
1416
|
+
const onMenuOpenChange = (next) => {
|
|
1417
|
+
if (next) props.load();
|
|
1418
|
+
};
|
|
1419
|
+
const report = (error) => {
|
|
1420
|
+
toastSeq.current += 1;
|
|
1421
|
+
setToast({
|
|
1422
|
+
seq: toastSeq.current,
|
|
1423
|
+
text: t("error.action", { message: `${error.code}: ${error.message}` })
|
|
1424
|
+
});
|
|
1425
|
+
};
|
|
1426
|
+
const submit = (selection) => {
|
|
1427
|
+
props.select(selection).then((result) => {
|
|
1428
|
+
if (result !== void 0 && !result.ok) report(result.error);
|
|
1429
|
+
}).catch((error) => report({
|
|
1430
|
+
code: "model-picker/error",
|
|
1431
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1432
|
+
}));
|
|
1433
|
+
};
|
|
1434
|
+
const unroutable = state.routable === false && current !== null;
|
|
1435
|
+
const pickModel = (route) => {
|
|
1436
|
+
if (current?.provider === route.provider && current.model === route.model) return;
|
|
1437
|
+
if (unroutable && route.provider === current.provider && route.model === current.model) return;
|
|
1438
|
+
submit({
|
|
1439
|
+
provider: route.provider,
|
|
1440
|
+
model: route.model
|
|
1441
|
+
});
|
|
1442
|
+
};
|
|
1443
|
+
const pickEffort = (effort) => {
|
|
1444
|
+
if (!current) return;
|
|
1445
|
+
if ((current.reasoningEffort ?? void 0) === effort) return;
|
|
1446
|
+
submit({
|
|
1447
|
+
provider: current.provider,
|
|
1448
|
+
model: current.model,
|
|
1449
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
1450
|
+
});
|
|
1451
|
+
};
|
|
1452
|
+
const modelName = selectedChoice ? selectedChoice.label.split(" / ").slice(1).join(" / ") || selectedChoice.model : current?.model ?? t("trigger.fallback");
|
|
1453
|
+
const effortLabel = reasoning === void 0 ? state.retainedEffort : current?.reasoningEffort !== void 0 ? modelMenuEffortLabel(reasoning.efforts.find((row) => row.id === current?.reasoningEffort) ?? {
|
|
1454
|
+
id: current?.reasoningEffort ?? "",
|
|
1455
|
+
name: current?.reasoningEffort ?? ""
|
|
1456
|
+
}, void 0) : reasoning.efforts.length > 0 || reasoning.defaultEffort !== void 0 ? t("effort.providerDefault") : void 0;
|
|
1457
|
+
if (!props.available) return null;
|
|
1458
|
+
const fallback = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenuFallback, {
|
|
1459
|
+
t,
|
|
1460
|
+
state,
|
|
1461
|
+
choices,
|
|
1462
|
+
efforts,
|
|
1463
|
+
savedEffort: current?.reasoningEffort,
|
|
1464
|
+
effortLabel,
|
|
1465
|
+
modelName,
|
|
1466
|
+
locked: props.locked,
|
|
1467
|
+
busy,
|
|
1468
|
+
onPick: pickModel,
|
|
1469
|
+
onPickEffort: pickEffort,
|
|
1470
|
+
onOpen: onMenuOpenChange,
|
|
1471
|
+
onRetry: () => props.load()
|
|
1472
|
+
});
|
|
1473
|
+
const pickerProps = {
|
|
1474
|
+
choices,
|
|
1475
|
+
selected: current ? {
|
|
1476
|
+
provider: current.provider,
|
|
1477
|
+
model: current.model
|
|
1478
|
+
} : void 0,
|
|
1479
|
+
onPick: pickModel,
|
|
1480
|
+
efforts,
|
|
1481
|
+
selectedEffort: current?.reasoningEffort ?? "",
|
|
1482
|
+
onPickEffort: pickEffort,
|
|
1483
|
+
disabled: props.locked,
|
|
1484
|
+
busy,
|
|
1485
|
+
triggerLabel: modelName,
|
|
1486
|
+
triggerEffortLabel: effortLabel,
|
|
1487
|
+
onOpenChange: onMenuOpenChange,
|
|
1488
|
+
status: {
|
|
1489
|
+
error: state.error,
|
|
1490
|
+
busy: state.status === "loading",
|
|
1491
|
+
warning: unroutable ? t("warning.unroutable") : void 0,
|
|
1492
|
+
failures: state.failures,
|
|
1493
|
+
retry: () => props.load()
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1497
|
+
ref: rootRef,
|
|
1498
|
+
className: "dsh-model-picker",
|
|
1499
|
+
children: [props.renderFactorySlot ? props.renderFactorySlot(MODEL_PICKER_FACTORY, pickerProps, { fallback }) : fallback, toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
1500
|
+
text: toast.text,
|
|
1501
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
|
|
1502
|
+
anchor: rootRef.current?.closest("[data-composer-card]") ?? rootRef.current,
|
|
1503
|
+
onDone: () => setToast(null)
|
|
1504
|
+
}, toast.seq)]
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
/** Bare-menu fallback used only if the factory never registered. */
|
|
1508
|
+
function ModelMenuFallback(props) {
|
|
1509
|
+
const { t, state } = props;
|
|
1510
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1511
|
+
(0, react.useEffect)(() => {
|
|
1512
|
+
if (props.locked) setOpen(false);
|
|
1513
|
+
}, [props.locked]);
|
|
1514
|
+
const gated = props.locked || props.busy;
|
|
1515
|
+
const unroutable = state.routable === false && state.current !== null;
|
|
1516
|
+
const loading = state.status === "loading" && props.choices.length === 0;
|
|
1517
|
+
const menuHeader = loading || state.error || unroutable || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1518
|
+
className: "dsh-model-picker-status",
|
|
1519
|
+
role: state.error ? "alert" : "status",
|
|
1520
|
+
children: [
|
|
1521
|
+
loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1522
|
+
className: "dsh-model-picker-statusRow",
|
|
1523
|
+
"data-tone": "info",
|
|
1524
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1525
|
+
className: "dsh-model-picker-statusText",
|
|
1526
|
+
children: t("status.loading")
|
|
1527
|
+
})
|
|
1528
|
+
}) : null,
|
|
1529
|
+
unroutable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1530
|
+
className: "dsh-model-picker-statusRow",
|
|
1531
|
+
"data-tone": "warn",
|
|
1532
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1533
|
+
className: "dsh-model-picker-statusText",
|
|
1534
|
+
children: t("warning.unroutable")
|
|
1535
|
+
})
|
|
1536
|
+
}) : null,
|
|
1537
|
+
state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1538
|
+
className: "dsh-model-picker-statusRow",
|
|
1539
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1540
|
+
className: "dsh-model-picker-statusText",
|
|
1541
|
+
children: t("error.action", { message: state.error })
|
|
1542
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1543
|
+
type: "button",
|
|
1544
|
+
className: "dsh-model-picker-retry",
|
|
1545
|
+
disabled: gated,
|
|
1546
|
+
onClick: props.onRetry,
|
|
1547
|
+
children: t("action.retry")
|
|
1548
|
+
})]
|
|
1549
|
+
}) : null,
|
|
1550
|
+
state.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1551
|
+
className: "dsh-model-picker-statusRow",
|
|
1552
|
+
"data-tone": "warn",
|
|
1553
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1554
|
+
className: "dsh-model-picker-statusText",
|
|
1555
|
+
children: [
|
|
1556
|
+
failure.name || failure.id,
|
|
1557
|
+
": ",
|
|
1558
|
+
failure.message
|
|
1559
|
+
]
|
|
1560
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1561
|
+
type: "button",
|
|
1562
|
+
className: "dsh-model-picker-retry",
|
|
1563
|
+
disabled: gated,
|
|
1564
|
+
onClick: props.onRetry,
|
|
1565
|
+
children: t("action.retry")
|
|
1566
|
+
})]
|
|
1567
|
+
}, failure.id))
|
|
1568
|
+
]
|
|
1569
|
+
}) : void 0;
|
|
1570
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: modelPickerCss }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
1571
|
+
locale: "en",
|
|
1572
|
+
open: open && !props.locked,
|
|
1573
|
+
onOpenChange: (next) => {
|
|
1574
|
+
if (props.locked) return;
|
|
1575
|
+
setOpen(next);
|
|
1576
|
+
if (next) props.onOpen(next);
|
|
1577
|
+
},
|
|
1578
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DefaultTrigger, {
|
|
1579
|
+
open,
|
|
1580
|
+
disabled: gated,
|
|
1581
|
+
busy: props.busy,
|
|
1582
|
+
label: props.modelName,
|
|
1583
|
+
effortLabel: props.effortLabel,
|
|
1584
|
+
"aria-label": props.effortLabel ? t("trigger.ariaEffort", {
|
|
1585
|
+
model: props.modelName,
|
|
1586
|
+
effort: props.effortLabel
|
|
1587
|
+
}) : t("trigger.aria", { model: props.modelName })
|
|
1588
|
+
}),
|
|
1589
|
+
choices: props.choices,
|
|
1590
|
+
selected: state.current ? {
|
|
1591
|
+
provider: state.current.provider,
|
|
1592
|
+
model: state.current.model
|
|
1593
|
+
} : void 0,
|
|
1594
|
+
onPick: (route) => {
|
|
1595
|
+
if (!gated) props.onPick(route);
|
|
1596
|
+
},
|
|
1597
|
+
menuHeader,
|
|
1598
|
+
disabled: props.locked,
|
|
1599
|
+
busy: props.busy,
|
|
1600
|
+
efforts: props.efforts,
|
|
1601
|
+
selectedEffort: props.savedEffort ?? "",
|
|
1602
|
+
onPickEffort: (effort) => {
|
|
1603
|
+
if (!gated) props.onPickEffort(effort);
|
|
1604
|
+
},
|
|
1605
|
+
modelLabel: t("menu.model"),
|
|
1606
|
+
modelValue: props.modelName,
|
|
1607
|
+
effortLabel: t("menu.effort"),
|
|
1608
|
+
defaultEffortLabel: t("effort.providerDefault"),
|
|
1609
|
+
searchPlaceholder: t("search.placeholder"),
|
|
1610
|
+
emptyLabel: t("empty.models"),
|
|
1611
|
+
clearSearchLabel: t("search.clear"),
|
|
1612
|
+
initialPane: "model",
|
|
1613
|
+
alwaysShowSearch: true,
|
|
1614
|
+
reasoningSlider: true,
|
|
1615
|
+
closeOnPick: false,
|
|
1616
|
+
listClassName: "dsh-model-picker",
|
|
1617
|
+
className: "dsh-model-picker"
|
|
1618
|
+
})] });
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Wrap one per-session `ModelDirectory.select` with the page-lifetime effort
|
|
1622
|
+
* memory: a cross-route pick with no explicit effort re-attaches the route's
|
|
1623
|
+
* last successful human pick (only while the catalog still declares it), and
|
|
1624
|
+
* every confirmed selection records its effort so the Host default save —
|
|
1625
|
+
* `sessionController.selectModel` persists the complete selection through
|
|
1626
|
+
* `agentDefaultModel.saveSelection` — keeps the effort for new Sessions that
|
|
1627
|
+
* follow the default. Memory updates strictly on `result.ok === true`; a
|
|
1628
|
+
* failure leaves the remembered route untouched. Exported for the wiring spec.
|
|
1629
|
+
*/
|
|
1630
|
+
function createPreferredSelect(directory, preference) {
|
|
1631
|
+
return async (selection) => {
|
|
1632
|
+
const state = directory.store.getSnapshot();
|
|
1633
|
+
const current = state.current;
|
|
1634
|
+
const sameRoute = current !== null && current.provider === selection.provider && current.model === selection.model;
|
|
1635
|
+
let final = selection;
|
|
1636
|
+
if (!sameRoute && selection.reasoningEffort === void 0) {
|
|
1637
|
+
const reasoning = state.groups.find((group) => group.id === selection.provider)?.models.find((model) => model.id === selection.model)?.reasoning;
|
|
1638
|
+
const effort = preference.resolve(selection, reasoning === void 0 ? void 0 : { efforts: reasoning.efforts.map((row) => row.id) });
|
|
1639
|
+
if (effort !== void 0) final = {
|
|
1640
|
+
...selection,
|
|
1641
|
+
reasoningEffort: effort
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
const result = await directory.select(final);
|
|
1645
|
+
if (result?.ok === true) preference.remember(final, final.reasoningEffort);
|
|
1646
|
+
return result;
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function apply(ctx) {
|
|
1650
|
+
const preference = new EffortPreference();
|
|
1651
|
+
ctx.effect(() => {
|
|
1652
|
+
const zh = ctx.locale?.register(NS, "zh", dictZh);
|
|
1653
|
+
const en = ctx.locale?.register(NS, "en", dictEn);
|
|
1654
|
+
return () => {
|
|
1655
|
+
zh?.();
|
|
1656
|
+
en?.();
|
|
1657
|
+
};
|
|
1658
|
+
});
|
|
1659
|
+
ctx.slots.registerFactory({
|
|
1660
|
+
name: MODEL_PICKER_FACTORY,
|
|
1661
|
+
scope: "root",
|
|
1662
|
+
locale: NS,
|
|
1663
|
+
inject: () => ({ hooks: { activeLocale: ctx.locale } }),
|
|
1664
|
+
slots: { [MODEL_PICKER_ANCHOR_SLOT]: { scope: "root" } }
|
|
1665
|
+
}, ModelPicker);
|
|
1666
|
+
ctx.inject(["modelDirectories"], (scope) => {
|
|
1667
|
+
const directories = scope.modelDirectories;
|
|
1668
|
+
scope.slots.inject("conversation.input.model", () => scope.slots.register({
|
|
1669
|
+
name: "conversation.input.model",
|
|
1670
|
+
priority: MODEL_SEAT_PRIORITY,
|
|
1671
|
+
locale: NS,
|
|
1672
|
+
inject: (sessionId) => {
|
|
1673
|
+
const unavailable = {
|
|
1674
|
+
available: false,
|
|
1675
|
+
directory: {
|
|
1676
|
+
getSnapshot: () => EMPTY_STATE,
|
|
1677
|
+
subscribe: () => () => {},
|
|
1678
|
+
update: () => {},
|
|
1679
|
+
set: () => {}
|
|
1680
|
+
},
|
|
1681
|
+
load: () => {},
|
|
1682
|
+
resolveText: (text) => ctx.locale?.resolveText(text) ?? String(text),
|
|
1683
|
+
select: async () => void 0
|
|
1684
|
+
};
|
|
1685
|
+
if (scope.sessions?.subagentAddress?.(sessionId) !== void 0) return unavailable;
|
|
1686
|
+
try {
|
|
1687
|
+
const directory = directories.directoryFor(sessionId);
|
|
1688
|
+
return {
|
|
1689
|
+
available: true,
|
|
1690
|
+
directory: directory.store,
|
|
1691
|
+
load: () => {
|
|
1692
|
+
directory.load().catch(() => {});
|
|
1693
|
+
},
|
|
1694
|
+
resolveText: (text) => ctx.locale?.resolveText(text) ?? String(text),
|
|
1695
|
+
select: createPreferredSelect(directory, preference)
|
|
1696
|
+
};
|
|
1697
|
+
} catch {
|
|
1698
|
+
return unavailable;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}, ComposerModelSeat));
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
const EMPTY_STATE = {
|
|
1705
|
+
current: null,
|
|
1706
|
+
routable: null,
|
|
1707
|
+
groups: [],
|
|
1708
|
+
failures: [],
|
|
1709
|
+
status: "idle",
|
|
1710
|
+
pending: null,
|
|
1711
|
+
error: null
|
|
1712
|
+
};
|
|
1713
|
+
//#endregion
|
|
1714
|
+
exports.ComposerModelSeat = ComposerModelSeat;
|
|
1715
|
+
exports.ModelPicker = ModelPicker;
|
|
1716
|
+
exports.apply = apply;
|
|
1717
|
+
exports.createPreferredSelect = createPreferredSelect;
|
|
1718
|
+
exports.dictEn = dictEn;
|
|
1719
|
+
exports.dictZh = dictZh;
|
|
1720
|
+
exports.directoryChoices = directoryChoices;
|
|
1721
|
+
exports.inject = inject;
|
|
1722
|
+
exports.modelPickerCss = modelPickerCss;
|
|
1723
|
+
exports.name = name;
|
|
1724
|
+
exports.pickerCopy = pickerCopy;
|
|
1725
|
+
|
|
1726
|
+
//# sourceMappingURL=client.inner.cjs.map
|
|
1727
|
+
return module.exports;
|
|
1728
|
+
}
|
|
1729
|
+
});
|