@klarkxy/dsh-model-hub 0.2.1 → 0.2.2
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.inner.cjs
CHANGED
|
@@ -92,7 +92,9 @@ Object.values({
|
|
|
92
92
|
"--dsh-scrollbar-thumb-hover",
|
|
93
93
|
"--dsh-content-font-size-secondary",
|
|
94
94
|
"--dsh-content-font-delta",
|
|
95
|
-
"--dsh-frame-top-clearance"
|
|
95
|
+
"--dsh-frame-top-clearance",
|
|
96
|
+
"--dsh-composer-model-icon-display",
|
|
97
|
+
"--dsh-composer-model-text-display"
|
|
96
98
|
]
|
|
97
99
|
}).flat();
|
|
98
100
|
/**
|
|
@@ -540,6 +542,7 @@ function parseModelMenuChoices(value, bound) {
|
|
|
540
542
|
provider,
|
|
541
543
|
model,
|
|
542
544
|
label: `${providerName} / ${name}`,
|
|
545
|
+
providerLabel: providerName,
|
|
543
546
|
efforts
|
|
544
547
|
});
|
|
545
548
|
}
|
|
@@ -586,11 +589,11 @@ function modelMenuShortName(choice) {
|
|
|
586
589
|
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
587
590
|
function groupModelMenuChoices(choices, query) {
|
|
588
591
|
const needle = query.trim().toLowerCase();
|
|
589
|
-
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
592
|
+
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;
|
|
590
593
|
const groups = [];
|
|
591
594
|
for (const choice of matched) {
|
|
592
595
|
const sep = choice.label.indexOf(" / ");
|
|
593
|
-
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
596
|
+
const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
|
|
594
597
|
const last = groups.at(-1);
|
|
595
598
|
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
596
599
|
else groups.push({
|
|
@@ -976,7 +979,7 @@ function ModelMenu(props) {
|
|
|
976
979
|
empty: "No matching models",
|
|
977
980
|
clear: "Clear search"
|
|
978
981
|
};
|
|
979
|
-
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;
|
|
982
|
+
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;
|
|
980
983
|
const rootRef = (0, react.useRef)(null);
|
|
981
984
|
const openRef = (0, react.useRef)(open);
|
|
982
985
|
openRef.current = open;
|
|
@@ -991,9 +994,10 @@ function ModelMenu(props) {
|
|
|
991
994
|
const [pane, setPane] = (0, react.useState)("root");
|
|
992
995
|
const [query, setQuery] = (0, react.useState)("");
|
|
993
996
|
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
997
|
+
const [keyNav, setKeyNav] = (0, react.useState)(0);
|
|
994
998
|
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
995
999
|
const id = (0, react.useId)();
|
|
996
|
-
const showSearch = choices.length > 4;
|
|
1000
|
+
const showSearch = alwaysShowSearch || choices.length > 4;
|
|
997
1001
|
const effortRows = (0, react.useMemo)(() => [{
|
|
998
1002
|
id: "",
|
|
999
1003
|
name: defaultEffortLabel
|
|
@@ -1010,7 +1014,12 @@ function ModelMenu(props) {
|
|
|
1010
1014
|
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
1011
1015
|
choice,
|
|
1012
1016
|
name: modelMenuShortName(choice),
|
|
1013
|
-
label:
|
|
1017
|
+
label: [
|
|
1018
|
+
choice.provider,
|
|
1019
|
+
choice.providerLabel,
|
|
1020
|
+
choice.model,
|
|
1021
|
+
choice.label
|
|
1022
|
+
].filter(Boolean).join(" ")
|
|
1014
1023
|
})), showSearch ? query.trim() : "")
|
|
1015
1024
|
})).filter((group) => group.items.length > 0), [
|
|
1016
1025
|
choices,
|
|
@@ -1042,10 +1051,12 @@ function ModelMenu(props) {
|
|
|
1042
1051
|
paneFocus.current = "drill";
|
|
1043
1052
|
setPane(next);
|
|
1044
1053
|
};
|
|
1045
|
-
/** Leave a drilled pane for the
|
|
1054
|
+
/** Leave a drilled pane for the entry pane, handing the keyboard back to it.
|
|
1055
|
+
* A root entry refocuses the cell that opened the pane; a search-first
|
|
1056
|
+
* (model) entry refocuses that pane's own intent. */
|
|
1046
1057
|
const back = (from) => {
|
|
1047
|
-
paneFocus.current = from;
|
|
1048
|
-
setPane(
|
|
1058
|
+
paneFocus.current = initialPane === "root" ? from : "drill";
|
|
1059
|
+
setPane(initialPane);
|
|
1049
1060
|
};
|
|
1050
1061
|
const moveFocus = (offset) => {
|
|
1051
1062
|
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
@@ -1085,10 +1096,18 @@ function ModelMenu(props) {
|
|
|
1085
1096
|
}, [requestedOpen, open]);
|
|
1086
1097
|
(0, react.useEffect)(() => {
|
|
1087
1098
|
if (!open) return;
|
|
1088
|
-
setPane(
|
|
1099
|
+
setPane(initialPane);
|
|
1100
|
+
if (initialPane === "model") paneFocus.current = "drill";
|
|
1089
1101
|
setQuery("");
|
|
1090
1102
|
setHighlightedIndex(null);
|
|
1091
|
-
}, [open]);
|
|
1103
|
+
}, [open, initialPane]);
|
|
1104
|
+
(0, react.useEffect)(() => {
|
|
1105
|
+
if (disabled && open && closeOnDisable) changeOpen(false);
|
|
1106
|
+
}, [
|
|
1107
|
+
disabled,
|
|
1108
|
+
open,
|
|
1109
|
+
closeOnDisable
|
|
1110
|
+
]);
|
|
1092
1111
|
(0, react.useEffect)(() => {
|
|
1093
1112
|
if (!open) return;
|
|
1094
1113
|
const closeOutside = (event) => {
|
|
@@ -1096,9 +1115,25 @@ function ModelMenu(props) {
|
|
|
1096
1115
|
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
1097
1116
|
changeOpen(false);
|
|
1098
1117
|
};
|
|
1118
|
+
const escapeOutside = (event) => {
|
|
1119
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
1120
|
+
if (!openRef.current) return;
|
|
1121
|
+
event.preventDefault();
|
|
1122
|
+
if (pane !== initialPane) back(pane);
|
|
1123
|
+
else close(true);
|
|
1124
|
+
};
|
|
1099
1125
|
document.addEventListener("mousedown", closeOutside);
|
|
1100
|
-
|
|
1101
|
-
|
|
1126
|
+
document.addEventListener("keydown", escapeOutside);
|
|
1127
|
+
return () => {
|
|
1128
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
1129
|
+
document.removeEventListener("keydown", escapeOutside);
|
|
1130
|
+
};
|
|
1131
|
+
}, [
|
|
1132
|
+
open,
|
|
1133
|
+
onOpenChange,
|
|
1134
|
+
pane,
|
|
1135
|
+
initialPane
|
|
1136
|
+
]);
|
|
1102
1137
|
(0, react.useEffect)(() => {
|
|
1103
1138
|
const intent = paneFocus.current;
|
|
1104
1139
|
if (!open || menuPos === null || intent === null) return;
|
|
@@ -1128,13 +1163,21 @@ function ModelMenu(props) {
|
|
|
1128
1163
|
pane,
|
|
1129
1164
|
filteredGroups
|
|
1130
1165
|
]);
|
|
1166
|
+
const modelPaneOpen = open && pane === "model";
|
|
1167
|
+
const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
|
|
1168
|
+
const scrollSignalRef = (0, react.useRef)(null);
|
|
1131
1169
|
(0, react.useLayoutEffect)(() => {
|
|
1132
|
-
if (
|
|
1170
|
+
if (!modelPaneOpen || activeModelIndex < 0) {
|
|
1171
|
+
if (!modelPaneOpen) scrollSignalRef.current = null;
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
if (scrollSignalRef.current === scrollSignal) return;
|
|
1175
|
+
scrollSignalRef.current = scrollSignal;
|
|
1176
|
+
itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
1133
1177
|
}, [
|
|
1134
|
-
|
|
1135
|
-
|
|
1178
|
+
modelPaneOpen,
|
|
1179
|
+
scrollSignal,
|
|
1136
1180
|
activeModelIndex,
|
|
1137
|
-
visibleModels,
|
|
1138
1181
|
leading.length
|
|
1139
1182
|
]);
|
|
1140
1183
|
(0, react.useLayoutEffect)(() => {
|
|
@@ -1149,7 +1192,7 @@ function ModelMenu(props) {
|
|
|
1149
1192
|
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
1150
1193
|
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
1151
1194
|
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
1152
|
-
setMenuPos({
|
|
1195
|
+
setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
|
|
1153
1196
|
left: x,
|
|
1154
1197
|
top: y
|
|
1155
1198
|
});
|
|
@@ -1169,6 +1212,13 @@ function ModelMenu(props) {
|
|
|
1169
1212
|
]);
|
|
1170
1213
|
function onKeyDown(event) {
|
|
1171
1214
|
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
1215
|
+
if (event.key === "Escape" && open) {
|
|
1216
|
+
event.preventDefault();
|
|
1217
|
+
if (pane !== initialPane) back(pane);
|
|
1218
|
+
else close(true);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (disabled) return;
|
|
1172
1222
|
if (!open && [
|
|
1173
1223
|
"Enter",
|
|
1174
1224
|
" ",
|
|
@@ -1182,17 +1232,12 @@ function ModelMenu(props) {
|
|
|
1182
1232
|
requestOpen();
|
|
1183
1233
|
return;
|
|
1184
1234
|
}
|
|
1185
|
-
if (event.key === "Escape" && open) {
|
|
1186
|
-
event.preventDefault();
|
|
1187
|
-
if (pane !== "root") back(pane);
|
|
1188
|
-
else close(true);
|
|
1189
|
-
return;
|
|
1190
|
-
}
|
|
1191
1235
|
if (!open) return;
|
|
1192
1236
|
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
1193
1237
|
event.preventDefault();
|
|
1194
1238
|
if (visibleModels.length > 0) {
|
|
1195
1239
|
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
1240
|
+
setKeyNav((n) => n + 1);
|
|
1196
1241
|
searchRef.current?.focus();
|
|
1197
1242
|
}
|
|
1198
1243
|
return;
|
|
@@ -1201,19 +1246,19 @@ function ModelMenu(props) {
|
|
|
1201
1246
|
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
1202
1247
|
event.preventDefault();
|
|
1203
1248
|
const highlighted = visibleModels[activeModelIndex];
|
|
1204
|
-
if (highlighted) {
|
|
1249
|
+
if (highlighted && !busy) {
|
|
1205
1250
|
onPick({
|
|
1206
1251
|
provider: highlighted.provider,
|
|
1207
1252
|
model: highlighted.model
|
|
1208
1253
|
});
|
|
1209
|
-
close(true);
|
|
1254
|
+
if (closeOnPick) close(true);
|
|
1210
1255
|
}
|
|
1211
1256
|
return;
|
|
1212
1257
|
}
|
|
1213
1258
|
if (event.key === "Tab") {
|
|
1214
1259
|
if (event.shiftKey) {
|
|
1215
1260
|
event.preventDefault();
|
|
1216
|
-
if (pane !==
|
|
1261
|
+
if (pane !== initialPane) back(pane);
|
|
1217
1262
|
else close(true);
|
|
1218
1263
|
return;
|
|
1219
1264
|
}
|
|
@@ -1237,7 +1282,10 @@ function ModelMenu(props) {
|
|
|
1237
1282
|
}
|
|
1238
1283
|
}
|
|
1239
1284
|
function onBlur(event) {
|
|
1240
|
-
|
|
1285
|
+
const related = event.relatedTarget;
|
|
1286
|
+
if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
|
|
1287
|
+
if (!closeOnPick && !(related instanceof Node)) return;
|
|
1288
|
+
if (busy && related == null) return;
|
|
1241
1289
|
if (open) changeOpen(false);
|
|
1242
1290
|
}
|
|
1243
1291
|
itemRefs.current = [];
|
|
@@ -1258,11 +1306,16 @@ function ModelMenu(props) {
|
|
|
1258
1306
|
"aria-label": modelLabel,
|
|
1259
1307
|
lang: language,
|
|
1260
1308
|
children: [
|
|
1309
|
+
menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1310
|
+
className: "dsh-model-menu-header",
|
|
1311
|
+
children: menuHeader
|
|
1312
|
+
}) : null,
|
|
1261
1313
|
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1262
1314
|
ref: itemRef(),
|
|
1263
1315
|
type: "button",
|
|
1264
1316
|
role: "menuitem",
|
|
1265
1317
|
className: "dsh-model-menu-cell",
|
|
1318
|
+
disabled,
|
|
1266
1319
|
onClick: () => drill("model"),
|
|
1267
1320
|
children: [
|
|
1268
1321
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1275,11 +1328,12 @@ function ModelMenu(props) {
|
|
|
1275
1328
|
}),
|
|
1276
1329
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
1277
1330
|
]
|
|
1278
|
-
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1331
|
+
}), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1279
1332
|
ref: itemRef(),
|
|
1280
1333
|
type: "button",
|
|
1281
1334
|
role: "menuitem",
|
|
1282
1335
|
className: "dsh-model-menu-cell",
|
|
1336
|
+
disabled,
|
|
1283
1337
|
onClick: () => drill("effort"),
|
|
1284
1338
|
children: [
|
|
1285
1339
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1298,6 +1352,7 @@ function ModelMenu(props) {
|
|
|
1298
1352
|
className: "dsh-model-menu-searchRow",
|
|
1299
1353
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1300
1354
|
ref: searchRef,
|
|
1355
|
+
disabled,
|
|
1301
1356
|
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
1302
1357
|
type: "text",
|
|
1303
1358
|
role: "searchbox",
|
|
@@ -1314,6 +1369,7 @@ function ModelMenu(props) {
|
|
|
1314
1369
|
type: "button",
|
|
1315
1370
|
className: "dsh-model-menu-searchClear",
|
|
1316
1371
|
"aria-label": clearSearchLabel,
|
|
1372
|
+
disabled,
|
|
1317
1373
|
onClick: () => {
|
|
1318
1374
|
setQuery("");
|
|
1319
1375
|
setHighlightedIndex(null);
|
|
@@ -1336,10 +1392,12 @@ function ModelMenu(props) {
|
|
|
1336
1392
|
type: "button",
|
|
1337
1393
|
role: "menuitemradio",
|
|
1338
1394
|
"aria-checked": item.selected === true,
|
|
1395
|
+
disabled: disabled || busy,
|
|
1339
1396
|
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
1340
1397
|
onClick: () => {
|
|
1398
|
+
if (disabled || busy) return;
|
|
1341
1399
|
onPickLeading?.(item.id);
|
|
1342
|
-
close(true);
|
|
1400
|
+
if (closeOnPick) close(true);
|
|
1343
1401
|
},
|
|
1344
1402
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1345
1403
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1365,6 +1423,7 @@ function ModelMenu(props) {
|
|
|
1365
1423
|
"aria-checked": checked,
|
|
1366
1424
|
id: `${id}-model-${index}`,
|
|
1367
1425
|
tabIndex: showSearch ? -1 : 0,
|
|
1426
|
+
disabled: disabled || busy,
|
|
1368
1427
|
onFocus: () => setHighlightedIndex(index),
|
|
1369
1428
|
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
1370
1429
|
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
@@ -1374,11 +1433,12 @@ function ModelMenu(props) {
|
|
|
1374
1433
|
},
|
|
1375
1434
|
title: choice.label,
|
|
1376
1435
|
onClick: () => {
|
|
1436
|
+
if (disabled || busy) return;
|
|
1377
1437
|
onPick({
|
|
1378
1438
|
provider: choice.provider,
|
|
1379
1439
|
model: choice.model
|
|
1380
1440
|
});
|
|
1381
|
-
close(true);
|
|
1441
|
+
if (closeOnPick) close(true);
|
|
1382
1442
|
},
|
|
1383
1443
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1384
1444
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1394,6 +1454,14 @@ function ModelMenu(props) {
|
|
|
1394
1454
|
})
|
|
1395
1455
|
}, group.provider))]
|
|
1396
1456
|
}),
|
|
1457
|
+
reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
1458
|
+
label: effortLabel,
|
|
1459
|
+
rows: effortRows,
|
|
1460
|
+
selected: selectedEffort ?? "",
|
|
1461
|
+
disabled,
|
|
1462
|
+
busy,
|
|
1463
|
+
onPick: (value) => onPickEffort?.(value)
|
|
1464
|
+
}) : null,
|
|
1397
1465
|
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1398
1466
|
className: "dsh-model-menu-empty",
|
|
1399
1467
|
role: "status",
|
|
@@ -1407,10 +1475,12 @@ function ModelMenu(props) {
|
|
|
1407
1475
|
type: "button",
|
|
1408
1476
|
role: "menuitemradio",
|
|
1409
1477
|
"aria-checked": checked,
|
|
1478
|
+
disabled: disabled || busy,
|
|
1410
1479
|
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
1411
1480
|
onClick: () => {
|
|
1481
|
+
if (disabled || busy) return;
|
|
1412
1482
|
onPickEffort?.(row.id || void 0);
|
|
1413
|
-
close(true);
|
|
1483
|
+
if (closeOnPick) close(true);
|
|
1414
1484
|
},
|
|
1415
1485
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1416
1486
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1429,7 +1499,8 @@ function ModelMenu(props) {
|
|
|
1429
1499
|
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
1430
1500
|
"aria-haspopup": "menu",
|
|
1431
1501
|
"aria-expanded": open,
|
|
1432
|
-
"aria-controls": open ? `${id}-menu` : void 0
|
|
1502
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
1503
|
+
"aria-disabled": disabled || void 0
|
|
1433
1504
|
}) : anchor;
|
|
1434
1505
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1435
1506
|
ref: rootRef,
|
|
@@ -1437,7 +1508,7 @@ function ModelMenu(props) {
|
|
|
1437
1508
|
onKeyDown,
|
|
1438
1509
|
onBlur,
|
|
1439
1510
|
onClick: (event) => {
|
|
1440
|
-
if (event.defaultPrevented) return;
|
|
1511
|
+
if (disabled || event.defaultPrevented) return;
|
|
1441
1512
|
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
1442
1513
|
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
1443
1514
|
requestOpen();
|
|
@@ -1449,6 +1520,102 @@ function ModelMenu(props) {
|
|
|
1449
1520
|
});
|
|
1450
1521
|
}
|
|
1451
1522
|
/**
|
|
1523
|
+
* Discrete reasoning-effort slider shown under the model list in enhanced mode.
|
|
1524
|
+
*
|
|
1525
|
+
* A native range input (`step=1`) over exactly the rows the caller supplies —
|
|
1526
|
+
* the default row plus each effort the selected model advertises — so it can
|
|
1527
|
+
* never invent a level. It previews locally while dragging or arrowing and
|
|
1528
|
+
* commits only when the gesture ends (pointer up, key up, or blur), so a drag
|
|
1529
|
+
* does not write on every step. The menu stays open, so adjusting the effort
|
|
1530
|
+
* never loses the model selection or the search query.
|
|
1531
|
+
*
|
|
1532
|
+
* The input itself stays the entire interaction and accessibility surface —
|
|
1533
|
+
* keyboard, ARIA, disabled and the native step snapping all come from it —
|
|
1534
|
+
* while the track, gradient fill and thumb below are decorative layers painted
|
|
1535
|
+
* from the `--_p` percentage the preview position sets on the rail. Value
|
|
1536
|
+
* changes only update that custom property, so the CSS transitions animate
|
|
1537
|
+
* between levels without remounting nodes or restarting an animation.
|
|
1538
|
+
*/
|
|
1539
|
+
function EffortSlider(props) {
|
|
1540
|
+
const { label, rows, selected, disabled = false, busy = false, onPick } = props;
|
|
1541
|
+
const active = Math.max(0, rows.findIndex((row) => row.id === selected));
|
|
1542
|
+
const [position, setPosition] = (0, react.useState)(active);
|
|
1543
|
+
const positionRef = (0, react.useRef)(position);
|
|
1544
|
+
(0, react.useEffect)(() => {
|
|
1545
|
+
positionRef.current = active;
|
|
1546
|
+
setPosition(active);
|
|
1547
|
+
}, [active]);
|
|
1548
|
+
const preview = (next) => {
|
|
1549
|
+
positionRef.current = next;
|
|
1550
|
+
setPosition(next);
|
|
1551
|
+
};
|
|
1552
|
+
const commit = () => {
|
|
1553
|
+
if (disabled || busy) return;
|
|
1554
|
+
const row = rows[positionRef.current];
|
|
1555
|
+
if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
|
|
1556
|
+
};
|
|
1557
|
+
const max = Math.max(0, rows.length - 1);
|
|
1558
|
+
const percent = max > 0 ? Math.round(position / max * 100) : 0;
|
|
1559
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1560
|
+
className: "dsh-model-menu-sliderGroup",
|
|
1561
|
+
role: "group",
|
|
1562
|
+
"aria-label": label,
|
|
1563
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1564
|
+
className: "dsh-model-menu-sliderHead",
|
|
1565
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1566
|
+
className: "dsh-model-menu-sliderTitle",
|
|
1567
|
+
children: label
|
|
1568
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1569
|
+
className: "dsh-model-menu-sliderValue",
|
|
1570
|
+
children: rows[position]?.name
|
|
1571
|
+
})]
|
|
1572
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1573
|
+
className: "dsh-model-menu-sliderRail",
|
|
1574
|
+
style: { "--_p": percent },
|
|
1575
|
+
children: [
|
|
1576
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1577
|
+
type: "range",
|
|
1578
|
+
className: "dsh-model-menu-sliderInput",
|
|
1579
|
+
min: 0,
|
|
1580
|
+
max,
|
|
1581
|
+
step: 1,
|
|
1582
|
+
value: position,
|
|
1583
|
+
disabled,
|
|
1584
|
+
"aria-label": label,
|
|
1585
|
+
"aria-disabled": disabled || busy || void 0,
|
|
1586
|
+
"aria-valuetext": rows[position]?.name,
|
|
1587
|
+
onChange: (event) => {
|
|
1588
|
+
const value = Number(event.target.value);
|
|
1589
|
+
if (Number.isFinite(value)) preview(value);
|
|
1590
|
+
},
|
|
1591
|
+
onPointerUp: commit,
|
|
1592
|
+
onKeyUp: commit,
|
|
1593
|
+
onBlur: commit
|
|
1594
|
+
}),
|
|
1595
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1596
|
+
className: "dsh-model-menu-sliderTrack",
|
|
1597
|
+
"aria-hidden": "true",
|
|
1598
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1599
|
+
className: "dsh-model-menu-sliderFill",
|
|
1600
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
|
|
1601
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1602
|
+
className: "dsh-model-menu-sliderNodes",
|
|
1603
|
+
children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1604
|
+
"data-covered": index <= position ? "" : void 0,
|
|
1605
|
+
className: "dsh-model-menu-sliderNode",
|
|
1606
|
+
style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
|
|
1607
|
+
}, row.id || "__default"))
|
|
1608
|
+
})]
|
|
1609
|
+
}),
|
|
1610
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1611
|
+
className: "dsh-model-menu-sliderThumb",
|
|
1612
|
+
"aria-hidden": "true"
|
|
1613
|
+
})
|
|
1614
|
+
]
|
|
1615
|
+
})]
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
/**
|
|
1452
1619
|
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
1453
1620
|
* published dsh-client-ui-model-selection bundle): card capped at
|
|
1454
1621
|
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
@@ -1507,6 +1674,7 @@ const modelMenuCss = `
|
|
|
1507
1674
|
overflow: hidden;
|
|
1508
1675
|
}
|
|
1509
1676
|
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
1677
|
+
.dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
|
|
1510
1678
|
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
1511
1679
|
.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; }
|
|
1512
1680
|
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
@@ -1585,6 +1753,145 @@ const modelMenuCss = `
|
|
|
1585
1753
|
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
1586
1754
|
.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; }
|
|
1587
1755
|
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
1756
|
+
/* Discrete reasoning-effort slider under the model list (enhanced mode).
|
|
1757
|
+
* The painted control is layered over a fully functional native range input:
|
|
1758
|
+
* the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
|
|
1759
|
+
* the decorative track, fill and thumb render beneath it at pointer-events:
|
|
1760
|
+
* none. Position lives in one custom property (--_p, 0–100) on the rail, so
|
|
1761
|
+
* the only animated work a level change triggers is a clip-path/left
|
|
1762
|
+
* transition — no keyframes, no element churn. */
|
|
1763
|
+
.dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
|
|
1764
|
+
.dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
|
|
1765
|
+
.dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
1766
|
+
.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); }
|
|
1767
|
+
.dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
|
|
1768
|
+
.dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
|
|
1769
|
+
.dsh-model-menu-sliderInput {
|
|
1770
|
+
appearance: none;
|
|
1771
|
+
-webkit-appearance: none;
|
|
1772
|
+
box-sizing: border-box;
|
|
1773
|
+
border: 0;
|
|
1774
|
+
cursor: pointer;
|
|
1775
|
+
direction: ltr;
|
|
1776
|
+
opacity: 0;
|
|
1777
|
+
margin: 0;
|
|
1778
|
+
padding: 0;
|
|
1779
|
+
width: 100%;
|
|
1780
|
+
height: 100%;
|
|
1781
|
+
position: absolute;
|
|
1782
|
+
inset: 0;
|
|
1783
|
+
z-index: 1;
|
|
1784
|
+
touch-action: pan-y;
|
|
1785
|
+
}
|
|
1786
|
+
.dsh-model-menu-sliderInput:disabled { cursor: default; }
|
|
1787
|
+
/* The transparent input must keep the painted thumb's geometry: an
|
|
1788
|
+
* appearance:none webkit thumb defaults to 16px, which would shift the
|
|
1789
|
+
* end-point positions 7px off the decorative 30px disc and misplace the
|
|
1790
|
+
* native drag hit target. Track gets the full rail height so the whole band
|
|
1791
|
+
* maps to values identically. */
|
|
1792
|
+
.dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
|
|
1793
|
+
.dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
|
|
1794
|
+
.dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
|
|
1795
|
+
.dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
|
|
1796
|
+
.dsh-model-menu-sliderTrack {
|
|
1797
|
+
pointer-events: none;
|
|
1798
|
+
border-radius: 999px;
|
|
1799
|
+
width: 100%;
|
|
1800
|
+
height: 26px;
|
|
1801
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1802
|
+
box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
|
|
1803
|
+
position: absolute;
|
|
1804
|
+
inset-inline: 0;
|
|
1805
|
+
top: 50%;
|
|
1806
|
+
transform: translateY(-50%);
|
|
1807
|
+
/* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
|
|
1808
|
+
* own clip-path already supplies the rounded reveal shape. */
|
|
1809
|
+
}
|
|
1810
|
+
/* The accent ramp paints the full track width once and is revealed by an
|
|
1811
|
+
* animated clip-path, so raising the effort unwraps more of the spectrum
|
|
1812
|
+
* instead of squashing the same gradient into a narrower bar. The glow lives
|
|
1813
|
+
* on the un-clipped parent: a filter on the clipped element itself would be
|
|
1814
|
+
* clipped away with it. */
|
|
1815
|
+
.dsh-model-menu-sliderFill {
|
|
1816
|
+
display: block;
|
|
1817
|
+
height: 100%;
|
|
1818
|
+
filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
|
|
1819
|
+
transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1820
|
+
}
|
|
1821
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
|
|
1822
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
|
|
1823
|
+
filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
|
|
1824
|
+
}
|
|
1825
|
+
/* The fill stops at the thumb's center, not at the track edge — at a 30px
|
|
1826
|
+
* thumb the end stops sit 15px in from each side, matching the node dots. */
|
|
1827
|
+
.dsh-model-menu-sliderFillBar {
|
|
1828
|
+
display: block;
|
|
1829
|
+
height: 100%;
|
|
1830
|
+
background: linear-gradient(90deg,
|
|
1831
|
+
var(--dsw-alias-state-business-primary),
|
|
1832
|
+
var(--dsw-alias-state-success-primary),
|
|
1833
|
+
var(--dsw-alias-state-warn-primary),
|
|
1834
|
+
var(--dsw-alias-state-error-primary));
|
|
1835
|
+
clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
|
|
1836
|
+
transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1837
|
+
}
|
|
1838
|
+
/* Stop dots: one per declared row (default + every advertised effort), evenly
|
|
1839
|
+
* spread across the band between the two thumb-center extremes (inset 15px,
|
|
1840
|
+
* i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
|
|
1841
|
+
* reads as a pale pin through the gradient; an uncovered one stays a neutral
|
|
1842
|
+
* notch on the empty track. */
|
|
1843
|
+
.dsh-model-menu-sliderNodes {
|
|
1844
|
+
pointer-events: none;
|
|
1845
|
+
display: block;
|
|
1846
|
+
position: absolute;
|
|
1847
|
+
inset-inline: 15px;
|
|
1848
|
+
inset-block: 0;
|
|
1849
|
+
}
|
|
1850
|
+
.dsh-model-menu-sliderNode {
|
|
1851
|
+
pointer-events: none;
|
|
1852
|
+
box-sizing: border-box;
|
|
1853
|
+
width: 4px;
|
|
1854
|
+
height: 4px;
|
|
1855
|
+
border-radius: 50%;
|
|
1856
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
|
|
1857
|
+
position: absolute;
|
|
1858
|
+
left: calc(var(--_n, 0) * 1%);
|
|
1859
|
+
top: 50%;
|
|
1860
|
+
transform: translate(-50%, -50%);
|
|
1861
|
+
transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1862
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1863
|
+
}
|
|
1864
|
+
.dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
|
|
1865
|
+
.dsh-model-menu-sliderThumb {
|
|
1866
|
+
pointer-events: none;
|
|
1867
|
+
box-sizing: border-box;
|
|
1868
|
+
border-radius: 50%;
|
|
1869
|
+
width: 30px;
|
|
1870
|
+
height: 30px;
|
|
1871
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
1872
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
1873
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
|
|
1874
|
+
position: absolute;
|
|
1875
|
+
left: calc((100% - 30px) * var(--_p, 0) / 100);
|
|
1876
|
+
top: 50%;
|
|
1877
|
+
transform: translateY(-50%) scale(0.92);
|
|
1878
|
+
transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1879
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
1880
|
+
box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
1881
|
+
}
|
|
1882
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
|
|
1883
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
|
|
1884
|
+
transform: translateY(-50%) scale(1.1);
|
|
1885
|
+
box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
|
|
1886
|
+
}
|
|
1887
|
+
.dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
|
|
1888
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
|
|
1889
|
+
0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
1890
|
+
}
|
|
1891
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1892
|
+
.dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
|
|
1893
|
+
.dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
|
|
1894
|
+
}
|
|
1588
1895
|
`;
|
|
1589
1896
|
//#endregion
|
|
1590
1897
|
//#region src/client-copy.ts
|
|
@@ -1670,6 +1977,95 @@ function hubMessage(message, language) {
|
|
|
1670
1977
|
return hubMessages.find((pair) => pair.includes(message))?.[language === "zh" ? 0 : 1] ?? message;
|
|
1671
1978
|
}
|
|
1672
1979
|
//#endregion
|
|
1980
|
+
//#region ../dsh-plugin-kit/src/model-picker.tsx
|
|
1981
|
+
/**
|
|
1982
|
+
* Optional shared model picker (`klarkxy.model-picker` Factory).
|
|
1983
|
+
*
|
|
1984
|
+
* `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
|
|
1985
|
+
* Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
|
|
1986
|
+
* a searchable provider-grouped list and a reasoning-effort slider, built on
|
|
1987
|
+
* the same ModelMenu recipe. Consumers keep their own ModelMenu as the
|
|
1988
|
+
* `fallback`; when no definition is registered the renderer resolves that
|
|
1989
|
+
* fallback verbatim, so the shared picker is strictly opt-in and every saved
|
|
1990
|
+
* value keeps its existing write path.
|
|
1991
|
+
*
|
|
1992
|
+
* `renderFactorySlot` is a framework-composed prop, never a context: the
|
|
1993
|
+
* component a plugin registers into any slot receives it automatically, but
|
|
1994
|
+
* the plugin must pass it down to whichever child renders the menu (or to the
|
|
1995
|
+
* render callback handed to registerSessionSettingsItem).
|
|
1996
|
+
*
|
|
1997
|
+
* The Factory manages its own open state; a consumer's `open`/`onOpenChange`
|
|
1998
|
+
* pair belongs to the fallback menu only and must not be fed to both.
|
|
1999
|
+
*/
|
|
2000
|
+
/**
|
|
2001
|
+
* The trigger every consumer used to hand its own ModelMenu, rebuilt as the
|
|
2002
|
+
* factory's `anchor` local component: identical classes and chevron, the menu
|
|
2003
|
+
* state coming from the occurrence's props instead of local open state.
|
|
2004
|
+
*
|
|
2005
|
+
* `ariaLabel` and `title` are the whole accessible name and tooltip (the
|
|
2006
|
+
* caller formats "field: current route" itself because the factory's resolved
|
|
2007
|
+
* caption may intentionally be a short name). The component identity stays
|
|
2008
|
+
* put across label changes — a new function would unmount the trigger, and
|
|
2009
|
+
* its cleanup would close a menu that `closeOnPick={false}` is keeping open.
|
|
2010
|
+
* The latest label, title, and `onClose` are read from refs.
|
|
2011
|
+
*/
|
|
2012
|
+
function useModelMenuAnchor(ariaLabel, title, onClose) {
|
|
2013
|
+
const closeRef = (0, react.useRef)(onClose);
|
|
2014
|
+
const labelRef = (0, react.useRef)(ariaLabel);
|
|
2015
|
+
const titleRef = (0, react.useRef)(title);
|
|
2016
|
+
closeRef.current = onClose;
|
|
2017
|
+
labelRef.current = ariaLabel;
|
|
2018
|
+
titleRef.current = title;
|
|
2019
|
+
return (0, react.useMemo)(() => {
|
|
2020
|
+
const ModelPickerTrigger = (props) => {
|
|
2021
|
+
(0, react.useEffect)(() => () => closeRef.current?.(), []);
|
|
2022
|
+
const record = props;
|
|
2023
|
+
const injected = {};
|
|
2024
|
+
for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
|
|
2025
|
+
const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
|
|
2026
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2027
|
+
type: "button",
|
|
2028
|
+
className: `dsh-model-menu-trigger${extraClass}`,
|
|
2029
|
+
disabled: props.disabled,
|
|
2030
|
+
"aria-haspopup": "menu",
|
|
2031
|
+
"aria-expanded": props.open,
|
|
2032
|
+
...injected,
|
|
2033
|
+
"aria-label": labelRef.current,
|
|
2034
|
+
title: titleRef.current,
|
|
2035
|
+
children: [
|
|
2036
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2037
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2038
|
+
children: props.label
|
|
2039
|
+
}),
|
|
2040
|
+
props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2041
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2042
|
+
children: props.effortLabel
|
|
2043
|
+
}) : null,
|
|
2044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2045
|
+
]
|
|
2046
|
+
});
|
|
2047
|
+
};
|
|
2048
|
+
return ModelPickerTrigger;
|
|
2049
|
+
}, []);
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Render the shared model picker while it is registered; `options.fallback`
|
|
2053
|
+
* whenever it is not (plugin absent, disabled, or unloaded).
|
|
2054
|
+
*
|
|
2055
|
+
* `options.anchor` is optional: the factory ships a default trigger, so only
|
|
2056
|
+
* consumers that need their own chrome (custom aria-label, tooltip, class)
|
|
2057
|
+
* pass one built with {@link useModelMenuAnchor}. The returned node is never
|
|
2058
|
+
* assumed to be the factory's own output — a defensive `?? fallback` covers a
|
|
2059
|
+
* renderer that yields null for an unregistered definition.
|
|
2060
|
+
*/
|
|
2061
|
+
function renderModelPicker(renderFactorySlot, props, options) {
|
|
2062
|
+
if (typeof renderFactorySlot !== "function") return options.fallback;
|
|
2063
|
+
return renderFactorySlot("klarkxy.model-picker", props, {
|
|
2064
|
+
...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
|
|
2065
|
+
fallback: options.fallback
|
|
2066
|
+
}) ?? options.fallback;
|
|
2067
|
+
}
|
|
2068
|
+
//#endregion
|
|
1673
2069
|
//#region src/client-model-menu.tsx
|
|
1674
2070
|
/**
|
|
1675
2071
|
* One detected field's standard editor: the same ModelMenu every marked field gets.
|
|
@@ -1680,7 +2076,7 @@ function hubMessage(message, language) {
|
|
|
1680
2076
|
* the current reasoning effort when the new model advertises it, so switching a
|
|
1681
2077
|
* route does not silently reset the user's choice.
|
|
1682
2078
|
*/
|
|
1683
|
-
function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet }) {
|
|
2079
|
+
function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, followLabel, reasoningEffort = true, display = "full", onSet, renderFactorySlot }) {
|
|
1684
2080
|
const [open, setOpen] = (0, react.useState)(false);
|
|
1685
2081
|
(0, react.useEffect)(() => {
|
|
1686
2082
|
if (disabled) setOpen(false);
|
|
@@ -1701,46 +2097,28 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
|
|
|
1701
2097
|
const currentLabel = display === "short" && !follow ? selected ? modelMenuShortName(selected) : route.model : menuLabel;
|
|
1702
2098
|
const selectedEffort = efforts.find((item) => item.id === (route.reasoningEffort || ""));
|
|
1703
2099
|
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, t.language) : void 0;
|
|
1704
|
-
|
|
2100
|
+
const anchor = useModelMenuAnchor(accessible, routeLabel, () => setOpen(false));
|
|
2101
|
+
const pick = (picked) => {
|
|
2102
|
+
const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
|
|
2103
|
+
const effort = route.reasoningEffort;
|
|
2104
|
+
const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
|
|
2105
|
+
onSet({
|
|
2106
|
+
provider: picked.provider,
|
|
2107
|
+
model: picked.model,
|
|
2108
|
+
...supported ? { reasoningEffort: effort } : {}
|
|
2109
|
+
});
|
|
2110
|
+
};
|
|
2111
|
+
return renderModelPicker(renderFactorySlot, {
|
|
1705
2112
|
locale: t.language,
|
|
1706
|
-
|
|
2113
|
+
disabled,
|
|
1707
2114
|
onOpenChange: (next) => setOpen(next && !disabled),
|
|
1708
2115
|
listClassName: "dsh-model-hub",
|
|
1709
|
-
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1710
|
-
type: "button",
|
|
1711
|
-
className: "dsh-model-menu-trigger",
|
|
1712
|
-
disabled,
|
|
1713
|
-
"aria-label": accessible,
|
|
1714
|
-
title: routeLabel,
|
|
1715
|
-
"aria-haspopup": "menu",
|
|
1716
|
-
"aria-expanded": open && !disabled,
|
|
1717
|
-
children: [
|
|
1718
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1719
|
-
className: "dsh-model-menu-triggerLabel",
|
|
1720
|
-
children: currentLabel
|
|
1721
|
-
}),
|
|
1722
|
-
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1723
|
-
className: "dsh-model-menu-triggerEffort",
|
|
1724
|
-
children: ` · ${effortName ?? t("effortDefault")}`
|
|
1725
|
-
}) : null,
|
|
1726
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
1727
|
-
]
|
|
1728
|
-
}),
|
|
1729
2116
|
choices,
|
|
1730
2117
|
selected: follow ? void 0 : {
|
|
1731
2118
|
provider: route.provider,
|
|
1732
2119
|
model: route.model
|
|
1733
2120
|
},
|
|
1734
|
-
onPick:
|
|
1735
|
-
const choice = choices.find((item) => item.provider === picked.provider && item.model === picked.model);
|
|
1736
|
-
const effort = route.reasoningEffort;
|
|
1737
|
-
const supported = Boolean(effort) && Boolean(choice?.efforts.some((item) => item.id === effort));
|
|
1738
|
-
onSet({
|
|
1739
|
-
provider: picked.provider,
|
|
1740
|
-
model: picked.model,
|
|
1741
|
-
...supported ? { reasoningEffort: effort } : {}
|
|
1742
|
-
});
|
|
1743
|
-
},
|
|
2121
|
+
onPick: pick,
|
|
1744
2122
|
leading: allowFollow ? [{
|
|
1745
2123
|
id: "follow",
|
|
1746
2124
|
label: followLabel || t("follow"),
|
|
@@ -1760,6 +2138,8 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
|
|
|
1760
2138
|
model: route.model,
|
|
1761
2139
|
...effort ? { reasoningEffort: effort } : {}
|
|
1762
2140
|
}),
|
|
2141
|
+
triggerLabel: currentLabel,
|
|
2142
|
+
triggerEffortLabel: !follow && efforts.length ? ` · ${effortName ?? t("effortDefault")}` : void 0,
|
|
1763
2143
|
modelLabel: t("model"),
|
|
1764
2144
|
modelValue: currentLabel,
|
|
1765
2145
|
effortLabel: t("effort"),
|
|
@@ -1767,6 +2147,66 @@ function FieldMenu({ t, catalog, current, disabled, label, allowFollow = true, f
|
|
|
1767
2147
|
searchPlaceholder: t("searchModels"),
|
|
1768
2148
|
emptyLabel: t("noModels"),
|
|
1769
2149
|
clearSearchLabel: t("clearSearch")
|
|
2150
|
+
}, {
|
|
2151
|
+
anchor,
|
|
2152
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
2153
|
+
locale: t.language,
|
|
2154
|
+
open: open && !disabled,
|
|
2155
|
+
onOpenChange: (next) => setOpen(next && !disabled),
|
|
2156
|
+
listClassName: "dsh-model-hub",
|
|
2157
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2158
|
+
type: "button",
|
|
2159
|
+
className: "dsh-model-menu-trigger",
|
|
2160
|
+
disabled,
|
|
2161
|
+
"aria-label": accessible,
|
|
2162
|
+
title: routeLabel,
|
|
2163
|
+
"aria-haspopup": "menu",
|
|
2164
|
+
"aria-expanded": open && !disabled,
|
|
2165
|
+
children: [
|
|
2166
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2167
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2168
|
+
children: currentLabel
|
|
2169
|
+
}),
|
|
2170
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2171
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2172
|
+
children: ` · ${effortName ?? t("effortDefault")}`
|
|
2173
|
+
}) : null,
|
|
2174
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2175
|
+
]
|
|
2176
|
+
}),
|
|
2177
|
+
choices,
|
|
2178
|
+
selected: follow ? void 0 : {
|
|
2179
|
+
provider: route.provider,
|
|
2180
|
+
model: route.model
|
|
2181
|
+
},
|
|
2182
|
+
onPick: pick,
|
|
2183
|
+
leading: allowFollow ? [{
|
|
2184
|
+
id: "follow",
|
|
2185
|
+
label: followLabel || t("follow"),
|
|
2186
|
+
selected: follow
|
|
2187
|
+
}] : [],
|
|
2188
|
+
onPickLeading: () => onSet({
|
|
2189
|
+
provider: "",
|
|
2190
|
+
model: ""
|
|
2191
|
+
}),
|
|
2192
|
+
efforts,
|
|
2193
|
+
selectedEffort: route.reasoningEffort || "",
|
|
2194
|
+
onPickEffort: (effort) => onSet(follow ? {
|
|
2195
|
+
provider: "",
|
|
2196
|
+
model: ""
|
|
2197
|
+
} : {
|
|
2198
|
+
provider: route.provider,
|
|
2199
|
+
model: route.model,
|
|
2200
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
2201
|
+
}),
|
|
2202
|
+
modelLabel: t("model"),
|
|
2203
|
+
modelValue: currentLabel,
|
|
2204
|
+
effortLabel: t("effort"),
|
|
2205
|
+
defaultEffortLabel: t("effortDefault"),
|
|
2206
|
+
searchPlaceholder: t("searchModels"),
|
|
2207
|
+
emptyLabel: t("noModels"),
|
|
2208
|
+
clearSearchLabel: t("clearSearch")
|
|
2209
|
+
})
|
|
1770
2210
|
});
|
|
1771
2211
|
}
|
|
1772
2212
|
function messageOf(error) {
|
|
@@ -1902,7 +2342,7 @@ function routeInput(route) {
|
|
|
1902
2342
|
};
|
|
1903
2343
|
}
|
|
1904
2344
|
/** One cast entry: model picker, tendency input saved on blur, remove button. */
|
|
1905
|
-
function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, onSetRoute, onSetTendency, onRemove }) {
|
|
2345
|
+
function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, tendencyDisabled, menuDisabled, renderFactorySlot, onSetRoute, onSetTendency, onRemove }) {
|
|
1906
2346
|
const label = `${route.provider} / ${route.model}`;
|
|
1907
2347
|
const saved = route.tendency ?? "";
|
|
1908
2348
|
const [draft, setDraft] = (0, react.useState)({
|
|
@@ -1945,6 +2385,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
|
|
|
1945
2385
|
reasoningEffort: true,
|
|
1946
2386
|
display: "short",
|
|
1947
2387
|
disabled: menuDisabled,
|
|
2388
|
+
renderFactorySlot,
|
|
1948
2389
|
onSet: onSetRoute
|
|
1949
2390
|
})
|
|
1950
2391
|
}),
|
|
@@ -1981,7 +2422,7 @@ function CastRow({ t, catalog, route, tendencyRevision, draftReview, disabled, t
|
|
|
1981
2422
|
]
|
|
1982
2423
|
});
|
|
1983
2424
|
}
|
|
1984
|
-
function CastPage({ api, locale }) {
|
|
2425
|
+
function CastPage({ api, locale, renderFactorySlot }) {
|
|
1985
2426
|
const t = useText$1(locale);
|
|
1986
2427
|
const [cast, setCast] = (0, react.useState)(null);
|
|
1987
2428
|
const [catalog, setCatalog] = (0, react.useState)(null);
|
|
@@ -2217,6 +2658,7 @@ function CastPage({ api, locale }) {
|
|
|
2217
2658
|
t,
|
|
2218
2659
|
catalog,
|
|
2219
2660
|
route,
|
|
2661
|
+
renderFactorySlot,
|
|
2220
2662
|
tendencyRevision: cast.tendencyRevision,
|
|
2221
2663
|
draftReview,
|
|
2222
2664
|
tendencyDisabled: disabled || !!cast.tendencyError || cast.tendencyRevision === void 0,
|
|
@@ -2247,6 +2689,7 @@ function CastPage({ api, locale }) {
|
|
|
2247
2689
|
reasoningEffort: true,
|
|
2248
2690
|
display: "short",
|
|
2249
2691
|
disabled: disabled || catalog === null,
|
|
2692
|
+
renderFactorySlot,
|
|
2250
2693
|
onSet: (picked) => {
|
|
2251
2694
|
if (!picked.provider || !picked.model) {
|
|
2252
2695
|
setPending(void 0);
|
|
@@ -3146,13 +3589,14 @@ function localeActive(locale) {
|
|
|
3146
3589
|
return String(typeof locale === "string" ? locale : locale?.getSnapshot?.()?.active ?? "en");
|
|
3147
3590
|
}
|
|
3148
3591
|
const css = `${officialUiCss("dsh-model-hub")}${modelMenuCss}`;
|
|
3149
|
-
function SubagentModelsPage({ api, locale }) {
|
|
3592
|
+
function SubagentModelsPage({ api, locale, renderFactorySlot }) {
|
|
3150
3593
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
3151
3594
|
className: "dsh-model-hub dsh-ui-page",
|
|
3152
3595
|
"aria-label": localeActive(locale).toLowerCase().startsWith("zh") ? "子代理" : "Subagents",
|
|
3153
3596
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CastPage, {
|
|
3154
3597
|
api,
|
|
3155
|
-
locale
|
|
3598
|
+
locale,
|
|
3599
|
+
renderFactorySlot
|
|
3156
3600
|
})]
|
|
3157
3601
|
});
|
|
3158
3602
|
}
|