@klarkxy/dsh-model-hub 0.2.1 → 0.2.3
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 +1 -1
- package/docs/README.zh-CN.md +1 -1
- package/lib/client.inner.cjs +515 -71
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +515 -71
- package/package.json +12 -11
package/lib/client.js
CHANGED
|
@@ -98,7 +98,9 @@ Object.values({
|
|
|
98
98
|
"--dsh-scrollbar-thumb-hover",
|
|
99
99
|
"--dsh-content-font-size-secondary",
|
|
100
100
|
"--dsh-content-font-delta",
|
|
101
|
-
"--dsh-frame-top-clearance"
|
|
101
|
+
"--dsh-frame-top-clearance",
|
|
102
|
+
"--dsh-composer-model-icon-display",
|
|
103
|
+
"--dsh-composer-model-text-display"
|
|
102
104
|
]
|
|
103
105
|
}).flat();
|
|
104
106
|
/**
|
|
@@ -546,6 +548,7 @@ function parseModelMenuChoices(value, bound) {
|
|
|
546
548
|
provider,
|
|
547
549
|
model,
|
|
548
550
|
label: `${providerName} / ${name}`,
|
|
551
|
+
providerLabel: providerName,
|
|
549
552
|
efforts
|
|
550
553
|
});
|
|
551
554
|
}
|
|
@@ -592,11 +595,11 @@ function modelMenuShortName(choice) {
|
|
|
592
595
|
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
593
596
|
function groupModelMenuChoices(choices, query) {
|
|
594
597
|
const needle = query.trim().toLowerCase();
|
|
595
|
-
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
598
|
+
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;
|
|
596
599
|
const groups = [];
|
|
597
600
|
for (const choice of matched) {
|
|
598
601
|
const sep = choice.label.indexOf(" / ");
|
|
599
|
-
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
602
|
+
const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
|
|
600
603
|
const last = groups.at(-1);
|
|
601
604
|
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
602
605
|
else groups.push({
|
|
@@ -982,7 +985,7 @@ function ModelMenu(props) {
|
|
|
982
985
|
empty: "No matching models",
|
|
983
986
|
clear: "Clear search"
|
|
984
987
|
};
|
|
985
|
-
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, className, listClassName } = props;
|
|
988
|
+
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;
|
|
986
989
|
const rootRef = (0, react.useRef)(null);
|
|
987
990
|
const openRef = (0, react.useRef)(open);
|
|
988
991
|
openRef.current = open;
|
|
@@ -997,9 +1000,10 @@ function ModelMenu(props) {
|
|
|
997
1000
|
const [pane, setPane] = (0, react.useState)("root");
|
|
998
1001
|
const [query, setQuery] = (0, react.useState)("");
|
|
999
1002
|
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
1003
|
+
const [keyNav, setKeyNav] = (0, react.useState)(0);
|
|
1000
1004
|
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
1001
1005
|
const id = (0, react.useId)();
|
|
1002
|
-
const showSearch = choices.length > 4;
|
|
1006
|
+
const showSearch = alwaysShowSearch || choices.length > 4;
|
|
1003
1007
|
const effortRows = (0, react.useMemo)(() => [{
|
|
1004
1008
|
id: "",
|
|
1005
1009
|
name: defaultEffortLabel
|
|
@@ -1016,7 +1020,12 @@ function ModelMenu(props) {
|
|
|
1016
1020
|
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
1017
1021
|
choice,
|
|
1018
1022
|
name: modelMenuShortName(choice),
|
|
1019
|
-
label:
|
|
1023
|
+
label: [
|
|
1024
|
+
choice.provider,
|
|
1025
|
+
choice.providerLabel,
|
|
1026
|
+
choice.model,
|
|
1027
|
+
choice.label
|
|
1028
|
+
].filter(Boolean).join(" ")
|
|
1020
1029
|
})), showSearch ? query.trim() : "")
|
|
1021
1030
|
})).filter((group) => group.items.length > 0), [
|
|
1022
1031
|
choices,
|
|
@@ -1048,10 +1057,12 @@ function ModelMenu(props) {
|
|
|
1048
1057
|
paneFocus.current = "drill";
|
|
1049
1058
|
setPane(next);
|
|
1050
1059
|
};
|
|
1051
|
-
/** Leave a drilled pane for the
|
|
1060
|
+
/** Leave a drilled pane for the entry pane, handing the keyboard back to it.
|
|
1061
|
+
* A root entry refocuses the cell that opened the pane; a search-first
|
|
1062
|
+
* (model) entry refocuses that pane's own intent. */
|
|
1052
1063
|
const back = (from) => {
|
|
1053
|
-
paneFocus.current = from;
|
|
1054
|
-
setPane(
|
|
1064
|
+
paneFocus.current = initialPane === "root" ? from : "drill";
|
|
1065
|
+
setPane(initialPane);
|
|
1055
1066
|
};
|
|
1056
1067
|
const moveFocus = (offset) => {
|
|
1057
1068
|
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
@@ -1091,10 +1102,18 @@ function ModelMenu(props) {
|
|
|
1091
1102
|
}, [requestedOpen, open]);
|
|
1092
1103
|
(0, react.useEffect)(() => {
|
|
1093
1104
|
if (!open) return;
|
|
1094
|
-
setPane(
|
|
1105
|
+
setPane(initialPane);
|
|
1106
|
+
if (initialPane === "model") paneFocus.current = "drill";
|
|
1095
1107
|
setQuery("");
|
|
1096
1108
|
setHighlightedIndex(null);
|
|
1097
|
-
}, [open]);
|
|
1109
|
+
}, [open, initialPane]);
|
|
1110
|
+
(0, react.useEffect)(() => {
|
|
1111
|
+
if (disabled && open && closeOnDisable) changeOpen(false);
|
|
1112
|
+
}, [
|
|
1113
|
+
disabled,
|
|
1114
|
+
open,
|
|
1115
|
+
closeOnDisable
|
|
1116
|
+
]);
|
|
1098
1117
|
(0, react.useEffect)(() => {
|
|
1099
1118
|
if (!open) return;
|
|
1100
1119
|
const closeOutside = (event) => {
|
|
@@ -1102,9 +1121,25 @@ function ModelMenu(props) {
|
|
|
1102
1121
|
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
1103
1122
|
changeOpen(false);
|
|
1104
1123
|
};
|
|
1124
|
+
const escapeOutside = (event) => {
|
|
1125
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
1126
|
+
if (!openRef.current) return;
|
|
1127
|
+
event.preventDefault();
|
|
1128
|
+
if (pane !== initialPane) back(pane);
|
|
1129
|
+
else close(true);
|
|
1130
|
+
};
|
|
1105
1131
|
document.addEventListener("mousedown", closeOutside);
|
|
1106
|
-
|
|
1107
|
-
|
|
1132
|
+
document.addEventListener("keydown", escapeOutside);
|
|
1133
|
+
return () => {
|
|
1134
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
1135
|
+
document.removeEventListener("keydown", escapeOutside);
|
|
1136
|
+
};
|
|
1137
|
+
}, [
|
|
1138
|
+
open,
|
|
1139
|
+
onOpenChange,
|
|
1140
|
+
pane,
|
|
1141
|
+
initialPane
|
|
1142
|
+
]);
|
|
1108
1143
|
(0, react.useEffect)(() => {
|
|
1109
1144
|
const intent = paneFocus.current;
|
|
1110
1145
|
if (!open || menuPos === null || intent === null) return;
|
|
@@ -1134,13 +1169,21 @@ function ModelMenu(props) {
|
|
|
1134
1169
|
pane,
|
|
1135
1170
|
filteredGroups
|
|
1136
1171
|
]);
|
|
1172
|
+
const modelPaneOpen = open && pane === "model";
|
|
1173
|
+
const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
|
|
1174
|
+
const scrollSignalRef = (0, react.useRef)(null);
|
|
1137
1175
|
(0, react.useLayoutEffect)(() => {
|
|
1138
|
-
if (
|
|
1176
|
+
if (!modelPaneOpen || activeModelIndex < 0) {
|
|
1177
|
+
if (!modelPaneOpen) scrollSignalRef.current = null;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (scrollSignalRef.current === scrollSignal) return;
|
|
1181
|
+
scrollSignalRef.current = scrollSignal;
|
|
1182
|
+
itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
1139
1183
|
}, [
|
|
1140
|
-
|
|
1141
|
-
|
|
1184
|
+
modelPaneOpen,
|
|
1185
|
+
scrollSignal,
|
|
1142
1186
|
activeModelIndex,
|
|
1143
|
-
visibleModels,
|
|
1144
1187
|
leading.length
|
|
1145
1188
|
]);
|
|
1146
1189
|
(0, react.useLayoutEffect)(() => {
|
|
@@ -1155,7 +1198,7 @@ function ModelMenu(props) {
|
|
|
1155
1198
|
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
1156
1199
|
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
1157
1200
|
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
1158
|
-
setMenuPos({
|
|
1201
|
+
setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
|
|
1159
1202
|
left: x,
|
|
1160
1203
|
top: y
|
|
1161
1204
|
});
|
|
@@ -1175,6 +1218,13 @@ function ModelMenu(props) {
|
|
|
1175
1218
|
]);
|
|
1176
1219
|
function onKeyDown(event) {
|
|
1177
1220
|
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
1221
|
+
if (event.key === "Escape" && open) {
|
|
1222
|
+
event.preventDefault();
|
|
1223
|
+
if (pane !== initialPane) back(pane);
|
|
1224
|
+
else close(true);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
if (disabled) return;
|
|
1178
1228
|
if (!open && [
|
|
1179
1229
|
"Enter",
|
|
1180
1230
|
" ",
|
|
@@ -1188,17 +1238,12 @@ function ModelMenu(props) {
|
|
|
1188
1238
|
requestOpen();
|
|
1189
1239
|
return;
|
|
1190
1240
|
}
|
|
1191
|
-
if (event.key === "Escape" && open) {
|
|
1192
|
-
event.preventDefault();
|
|
1193
|
-
if (pane !== "root") back(pane);
|
|
1194
|
-
else close(true);
|
|
1195
|
-
return;
|
|
1196
|
-
}
|
|
1197
1241
|
if (!open) return;
|
|
1198
1242
|
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
1199
1243
|
event.preventDefault();
|
|
1200
1244
|
if (visibleModels.length > 0) {
|
|
1201
1245
|
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
1246
|
+
setKeyNav((n) => n + 1);
|
|
1202
1247
|
searchRef.current?.focus();
|
|
1203
1248
|
}
|
|
1204
1249
|
return;
|
|
@@ -1207,19 +1252,19 @@ function ModelMenu(props) {
|
|
|
1207
1252
|
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
1208
1253
|
event.preventDefault();
|
|
1209
1254
|
const highlighted = visibleModels[activeModelIndex];
|
|
1210
|
-
if (highlighted) {
|
|
1255
|
+
if (highlighted && !busy) {
|
|
1211
1256
|
onPick({
|
|
1212
1257
|
provider: highlighted.provider,
|
|
1213
1258
|
model: highlighted.model
|
|
1214
1259
|
});
|
|
1215
|
-
close(true);
|
|
1260
|
+
if (closeOnPick) close(true);
|
|
1216
1261
|
}
|
|
1217
1262
|
return;
|
|
1218
1263
|
}
|
|
1219
1264
|
if (event.key === "Tab") {
|
|
1220
1265
|
if (event.shiftKey) {
|
|
1221
1266
|
event.preventDefault();
|
|
1222
|
-
if (pane !==
|
|
1267
|
+
if (pane !== initialPane) back(pane);
|
|
1223
1268
|
else close(true);
|
|
1224
1269
|
return;
|
|
1225
1270
|
}
|
|
@@ -1243,7 +1288,10 @@ function ModelMenu(props) {
|
|
|
1243
1288
|
}
|
|
1244
1289
|
}
|
|
1245
1290
|
function onBlur(event) {
|
|
1246
|
-
|
|
1291
|
+
const related = event.relatedTarget;
|
|
1292
|
+
if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
|
|
1293
|
+
if (!closeOnPick && !(related instanceof Node)) return;
|
|
1294
|
+
if (busy && related == null) return;
|
|
1247
1295
|
if (open) changeOpen(false);
|
|
1248
1296
|
}
|
|
1249
1297
|
itemRefs.current = [];
|
|
@@ -1264,11 +1312,16 @@ function ModelMenu(props) {
|
|
|
1264
1312
|
"aria-label": modelLabel,
|
|
1265
1313
|
lang: language,
|
|
1266
1314
|
children: [
|
|
1315
|
+
menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1316
|
+
className: "dsh-model-menu-header",
|
|
1317
|
+
children: menuHeader
|
|
1318
|
+
}) : null,
|
|
1267
1319
|
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1268
1320
|
ref: itemRef(),
|
|
1269
1321
|
type: "button",
|
|
1270
1322
|
role: "menuitem",
|
|
1271
1323
|
className: "dsh-model-menu-cell",
|
|
1324
|
+
disabled,
|
|
1272
1325
|
onClick: () => drill("model"),
|
|
1273
1326
|
children: [
|
|
1274
1327
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1281,11 +1334,12 @@ function ModelMenu(props) {
|
|
|
1281
1334
|
}),
|
|
1282
1335
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
1283
1336
|
]
|
|
1284
|
-
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1337
|
+
}), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1285
1338
|
ref: itemRef(),
|
|
1286
1339
|
type: "button",
|
|
1287
1340
|
role: "menuitem",
|
|
1288
1341
|
className: "dsh-model-menu-cell",
|
|
1342
|
+
disabled,
|
|
1289
1343
|
onClick: () => drill("effort"),
|
|
1290
1344
|
children: [
|
|
1291
1345
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1304,6 +1358,7 @@ function ModelMenu(props) {
|
|
|
1304
1358
|
className: "dsh-model-menu-searchRow",
|
|
1305
1359
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1306
1360
|
ref: searchRef,
|
|
1361
|
+
disabled,
|
|
1307
1362
|
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
1308
1363
|
type: "text",
|
|
1309
1364
|
role: "searchbox",
|
|
@@ -1320,6 +1375,7 @@ function ModelMenu(props) {
|
|
|
1320
1375
|
type: "button",
|
|
1321
1376
|
className: "dsh-model-menu-searchClear",
|
|
1322
1377
|
"aria-label": clearSearchLabel,
|
|
1378
|
+
disabled,
|
|
1323
1379
|
onClick: () => {
|
|
1324
1380
|
setQuery("");
|
|
1325
1381
|
setHighlightedIndex(null);
|
|
@@ -1342,10 +1398,12 @@ function ModelMenu(props) {
|
|
|
1342
1398
|
type: "button",
|
|
1343
1399
|
role: "menuitemradio",
|
|
1344
1400
|
"aria-checked": item.selected === true,
|
|
1401
|
+
disabled: disabled || busy,
|
|
1345
1402
|
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
1346
1403
|
onClick: () => {
|
|
1404
|
+
if (disabled || busy) return;
|
|
1347
1405
|
onPickLeading?.(item.id);
|
|
1348
|
-
close(true);
|
|
1406
|
+
if (closeOnPick) close(true);
|
|
1349
1407
|
},
|
|
1350
1408
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1351
1409
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1371,6 +1429,7 @@ function ModelMenu(props) {
|
|
|
1371
1429
|
"aria-checked": checked,
|
|
1372
1430
|
id: `${id}-model-${index}`,
|
|
1373
1431
|
tabIndex: showSearch ? -1 : 0,
|
|
1432
|
+
disabled: disabled || busy,
|
|
1374
1433
|
onFocus: () => setHighlightedIndex(index),
|
|
1375
1434
|
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
1376
1435
|
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
@@ -1380,11 +1439,12 @@ function ModelMenu(props) {
|
|
|
1380
1439
|
},
|
|
1381
1440
|
title: choice.label,
|
|
1382
1441
|
onClick: () => {
|
|
1442
|
+
if (disabled || busy) return;
|
|
1383
1443
|
onPick({
|
|
1384
1444
|
provider: choice.provider,
|
|
1385
1445
|
model: choice.model
|
|
1386
1446
|
});
|
|
1387
|
-
close(true);
|
|
1447
|
+
if (closeOnPick) close(true);
|
|
1388
1448
|
},
|
|
1389
1449
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1390
1450
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1400,6 +1460,14 @@ function ModelMenu(props) {
|
|
|
1400
1460
|
})
|
|
1401
1461
|
}, group.provider))]
|
|
1402
1462
|
}),
|
|
1463
|
+
reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
1464
|
+
label: effortLabel,
|
|
1465
|
+
rows: effortRows,
|
|
1466
|
+
selected: selectedEffort ?? "",
|
|
1467
|
+
disabled,
|
|
1468
|
+
busy,
|
|
1469
|
+
onPick: (value) => onPickEffort?.(value)
|
|
1470
|
+
}) : null,
|
|
1403
1471
|
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1404
1472
|
className: "dsh-model-menu-empty",
|
|
1405
1473
|
role: "status",
|
|
@@ -1413,10 +1481,12 @@ function ModelMenu(props) {
|
|
|
1413
1481
|
type: "button",
|
|
1414
1482
|
role: "menuitemradio",
|
|
1415
1483
|
"aria-checked": checked,
|
|
1484
|
+
disabled: disabled || busy,
|
|
1416
1485
|
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
1417
1486
|
onClick: () => {
|
|
1487
|
+
if (disabled || busy) return;
|
|
1418
1488
|
onPickEffort?.(row.id || void 0);
|
|
1419
|
-
close(true);
|
|
1489
|
+
if (closeOnPick) close(true);
|
|
1420
1490
|
},
|
|
1421
1491
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1422
1492
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1435,7 +1505,8 @@ function ModelMenu(props) {
|
|
|
1435
1505
|
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
1436
1506
|
"aria-haspopup": "menu",
|
|
1437
1507
|
"aria-expanded": open,
|
|
1438
|
-
"aria-controls": open ? `${id}-menu` : void 0
|
|
1508
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
1509
|
+
"aria-disabled": disabled || void 0
|
|
1439
1510
|
}) : anchor;
|
|
1440
1511
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1441
1512
|
ref: rootRef,
|
|
@@ -1443,7 +1514,7 @@ function ModelMenu(props) {
|
|
|
1443
1514
|
onKeyDown,
|
|
1444
1515
|
onBlur,
|
|
1445
1516
|
onClick: (event) => {
|
|
1446
|
-
if (event.defaultPrevented) return;
|
|
1517
|
+
if (disabled || event.defaultPrevented) return;
|
|
1447
1518
|
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
1448
1519
|
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
1449
1520
|
requestOpen();
|
|
@@ -1455,6 +1526,102 @@ function ModelMenu(props) {
|
|
|
1455
1526
|
});
|
|
1456
1527
|
}
|
|
1457
1528
|
/**
|
|
1529
|
+
* Discrete reasoning-effort slider shown under the model list in enhanced mode.
|
|
1530
|
+
*
|
|
1531
|
+
* A native range input (`step=1`) over exactly the rows the caller supplies —
|
|
1532
|
+
* the default row plus each effort the selected model advertises — so it can
|
|
1533
|
+
* never invent a level. It previews locally while dragging or arrowing and
|
|
1534
|
+
* commits only when the gesture ends (pointer up, key up, or blur), so a drag
|
|
1535
|
+
* does not write on every step. The menu stays open, so adjusting the effort
|
|
1536
|
+
* never loses the model selection or the search query.
|
|
1537
|
+
*
|
|
1538
|
+
* The input itself stays the entire interaction and accessibility surface —
|
|
1539
|
+
* keyboard, ARIA, disabled and the native step snapping all come from it —
|
|
1540
|
+
* while the track, gradient fill and thumb below are decorative layers painted
|
|
1541
|
+
* from the `--_p` percentage the preview position sets on the rail. Value
|
|
1542
|
+
* changes only update that custom property, so the CSS transitions animate
|
|
1543
|
+
* between levels without remounting nodes or restarting an animation.
|
|
1544
|
+
*/
|
|
1545
|
+
function EffortSlider(props) {
|
|
1546
|
+
const { label, rows, selected, disabled = false, busy = false, onPick } = props;
|
|
1547
|
+
const active = Math.max(0, rows.findIndex((row) => row.id === selected));
|
|
1548
|
+
const [position, setPosition] = (0, react.useState)(active);
|
|
1549
|
+
const positionRef = (0, react.useRef)(position);
|
|
1550
|
+
(0, react.useEffect)(() => {
|
|
1551
|
+
positionRef.current = active;
|
|
1552
|
+
setPosition(active);
|
|
1553
|
+
}, [active]);
|
|
1554
|
+
const preview = (next) => {
|
|
1555
|
+
positionRef.current = next;
|
|
1556
|
+
setPosition(next);
|
|
1557
|
+
};
|
|
1558
|
+
const commit = () => {
|
|
1559
|
+
if (disabled || busy) return;
|
|
1560
|
+
const row = rows[positionRef.current];
|
|
1561
|
+
if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
|
|
1562
|
+
};
|
|
1563
|
+
const max = Math.max(0, rows.length - 1);
|
|
1564
|
+
const percent = max > 0 ? Math.round(position / max * 100) : 0;
|
|
1565
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1566
|
+
className: "dsh-model-menu-sliderGroup",
|
|
1567
|
+
role: "group",
|
|
1568
|
+
"aria-label": label,
|
|
1569
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1570
|
+
className: "dsh-model-menu-sliderHead",
|
|
1571
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1572
|
+
className: "dsh-model-menu-sliderTitle",
|
|
1573
|
+
children: label
|
|
1574
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1575
|
+
className: "dsh-model-menu-sliderValue",
|
|
1576
|
+
children: rows[position]?.name
|
|
1577
|
+
})]
|
|
1578
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1579
|
+
className: "dsh-model-menu-sliderRail",
|
|
1580
|
+
style: { "--_p": percent },
|
|
1581
|
+
children: [
|
|
1582
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1583
|
+
type: "range",
|
|
1584
|
+
className: "dsh-model-menu-sliderInput",
|
|
1585
|
+
min: 0,
|
|
1586
|
+
max,
|
|
1587
|
+
step: 1,
|
|
1588
|
+
value: position,
|
|
1589
|
+
disabled,
|
|
1590
|
+
"aria-label": label,
|
|
1591
|
+
"aria-disabled": disabled || busy || void 0,
|
|
1592
|
+
"aria-valuetext": rows[position]?.name,
|
|
1593
|
+
onChange: (event) => {
|
|
1594
|
+
const value = Number(event.target.value);
|
|
1595
|
+
if (Number.isFinite(value)) preview(value);
|
|
1596
|
+
},
|
|
1597
|
+
onPointerUp: commit,
|
|
1598
|
+
onKeyUp: commit,
|
|
1599
|
+
onBlur: commit
|
|
1600
|
+
}),
|
|
1601
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1602
|
+
className: "dsh-model-menu-sliderTrack",
|
|
1603
|
+
"aria-hidden": "true",
|
|
1604
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1605
|
+
className: "dsh-model-menu-sliderFill",
|
|
1606
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
|
|
1607
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1608
|
+
className: "dsh-model-menu-sliderNodes",
|
|
1609
|
+
children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1610
|
+
"data-covered": index <= position ? "" : void 0,
|
|
1611
|
+
className: "dsh-model-menu-sliderNode",
|
|
1612
|
+
style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
|
|
1613
|
+
}, row.id || "__default"))
|
|
1614
|
+
})]
|
|
1615
|
+
}),
|
|
1616
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1617
|
+
className: "dsh-model-menu-sliderThumb",
|
|
1618
|
+
"aria-hidden": "true"
|
|
1619
|
+
})
|
|
1620
|
+
]
|
|
1621
|
+
})]
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1458
1625
|
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
1459
1626
|
* published dsh-client-ui-model-selection bundle): card capped at
|
|
1460
1627
|
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
@@ -1513,6 +1680,7 @@ const modelMenuCss = `
|
|
|
1513
1680
|
overflow: hidden;
|
|
1514
1681
|
}
|
|
1515
1682
|
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
1683
|
+
.dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
|
|
1516
1684
|
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
1517
1685
|
.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; }
|
|
1518
1686
|
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
@@ -1591,6 +1759,145 @@ const modelMenuCss = `
|
|
|
1591
1759
|
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
1592
1760
|
.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; }
|
|
1593
1761
|
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
1762
|
+
/* Discrete reasoning-effort slider under the model list (enhanced mode).
|
|
1763
|
+
* The painted control is layered over a fully functional native range input:
|
|
1764
|
+
* the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
|
|
1765
|
+
* the decorative track, fill and thumb render beneath it at pointer-events:
|
|
1766
|
+
* none. Position lives in one custom property (--_p, 0–100) on the rail, so
|
|
1767
|
+
* the only animated work a level change triggers is a clip-path/left
|
|
1768
|
+
* transition — no keyframes, no element churn. */
|
|
1769
|
+
.dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
|
|
1770
|
+
.dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
|
|
1771
|
+
.dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
1772
|
+
.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); }
|
|
1773
|
+
.dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
|
|
1774
|
+
.dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
|
|
1775
|
+
.dsh-model-menu-sliderInput {
|
|
1776
|
+
appearance: none;
|
|
1777
|
+
-webkit-appearance: none;
|
|
1778
|
+
box-sizing: border-box;
|
|
1779
|
+
border: 0;
|
|
1780
|
+
cursor: pointer;
|
|
1781
|
+
direction: ltr;
|
|
1782
|
+
opacity: 0;
|
|
1783
|
+
margin: 0;
|
|
1784
|
+
padding: 0;
|
|
1785
|
+
width: 100%;
|
|
1786
|
+
height: 100%;
|
|
1787
|
+
position: absolute;
|
|
1788
|
+
inset: 0;
|
|
1789
|
+
z-index: 1;
|
|
1790
|
+
touch-action: pan-y;
|
|
1791
|
+
}
|
|
1792
|
+
.dsh-model-menu-sliderInput:disabled { cursor: default; }
|
|
1793
|
+
/* The transparent input must keep the painted thumb's geometry: an
|
|
1794
|
+
* appearance:none webkit thumb defaults to 16px, which would shift the
|
|
1795
|
+
* end-point positions 7px off the decorative 30px disc and misplace the
|
|
1796
|
+
* native drag hit target. Track gets the full rail height so the whole band
|
|
1797
|
+
* maps to values identically. */
|
|
1798
|
+
.dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
|
|
1799
|
+
.dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
|
|
1800
|
+
.dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
|
|
1801
|
+
.dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
|
|
1802
|
+
.dsh-model-menu-sliderTrack {
|
|
1803
|
+
pointer-events: none;
|
|
1804
|
+
border-radius: 999px;
|
|
1805
|
+
width: 100%;
|
|
1806
|
+
height: 26px;
|
|
1807
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1808
|
+
box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
|
|
1809
|
+
position: absolute;
|
|
1810
|
+
inset-inline: 0;
|
|
1811
|
+
top: 50%;
|
|
1812
|
+
transform: translateY(-50%);
|
|
1813
|
+
/* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
|
|
1814
|
+
* own clip-path already supplies the rounded reveal shape. */
|
|
1815
|
+
}
|
|
1816
|
+
/* The accent ramp paints the full track width once and is revealed by an
|
|
1817
|
+
* animated clip-path, so raising the effort unwraps more of the spectrum
|
|
1818
|
+
* instead of squashing the same gradient into a narrower bar. The glow lives
|
|
1819
|
+
* on the un-clipped parent: a filter on the clipped element itself would be
|
|
1820
|
+
* clipped away with it. */
|
|
1821
|
+
.dsh-model-menu-sliderFill {
|
|
1822
|
+
display: block;
|
|
1823
|
+
height: 100%;
|
|
1824
|
+
filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
|
|
1825
|
+
transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1826
|
+
}
|
|
1827
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
|
|
1828
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
|
|
1829
|
+
filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
|
|
1830
|
+
}
|
|
1831
|
+
/* The fill stops at the thumb's center, not at the track edge — at a 30px
|
|
1832
|
+
* thumb the end stops sit 15px in from each side, matching the node dots. */
|
|
1833
|
+
.dsh-model-menu-sliderFillBar {
|
|
1834
|
+
display: block;
|
|
1835
|
+
height: 100%;
|
|
1836
|
+
background: linear-gradient(90deg,
|
|
1837
|
+
var(--dsw-alias-state-business-primary),
|
|
1838
|
+
var(--dsw-alias-state-success-primary),
|
|
1839
|
+
var(--dsw-alias-state-warn-primary),
|
|
1840
|
+
var(--dsw-alias-state-error-primary));
|
|
1841
|
+
clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
|
|
1842
|
+
transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1843
|
+
}
|
|
1844
|
+
/* Stop dots: one per declared row (default + every advertised effort), evenly
|
|
1845
|
+
* spread across the band between the two thumb-center extremes (inset 15px,
|
|
1846
|
+
* i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
|
|
1847
|
+
* reads as a pale pin through the gradient; an uncovered one stays a neutral
|
|
1848
|
+
* notch on the empty track. */
|
|
1849
|
+
.dsh-model-menu-sliderNodes {
|
|
1850
|
+
pointer-events: none;
|
|
1851
|
+
display: block;
|
|
1852
|
+
position: absolute;
|
|
1853
|
+
inset-inline: 15px;
|
|
1854
|
+
inset-block: 0;
|
|
1855
|
+
}
|
|
1856
|
+
.dsh-model-menu-sliderNode {
|
|
1857
|
+
pointer-events: none;
|
|
1858
|
+
box-sizing: border-box;
|
|
1859
|
+
width: 4px;
|
|
1860
|
+
height: 4px;
|
|
1861
|
+
border-radius: 50%;
|
|
1862
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
|
|
1863
|
+
position: absolute;
|
|
1864
|
+
left: calc(var(--_n, 0) * 1%);
|
|
1865
|
+
top: 50%;
|
|
1866
|
+
transform: translate(-50%, -50%);
|
|
1867
|
+
transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1868
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1869
|
+
}
|
|
1870
|
+
.dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
|
|
1871
|
+
.dsh-model-menu-sliderThumb {
|
|
1872
|
+
pointer-events: none;
|
|
1873
|
+
box-sizing: border-box;
|
|
1874
|
+
border-radius: 50%;
|
|
1875
|
+
width: 30px;
|
|
1876
|
+
height: 30px;
|
|
1877
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
1878
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
1879
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
|
|
1880
|
+
position: absolute;
|
|
1881
|
+
left: calc((100% - 30px) * var(--_p, 0) / 100);
|
|
1882
|
+
top: 50%;
|
|
1883
|
+
transform: translateY(-50%) scale(0.92);
|
|
1884
|
+
transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1885
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1886
|
+
box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1887
|
+
}
|
|
1888
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
|
|
1889
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
|
|
1890
|
+
transform: translateY(-50%) scale(1.1);
|
|
1891
|
+
box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
|
|
1892
|
+
}
|
|
1893
|
+
.dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
|
|
1894
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
|
|
1895
|
+
0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
1896
|
+
}
|
|
1897
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1898
|
+
.dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
|
|
1899
|
+
.dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
|
|
1900
|
+
}
|
|
1594
1901
|
`;
|
|
1595
1902
|
//#endregion
|
|
1596
1903
|
//#region src/client-copy.ts
|
|
@@ -1676,6 +1983,95 @@ function hubMessage(message, language) {
|
|
|
1676
1983
|
return hubMessages.find((pair) => pair.includes(message))?.[language === "zh" ? 0 : 1] ?? message;
|
|
1677
1984
|
}
|
|
1678
1985
|
//#endregion
|
|
1986
|
+
//#region ../dsh-plugin-kit/src/model-picker.tsx
|
|
1987
|
+
/**
|
|
1988
|
+
* Optional shared model picker (`klarkxy.model-picker` Factory).
|
|
1989
|
+
*
|
|
1990
|
+
* `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
|
|
1991
|
+
* Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
|
|
1992
|
+
* a searchable provider-grouped list and a reasoning-effort slider, built on
|
|
1993
|
+
* the same ModelMenu recipe. Consumers keep their own ModelMenu as the
|
|
1994
|
+
* `fallback`; when no definition is registered the renderer resolves that
|
|
1995
|
+
* fallback verbatim, so the shared picker is strictly opt-in and every saved
|
|
1996
|
+
* value keeps its existing write path.
|
|
1997
|
+
*
|
|
1998
|
+
* `renderFactorySlot` is a framework-composed prop, never a context: the
|
|
1999
|
+
* component a plugin registers into any slot receives it automatically, but
|
|
2000
|
+
* the plugin must pass it down to whichever child renders the menu (or to the
|
|
2001
|
+
* render callback handed to registerSessionSettingsItem).
|
|
2002
|
+
*
|
|
2003
|
+
* The Factory manages its own open state; a consumer's `open`/`onOpenChange`
|
|
2004
|
+
* pair belongs to the fallback menu only and must not be fed to both.
|
|
2005
|
+
*/
|
|
2006
|
+
/**
|
|
2007
|
+
* The trigger every consumer used to hand its own ModelMenu, rebuilt as the
|
|
2008
|
+
* factory's `anchor` local component: identical classes and chevron, the menu
|
|
2009
|
+
* state coming from the occurrence's props instead of local open state.
|
|
2010
|
+
*
|
|
2011
|
+
* `ariaLabel` and `title` are the whole accessible name and tooltip (the
|
|
2012
|
+
* caller formats "field: current route" itself because the factory's resolved
|
|
2013
|
+
* caption may intentionally be a short name). The component identity stays
|
|
2014
|
+
* put across label changes — a new function would unmount the trigger, and
|
|
2015
|
+
* its cleanup would close a menu that `closeOnPick={false}` is keeping open.
|
|
2016
|
+
* The latest label, title, and `onClose` are read from refs.
|
|
2017
|
+
*/
|
|
2018
|
+
function useModelMenuAnchor(ariaLabel, title, onClose) {
|
|
2019
|
+
const closeRef = (0, react.useRef)(onClose);
|
|
2020
|
+
const labelRef = (0, react.useRef)(ariaLabel);
|
|
2021
|
+
const titleRef = (0, react.useRef)(title);
|
|
2022
|
+
closeRef.current = onClose;
|
|
2023
|
+
labelRef.current = ariaLabel;
|
|
2024
|
+
titleRef.current = title;
|
|
2025
|
+
return (0, react.useMemo)(() => {
|
|
2026
|
+
const ModelPickerTrigger = (props) => {
|
|
2027
|
+
(0, react.useEffect)(() => () => closeRef.current?.(), []);
|
|
2028
|
+
const record = props;
|
|
2029
|
+
const injected = {};
|
|
2030
|
+
for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
|
|
2031
|
+
const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
|
|
2032
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2033
|
+
type: "button",
|
|
2034
|
+
className: `dsh-model-menu-trigger${extraClass}`,
|
|
2035
|
+
disabled: props.disabled,
|
|
2036
|
+
"aria-haspopup": "menu",
|
|
2037
|
+
"aria-expanded": props.open,
|
|
2038
|
+
...injected,
|
|
2039
|
+
"aria-label": labelRef.current,
|
|
2040
|
+
title: titleRef.current,
|
|
2041
|
+
children: [
|
|
2042
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2043
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2044
|
+
children: props.label
|
|
2045
|
+
}),
|
|
2046
|
+
props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2047
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2048
|
+
children: props.effortLabel
|
|
2049
|
+
}) : null,
|
|
2050
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2051
|
+
]
|
|
2052
|
+
});
|
|
2053
|
+
};
|
|
2054
|
+
return ModelPickerTrigger;
|
|
2055
|
+
}, []);
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* Render the shared model picker while it is registered; `options.fallback`
|
|
2059
|
+
* whenever it is not (plugin absent, disabled, or unloaded).
|
|
2060
|
+
*
|
|
2061
|
+
* `options.anchor` is optional: the factory ships a default trigger, so only
|
|
2062
|
+
* consumers that need their own chrome (custom aria-label, tooltip, class)
|
|
2063
|
+
* pass one built with {@link useModelMenuAnchor}. The returned node is never
|
|
2064
|
+
* assumed to be the factory's own output — a defensive `?? fallback` covers a
|
|
2065
|
+
* renderer that yields null for an unregistered definition.
|
|
2066
|
+
*/
|
|
2067
|
+
function renderModelPicker(renderFactorySlot, props, options) {
|
|
2068
|
+
if (typeof renderFactorySlot !== "function") return options.fallback;
|
|
2069
|
+
return renderFactorySlot("klarkxy.model-picker", props, {
|
|
2070
|
+
...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
|
|
2071
|
+
fallback: options.fallback
|
|
2072
|
+
}) ?? options.fallback;
|
|
2073
|
+
}
|
|
2074
|
+
//#endregion
|
|
1679
2075
|
//#region src/client-model-menu.tsx
|
|
1680
2076
|
/**
|
|
1681
2077
|
* One detected field's standard editor: the same ModelMenu every marked field gets.
|
|
@@ -1686,7 +2082,7 @@ function hubMessage(message, language) {
|
|
|
1686
2082
|
* the current reasoning effort when the new model advertises it, so switching a
|
|
1687
2083
|
* route does not silently reset the user's choice.
|
|
1688
2084
|
*/
|
|
1689
|
-
function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet }) {
|
|
2085
|
+
function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet, renderFactorySlot }) {
|
|
1690
2086
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1691
2087
|
(0, react.useEffect)(() => {
|
|
1692
2088
|
if (disabled) setOpen(false);
|
|
@@ -1707,46 +2103,28 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
|
|
|
1707
2103
|
const currentLabel = display === "short" && !follow ? selected ? modelMenuShortName(selected) : route.model : menuLabel;
|
|
1708
2104
|
const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
|
|
1709
2105
|
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
|
|
1710
|
-
|
|
2106
|
+
const anchor = useModelMenuAnchor(accessible, routeLabel, () => setOpen(false));
|
|
2107
|
+
const pick = (picked) => {
|
|
2108
|
+
const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
|
|
2109
|
+
const effort = route.reasoningEffort;
|
|
2110
|
+
const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
|
|
2111
|
+
onSet({
|
|
2112
|
+
provider: picked.provider,
|
|
2113
|
+
model: picked.model,
|
|
2114
|
+
...supported ? { reasoningEffort: effort } : {}
|
|
2115
|
+
});
|
|
2116
|
+
};
|
|
2117
|
+
return renderModelPicker(renderFactorySlot, {
|
|
1711
2118
|
locale: t.language,
|
|
1712
|
-
|
|
2119
|
+
disabled,
|
|
1713
2120
|
onOpenChange: (next) => setOpen(next && !disabled),
|
|
1714
2121
|
listClassName: "dsh-model-hub",
|
|
1715
|
-
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1716
|
-
type: "button",
|
|
1717
|
-
className: "dsh-model-menu-trigger",
|
|
1718
|
-
disabled,
|
|
1719
|
-
"aria-label": accessible,
|
|
1720
|
-
title: routeLabel,
|
|
1721
|
-
"aria-haspopup": "menu",
|
|
1722
|
-
"aria-expanded": open && !disabled,
|
|
1723
|
-
children: [
|
|
1724
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1725
|
-
className: "dsh-model-menu-triggerLabel",
|
|
1726
|
-
children: currentLabel
|
|
1727
|
-
}),
|
|
1728
|
-
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1729
|
-
className: "dsh-model-menu-triggerEffort",
|
|
1730
|
-
children: ` · ${effortName ?? t("effortDefault")}`
|
|
1731
|
-
}) : null,
|
|
1732
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
1733
|
-
]
|
|
1734
|
-
}),
|
|
1735
2122
|
choices,
|
|
1736
2123
|
selected: follow ? void 0 : {
|
|
1737
2124
|
provider: route.provider,
|
|
1738
2125
|
model: route.model
|
|
1739
2126
|
},
|
|
1740
|
-
onPick:
|
|
1741
|
-
const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
|
|
1742
|
-
const effort = route.reasoningEffort;
|
|
1743
|
-
const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
|
|
1744
|
-
onSet({
|
|
1745
|
-
provider: picked.provider,
|
|
1746
|
-
model: picked.model,
|
|
1747
|
-
...supported ? { reasoningEffort: effort } : {}
|
|
1748
|
-
});
|
|
1749
|
-
},
|
|
2127
|
+
onPick: pick,
|
|
1750
2128
|
leading: allowFollow ? [{
|
|
1751
2129
|
id: "follow",
|
|
1752
2130
|
label: followLabel || t("follow"),
|
|
@@ -1766,6 +2144,8 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
|
|
|
1766
2144
|
model: route.model,
|
|
1767
2145
|
...effort ? { reasoningEffort: effort } : {}
|
|
1768
2146
|
}),
|
|
2147
|
+
triggerLabel: currentLabel,
|
|
2148
|
+
triggerEffortLabel: !follow && efforts.length ? ` · ${effortName ?? t("effortDefault")}` : void 0,
|
|
1769
2149
|
modelLabel: t("model"),
|
|
1770
2150
|
modelValue: currentLabel,
|
|
1771
2151
|
effortLabel: t("effort"),
|
|
@@ -1773,6 +2153,66 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
|
|
|
1773
2153
|
searchPlaceholder: t("searchModels"),
|
|
1774
2154
|
emptyLabel: t("noModels"),
|
|
1775
2155
|
clearSearchLabel: t("clearSearch")
|
|
2156
|
+
}, {
|
|
2157
|
+
anchor,
|
|
2158
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
2159
|
+
locale: t.language,
|
|
2160
|
+
open: open && !disabled,
|
|
2161
|
+
onOpenChange: (next) => setOpen(next && !disabled),
|
|
2162
|
+
listClassName: "dsh-model-hub",
|
|
2163
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2164
|
+
type: "button",
|
|
2165
|
+
className: "dsh-model-menu-trigger",
|
|
2166
|
+
disabled,
|
|
2167
|
+
"aria-label": accessible,
|
|
2168
|
+
title: routeLabel,
|
|
2169
|
+
"aria-haspopup": "menu",
|
|
2170
|
+
"aria-expanded": open && !disabled,
|
|
2171
|
+
children: [
|
|
2172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2173
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2174
|
+
children: currentLabel
|
|
2175
|
+
}),
|
|
2176
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2177
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2178
|
+
children: ` · ${effortName ?? t("effortDefault")}`
|
|
2179
|
+
}) : null,
|
|
2180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2181
|
+
]
|
|
2182
|
+
}),
|
|
2183
|
+
choices,
|
|
2184
|
+
selected: follow ? void 0 : {
|
|
2185
|
+
provider: route.provider,
|
|
2186
|
+
model: route.model
|
|
2187
|
+
},
|
|
2188
|
+
onPick: pick,
|
|
2189
|
+
leading: allowFollow ? [{
|
|
2190
|
+
id: "follow",
|
|
2191
|
+
label: followLabel || t("follow"),
|
|
2192
|
+
selected: follow
|
|
2193
|
+
}] : [],
|
|
2194
|
+
onPickLeading: () => onSet({
|
|
2195
|
+
provider: "",
|
|
2196
|
+
model: ""
|
|
2197
|
+
}),
|
|
2198
|
+
efforts,
|
|
2199
|
+
selectedEffort: route.reasoningEffort || "",
|
|
2200
|
+
onPickEffort: (effort) => onSet(follow ? {
|
|
2201
|
+
provider: "",
|
|
2202
|
+
model: ""
|
|
2203
|
+
} : {
|
|
2204
|
+
provider: route.provider,
|
|
2205
|
+
model: route.model,
|
|
2206
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
2207
|
+
}),
|
|
2208
|
+
modelLabel: t("model"),
|
|
2209
|
+
modelValue: currentLabel,
|
|
2210
|
+
effortLabel: t("effort"),
|
|
2211
|
+
defaultEffortLabel: t("effortDefault"),
|
|
2212
|
+
searchPlaceholder: t("searchModels"),
|
|
2213
|
+
emptyLabel: t("noModels"),
|
|
2214
|
+
clearSearchLabel: t("clearSearch")
|
|
2215
|
+
})
|
|
1776
2216
|
});
|
|
1777
2217
|
}
|
|
1778
2218
|
function messageOf(error) {
|
|
@@ -1908,7 +2348,7 @@ function routeInput(route) {
|
|
|
1908
2348
|
};
|
|
1909
2349
|
}
|
|
1910
2350
|
/** One cast entry: model picker, tendency input saved on blur, remove button. */
|
|
1911
|
-
function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
|
|
2351
|
+
function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, renderFactorySlot, onSetRoute, onSetTendency, onRemove }) {
|
|
1912
2352
|
const label = `${route.provider} / ${route.model}`;
|
|
1913
2353
|
const saved = route.tendency ?? "";
|
|
1914
2354
|
const [draft, setDraft] = (0, react.useState)({
|
|
@@ -1951,6 +2391,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
|
|
|
1951
2391
|
reasoningEffort: true,
|
|
1952
2392
|
display: "short",
|
|
1953
2393
|
disabled: menuDisabled,
|
|
2394
|
+
renderFactorySlot,
|
|
1954
2395
|
onSet: onSetRoute
|
|
1955
2396
|
})
|
|
1956
2397
|
}),
|
|
@@ -1987,7 +2428,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
|
|
|
1987
2428
|
]
|
|
1988
2429
|
});
|
|
1989
2430
|
}
|
|
1990
|
-
function CastPage({ api, locale }) {
|
|
2431
|
+
function CastPage({ api, locale, renderFactorySlot }) {
|
|
1991
2432
|
const t = useText$1(locale);
|
|
1992
2433
|
const [cast, setCast] = (0, react.useState)(null);
|
|
1993
2434
|
const [catalog, setCatalog] = (0, react.useState)(null);
|
|
@@ -2223,6 +2664,7 @@ function CastPage({ api, locale }) {
|
|
|
2223
2664
|
t,
|
|
2224
2665
|
catalog,
|
|
2225
2666
|
route,
|
|
2667
|
+
renderFactorySlot,
|
|
2226
2668
|
tendencyRevision: cast.tendencyRevision,
|
|
2227
2669
|
draftReview,
|
|
2228
2670
|
tendencyDisabled: disabled || !!cast.tendencyError || cast.tendencyRevision === void 0,
|
|
@@ -2253,6 +2695,7 @@ function CastPage({ api, locale }) {
|
|
|
2253
2695
|
reasoningEffort: true,
|
|
2254
2696
|
display: "short",
|
|
2255
2697
|
disabled: disabled || catalog === null,
|
|
2698
|
+
renderFactorySlot,
|
|
2256
2699
|
onSet: (picked) => {
|
|
2257
2700
|
if (!picked.provider || !picked.model) {
|
|
2258
2701
|
setPending(void 0);
|
|
@@ -3152,13 +3595,14 @@ function localeActive(locale) {
|
|
|
3152
3595
|
return String(typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en");
|
|
3153
3596
|
}
|
|
3154
3597
|
const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}`;
|
|
3155
|
-
function SubagentModelsPage({ api, locale }) {
|
|
3598
|
+
function SubagentModelsPage({ api, locale, renderFactorySlot }) {
|
|
3156
3599
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3157
3600
|
className: "dsh-model-hub dsh-ui-page",
|
|
3158
3601
|
"aria-label": localeActive(locale).toLowerCase().startsWith("zh") ? "子代理" : "Subagents",
|
|
3159
3602
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
|
|
3160
3603
|
api,
|
|
3161
|
-
locale
|
|
3604
|
+
locale,
|
|
3605
|
+
renderFactorySlot
|
|
3162
3606
|
})]
|
|
3163
3607
|
});
|
|
3164
3608
|
}
|