@klarkxy/dsh-current-title 0.3.3 → 0.3.4
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 +2 -0
- package/docs/README.zh-CN.md +2 -0
- package/lib/client.inner.cjs +567 -88
- package/lib/client.inner.cjs.map +1 -1
- package/lib/client.js +567 -88
- package/lib/index.js +3 -2
- package/lib/index.js.map +1 -1
- package/package.json +10 -9
package/lib/client.js
CHANGED
|
@@ -102,6 +102,7 @@ function parseModelMenuChoices(value, bound) {
|
|
|
102
102
|
provider,
|
|
103
103
|
model,
|
|
104
104
|
label: `${providerName} / ${name}`,
|
|
105
|
+
providerLabel: providerName,
|
|
105
106
|
efforts
|
|
106
107
|
});
|
|
107
108
|
}
|
|
@@ -148,11 +149,11 @@ function modelMenuShortName(choice) {
|
|
|
148
149
|
/** Group catalog choices by provider and filter them by a case-insensitive query. */
|
|
149
150
|
function groupModelMenuChoices(choices, query) {
|
|
150
151
|
const needle = query.trim().toLowerCase();
|
|
151
|
-
const matched = needle ? choices.filter((choice) => choice.label.toLowerCase().includes(needle) || choice.model.toLowerCase().includes(needle) || choice.provider.toLowerCase().includes(needle)) : choices;
|
|
152
|
+
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;
|
|
152
153
|
const groups = [];
|
|
153
154
|
for (const choice of matched) {
|
|
154
155
|
const sep = choice.label.indexOf(" / ");
|
|
155
|
-
const name = sep > 0 ? choice.label.slice(0, sep) : choice.provider;
|
|
156
|
+
const name = choice.providerLabel ?? (sep > 0 ? choice.label.slice(0, sep) : choice.provider);
|
|
156
157
|
const last = groups.at(-1);
|
|
157
158
|
if (last && last.provider === choice.provider) last.items.push(choice);
|
|
158
159
|
else groups.push({
|
|
@@ -525,7 +526,9 @@ Object.values({
|
|
|
525
526
|
"--dsh-scrollbar-thumb-hover",
|
|
526
527
|
"--dsh-content-font-size-secondary",
|
|
527
528
|
"--dsh-content-font-delta",
|
|
528
|
-
"--dsh-frame-top-clearance"
|
|
529
|
+
"--dsh-frame-top-clearance",
|
|
530
|
+
"--dsh-composer-model-icon-display",
|
|
531
|
+
"--dsh-composer-model-text-display"
|
|
529
532
|
]
|
|
530
533
|
}).flat();
|
|
531
534
|
/**
|
|
@@ -1229,7 +1232,7 @@ function ModelMenu(props) {
|
|
|
1229
1232
|
empty: "No matching models",
|
|
1230
1233
|
clear: "Clear search"
|
|
1231
1234
|
};
|
|
1232
|
-
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;
|
|
1235
|
+
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;
|
|
1233
1236
|
const rootRef = (0, react.useRef)(null);
|
|
1234
1237
|
const openRef = (0, react.useRef)(open);
|
|
1235
1238
|
openRef.current = open;
|
|
@@ -1244,9 +1247,10 @@ function ModelMenu(props) {
|
|
|
1244
1247
|
const [pane, setPane] = (0, react.useState)("root");
|
|
1245
1248
|
const [query, setQuery] = (0, react.useState)("");
|
|
1246
1249
|
const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(null);
|
|
1250
|
+
const [keyNav, setKeyNav] = (0, react.useState)(0);
|
|
1247
1251
|
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
1248
1252
|
const id = (0, react.useId)();
|
|
1249
|
-
const showSearch = choices.length > 4;
|
|
1253
|
+
const showSearch = alwaysShowSearch || choices.length > 4;
|
|
1250
1254
|
const effortRows = (0, react.useMemo)(() => [{
|
|
1251
1255
|
id: "",
|
|
1252
1256
|
name: defaultEffortLabel
|
|
@@ -1263,7 +1267,12 @@ function ModelMenu(props) {
|
|
|
1263
1267
|
items: (0, _deepseek_ai_dsh_client_ui_primitives.rankByName)(group.items.map((choice) => ({
|
|
1264
1268
|
choice,
|
|
1265
1269
|
name: modelMenuShortName(choice),
|
|
1266
|
-
label:
|
|
1270
|
+
label: [
|
|
1271
|
+
choice.provider,
|
|
1272
|
+
choice.providerLabel,
|
|
1273
|
+
choice.model,
|
|
1274
|
+
choice.label
|
|
1275
|
+
].filter(Boolean).join(" ")
|
|
1267
1276
|
})), showSearch ? query.trim() : "")
|
|
1268
1277
|
})).filter((group) => group.items.length > 0), [
|
|
1269
1278
|
choices,
|
|
@@ -1295,10 +1304,12 @@ function ModelMenu(props) {
|
|
|
1295
1304
|
paneFocus.current = "drill";
|
|
1296
1305
|
setPane(next);
|
|
1297
1306
|
};
|
|
1298
|
-
/** Leave a drilled pane for the
|
|
1307
|
+
/** Leave a drilled pane for the entry pane, handing the keyboard back to it.
|
|
1308
|
+
* A root entry refocuses the cell that opened the pane; a search-first
|
|
1309
|
+
* (model) entry refocuses that pane's own intent. */
|
|
1299
1310
|
const back = (from) => {
|
|
1300
|
-
paneFocus.current = from;
|
|
1301
|
-
setPane(
|
|
1311
|
+
paneFocus.current = initialPane === "root" ? from : "drill";
|
|
1312
|
+
setPane(initialPane);
|
|
1302
1313
|
};
|
|
1303
1314
|
const moveFocus = (offset) => {
|
|
1304
1315
|
const items = itemRefs.current.filter((item) => item !== null && !item.disabled);
|
|
@@ -1338,10 +1349,18 @@ function ModelMenu(props) {
|
|
|
1338
1349
|
}, [requestedOpen, open]);
|
|
1339
1350
|
(0, react.useEffect)(() => {
|
|
1340
1351
|
if (!open) return;
|
|
1341
|
-
setPane(
|
|
1352
|
+
setPane(initialPane);
|
|
1353
|
+
if (initialPane === "model") paneFocus.current = "drill";
|
|
1342
1354
|
setQuery("");
|
|
1343
1355
|
setHighlightedIndex(null);
|
|
1344
|
-
}, [open]);
|
|
1356
|
+
}, [open, initialPane]);
|
|
1357
|
+
(0, react.useEffect)(() => {
|
|
1358
|
+
if (disabled && open && closeOnDisable) changeOpen(false);
|
|
1359
|
+
}, [
|
|
1360
|
+
disabled,
|
|
1361
|
+
open,
|
|
1362
|
+
closeOnDisable
|
|
1363
|
+
]);
|
|
1345
1364
|
(0, react.useEffect)(() => {
|
|
1346
1365
|
if (!open) return;
|
|
1347
1366
|
const closeOutside = (event) => {
|
|
@@ -1349,9 +1368,25 @@ function ModelMenu(props) {
|
|
|
1349
1368
|
if (rootRef.current?.contains(target) || menuRef.current?.contains(target)) return;
|
|
1350
1369
|
changeOpen(false);
|
|
1351
1370
|
};
|
|
1371
|
+
const escapeOutside = (event) => {
|
|
1372
|
+
if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
|
|
1373
|
+
if (!openRef.current) return;
|
|
1374
|
+
event.preventDefault();
|
|
1375
|
+
if (pane !== initialPane) back(pane);
|
|
1376
|
+
else close(true);
|
|
1377
|
+
};
|
|
1352
1378
|
document.addEventListener("mousedown", closeOutside);
|
|
1353
|
-
|
|
1354
|
-
|
|
1379
|
+
document.addEventListener("keydown", escapeOutside);
|
|
1380
|
+
return () => {
|
|
1381
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
1382
|
+
document.removeEventListener("keydown", escapeOutside);
|
|
1383
|
+
};
|
|
1384
|
+
}, [
|
|
1385
|
+
open,
|
|
1386
|
+
onOpenChange,
|
|
1387
|
+
pane,
|
|
1388
|
+
initialPane
|
|
1389
|
+
]);
|
|
1355
1390
|
(0, react.useEffect)(() => {
|
|
1356
1391
|
const intent = paneFocus.current;
|
|
1357
1392
|
if (!open || menuPos === null || intent === null) return;
|
|
@@ -1381,13 +1416,21 @@ function ModelMenu(props) {
|
|
|
1381
1416
|
pane,
|
|
1382
1417
|
filteredGroups
|
|
1383
1418
|
]);
|
|
1419
|
+
const modelPaneOpen = open && pane === "model";
|
|
1420
|
+
const scrollSignal = !modelPaneOpen ? "closed" : keyNav === 0 ? "open" : `key:${keyNav}`;
|
|
1421
|
+
const scrollSignalRef = (0, react.useRef)(null);
|
|
1384
1422
|
(0, react.useLayoutEffect)(() => {
|
|
1385
|
-
if (
|
|
1423
|
+
if (!modelPaneOpen || activeModelIndex < 0) {
|
|
1424
|
+
if (!modelPaneOpen) scrollSignalRef.current = null;
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
if (scrollSignalRef.current === scrollSignal) return;
|
|
1428
|
+
scrollSignalRef.current = scrollSignal;
|
|
1429
|
+
itemRefs.current[leading.length + activeModelIndex]?.scrollIntoView({ block: "nearest" });
|
|
1386
1430
|
}, [
|
|
1387
|
-
|
|
1388
|
-
|
|
1431
|
+
modelPaneOpen,
|
|
1432
|
+
scrollSignal,
|
|
1389
1433
|
activeModelIndex,
|
|
1390
|
-
visibleModels,
|
|
1391
1434
|
leading.length
|
|
1392
1435
|
]);
|
|
1393
1436
|
(0, react.useLayoutEffect)(() => {
|
|
@@ -1402,7 +1445,7 @@ function ModelMenu(props) {
|
|
|
1402
1445
|
let x = rect.right - lw, y = rect.top - 8 - lh;
|
|
1403
1446
|
if (lw > 0) x = Math.min(Math.max(x, margin), window.innerWidth - lw - margin);
|
|
1404
1447
|
if (lh > 0) y = Math.min(Math.max(y, margin), window.innerHeight - lh - margin);
|
|
1405
|
-
setMenuPos({
|
|
1448
|
+
setMenuPos((prev) => prev && prev.left === x && prev.top === y ? prev : {
|
|
1406
1449
|
left: x,
|
|
1407
1450
|
top: y
|
|
1408
1451
|
});
|
|
@@ -1422,6 +1465,13 @@ function ModelMenu(props) {
|
|
|
1422
1465
|
]);
|
|
1423
1466
|
function onKeyDown(event) {
|
|
1424
1467
|
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
|
|
1468
|
+
if (event.key === "Escape" && open) {
|
|
1469
|
+
event.preventDefault();
|
|
1470
|
+
if (pane !== initialPane) back(pane);
|
|
1471
|
+
else close(true);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (disabled) return;
|
|
1425
1475
|
if (!open && [
|
|
1426
1476
|
"Enter",
|
|
1427
1477
|
" ",
|
|
@@ -1435,17 +1485,12 @@ function ModelMenu(props) {
|
|
|
1435
1485
|
requestOpen();
|
|
1436
1486
|
return;
|
|
1437
1487
|
}
|
|
1438
|
-
if (event.key === "Escape" && open) {
|
|
1439
|
-
event.preventDefault();
|
|
1440
|
-
if (pane !== "root") back(pane);
|
|
1441
|
-
else close(true);
|
|
1442
|
-
return;
|
|
1443
|
-
}
|
|
1444
1488
|
if (!open) return;
|
|
1445
1489
|
if (pane === "model" && showSearch && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
1446
1490
|
event.preventDefault();
|
|
1447
1491
|
if (visibleModels.length > 0) {
|
|
1448
1492
|
setHighlightedIndex((activeModelIndex + (event.key === "ArrowDown" ? 1 : -1) + visibleModels.length) % visibleModels.length);
|
|
1493
|
+
setKeyNav((n) => n + 1);
|
|
1449
1494
|
searchRef.current?.focus();
|
|
1450
1495
|
}
|
|
1451
1496
|
return;
|
|
@@ -1454,19 +1499,19 @@ function ModelMenu(props) {
|
|
|
1454
1499
|
if (event.key === "Tab" && visibleModels.length === 0) return;
|
|
1455
1500
|
event.preventDefault();
|
|
1456
1501
|
const highlighted = visibleModels[activeModelIndex];
|
|
1457
|
-
if (highlighted) {
|
|
1502
|
+
if (highlighted && !busy) {
|
|
1458
1503
|
onPick({
|
|
1459
1504
|
provider: highlighted.provider,
|
|
1460
1505
|
model: highlighted.model
|
|
1461
1506
|
});
|
|
1462
|
-
close(true);
|
|
1507
|
+
if (closeOnPick) close(true);
|
|
1463
1508
|
}
|
|
1464
1509
|
return;
|
|
1465
1510
|
}
|
|
1466
1511
|
if (event.key === "Tab") {
|
|
1467
1512
|
if (event.shiftKey) {
|
|
1468
1513
|
event.preventDefault();
|
|
1469
|
-
if (pane !==
|
|
1514
|
+
if (pane !== initialPane) back(pane);
|
|
1470
1515
|
else close(true);
|
|
1471
1516
|
return;
|
|
1472
1517
|
}
|
|
@@ -1490,7 +1535,10 @@ function ModelMenu(props) {
|
|
|
1490
1535
|
}
|
|
1491
1536
|
}
|
|
1492
1537
|
function onBlur(event) {
|
|
1493
|
-
|
|
1538
|
+
const related = event.relatedTarget;
|
|
1539
|
+
if (related instanceof Node && (rootRef.current?.contains(related) || menuRef.current?.contains(related) || !related.isConnected)) return;
|
|
1540
|
+
if (!closeOnPick && !(related instanceof Node)) return;
|
|
1541
|
+
if (busy && related == null) return;
|
|
1494
1542
|
if (open) changeOpen(false);
|
|
1495
1543
|
}
|
|
1496
1544
|
itemRefs.current = [];
|
|
@@ -1511,11 +1559,16 @@ function ModelMenu(props) {
|
|
|
1511
1559
|
"aria-label": modelLabel,
|
|
1512
1560
|
lang: language,
|
|
1513
1561
|
children: [
|
|
1562
|
+
menuHeader ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1563
|
+
className: "dsh-model-menu-header",
|
|
1564
|
+
children: menuHeader
|
|
1565
|
+
}) : null,
|
|
1514
1566
|
pane === "root" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1515
1567
|
ref: itemRef(),
|
|
1516
1568
|
type: "button",
|
|
1517
1569
|
role: "menuitem",
|
|
1518
1570
|
className: "dsh-model-menu-cell",
|
|
1571
|
+
disabled,
|
|
1519
1572
|
onClick: () => drill("model"),
|
|
1520
1573
|
children: [
|
|
1521
1574
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1528,11 +1581,12 @@ function ModelMenu(props) {
|
|
|
1528
1581
|
}),
|
|
1529
1582
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: "dsh-model-menu-cellChevron" })
|
|
1530
1583
|
]
|
|
1531
|
-
}), efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1584
|
+
}), efforts.length > 0 && !reasoningSlider ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1532
1585
|
ref: itemRef(),
|
|
1533
1586
|
type: "button",
|
|
1534
1587
|
role: "menuitem",
|
|
1535
1588
|
className: "dsh-model-menu-cell",
|
|
1589
|
+
disabled,
|
|
1536
1590
|
onClick: () => drill("effort"),
|
|
1537
1591
|
children: [
|
|
1538
1592
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1551,6 +1605,7 @@ function ModelMenu(props) {
|
|
|
1551
1605
|
className: "dsh-model-menu-searchRow",
|
|
1552
1606
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
1553
1607
|
ref: searchRef,
|
|
1608
|
+
disabled,
|
|
1554
1609
|
className: `dsh-model-menu-search${query !== "" ? " dsh-model-menu-searchWithQuery" : ""}`,
|
|
1555
1610
|
type: "text",
|
|
1556
1611
|
role: "searchbox",
|
|
@@ -1567,6 +1622,7 @@ function ModelMenu(props) {
|
|
|
1567
1622
|
type: "button",
|
|
1568
1623
|
className: "dsh-model-menu-searchClear",
|
|
1569
1624
|
"aria-label": clearSearchLabel,
|
|
1625
|
+
disabled,
|
|
1570
1626
|
onClick: () => {
|
|
1571
1627
|
setQuery("");
|
|
1572
1628
|
setHighlightedIndex(null);
|
|
@@ -1589,10 +1645,12 @@ function ModelMenu(props) {
|
|
|
1589
1645
|
type: "button",
|
|
1590
1646
|
role: "menuitemradio",
|
|
1591
1647
|
"aria-checked": item.selected === true,
|
|
1648
|
+
disabled: disabled || busy,
|
|
1592
1649
|
className: `dsh-model-menu-option${item.selected === true ? " dsh-model-menu-selected" : ""}`,
|
|
1593
1650
|
onClick: () => {
|
|
1651
|
+
if (disabled || busy) return;
|
|
1594
1652
|
onPickLeading?.(item.id);
|
|
1595
|
-
close(true);
|
|
1653
|
+
if (closeOnPick) close(true);
|
|
1596
1654
|
},
|
|
1597
1655
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1598
1656
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1618,6 +1676,7 @@ function ModelMenu(props) {
|
|
|
1618
1676
|
"aria-checked": checked,
|
|
1619
1677
|
id: `${id}-model-${index}`,
|
|
1620
1678
|
tabIndex: showSearch ? -1 : 0,
|
|
1679
|
+
disabled: disabled || busy,
|
|
1621
1680
|
onFocus: () => setHighlightedIndex(index),
|
|
1622
1681
|
"data-highlighted": index === activeModelIndex ? "" : void 0,
|
|
1623
1682
|
className: `dsh-model-menu-option dsh-model-menu-modelOption${checked ? " dsh-model-menu-selected" : ""}${index === activeModelIndex ? " dsh-model-menu-optionActive" : ""}`,
|
|
@@ -1627,11 +1686,12 @@ function ModelMenu(props) {
|
|
|
1627
1686
|
},
|
|
1628
1687
|
title: choice.label,
|
|
1629
1688
|
onClick: () => {
|
|
1689
|
+
if (disabled || busy) return;
|
|
1630
1690
|
onPick({
|
|
1631
1691
|
provider: choice.provider,
|
|
1632
1692
|
model: choice.model
|
|
1633
1693
|
});
|
|
1634
|
-
close(true);
|
|
1694
|
+
if (closeOnPick) close(true);
|
|
1635
1695
|
},
|
|
1636
1696
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1637
1697
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1647,6 +1707,14 @@ function ModelMenu(props) {
|
|
|
1647
1707
|
})
|
|
1648
1708
|
}, group.provider))]
|
|
1649
1709
|
}),
|
|
1710
|
+
reasoningSlider && efforts.length > 0 && filteredGroups.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
1711
|
+
label: effortLabel,
|
|
1712
|
+
rows: effortRows,
|
|
1713
|
+
selected: selectedEffort ?? "",
|
|
1714
|
+
disabled,
|
|
1715
|
+
busy,
|
|
1716
|
+
onPick: (value) => onPickEffort?.(value)
|
|
1717
|
+
}) : null,
|
|
1650
1718
|
filteredGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1651
1719
|
className: "dsh-model-menu-empty",
|
|
1652
1720
|
role: "status",
|
|
@@ -1660,10 +1728,12 @@ function ModelMenu(props) {
|
|
|
1660
1728
|
type: "button",
|
|
1661
1729
|
role: "menuitemradio",
|
|
1662
1730
|
"aria-checked": checked,
|
|
1731
|
+
disabled: disabled || busy,
|
|
1663
1732
|
className: `dsh-model-menu-option${checked ? " dsh-model-menu-selected" : ""}`,
|
|
1664
1733
|
onClick: () => {
|
|
1734
|
+
if (disabled || busy) return;
|
|
1665
1735
|
onPickEffort?.(row.id || void 0);
|
|
1666
|
-
close(true);
|
|
1736
|
+
if (closeOnPick) close(true);
|
|
1667
1737
|
},
|
|
1668
1738
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1669
1739
|
className: "dsh-model-menu-optionCopy",
|
|
@@ -1682,7 +1752,8 @@ function ModelMenu(props) {
|
|
|
1682
1752
|
const trigger = (0, react.isValidElement)(anchor) ? (0, react.cloneElement)(anchor, {
|
|
1683
1753
|
"aria-haspopup": "menu",
|
|
1684
1754
|
"aria-expanded": open,
|
|
1685
|
-
"aria-controls": open ? `${id}-menu` : void 0
|
|
1755
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
1756
|
+
"aria-disabled": disabled || void 0
|
|
1686
1757
|
}) : anchor;
|
|
1687
1758
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1688
1759
|
ref: rootRef,
|
|
@@ -1690,7 +1761,7 @@ function ModelMenu(props) {
|
|
|
1690
1761
|
onKeyDown,
|
|
1691
1762
|
onBlur,
|
|
1692
1763
|
onClick: (event) => {
|
|
1693
|
-
if (event.defaultPrevented) return;
|
|
1764
|
+
if (disabled || event.defaultPrevented) return;
|
|
1694
1765
|
const button = event.target instanceof Element ? event.target.closest("button") : null;
|
|
1695
1766
|
if (!(button instanceof HTMLButtonElement) || button.disabled || !rootRef.current?.contains(button)) return;
|
|
1696
1767
|
requestOpen();
|
|
@@ -1702,6 +1773,102 @@ function ModelMenu(props) {
|
|
|
1702
1773
|
});
|
|
1703
1774
|
}
|
|
1704
1775
|
/**
|
|
1776
|
+
* Discrete reasoning-effort slider shown under the model list in enhanced mode.
|
|
1777
|
+
*
|
|
1778
|
+
* A native range input (`step=1`) over exactly the rows the caller supplies —
|
|
1779
|
+
* the default row plus each effort the selected model advertises — so it can
|
|
1780
|
+
* never invent a level. It previews locally while dragging or arrowing and
|
|
1781
|
+
* commits only when the gesture ends (pointer up, key up, or blur), so a drag
|
|
1782
|
+
* does not write on every step. The menu stays open, so adjusting the effort
|
|
1783
|
+
* never loses the model selection or the search query.
|
|
1784
|
+
*
|
|
1785
|
+
* The input itself stays the entire interaction and accessibility surface —
|
|
1786
|
+
* keyboard, ARIA, disabled and the native step snapping all come from it —
|
|
1787
|
+
* while the track, gradient fill and thumb below are decorative layers painted
|
|
1788
|
+
* from the `--_p` percentage the preview position sets on the rail. Value
|
|
1789
|
+
* changes only update that custom property, so the CSS transitions animate
|
|
1790
|
+
* between levels without remounting nodes or restarting an animation.
|
|
1791
|
+
*/
|
|
1792
|
+
function EffortSlider(props) {
|
|
1793
|
+
const { label, rows, selected, disabled = false, busy = false, onPick } = props;
|
|
1794
|
+
const active = Math.max(0, rows.findIndex((row) => row.id === selected));
|
|
1795
|
+
const [position, setPosition] = (0, react.useState)(active);
|
|
1796
|
+
const positionRef = (0, react.useRef)(position);
|
|
1797
|
+
(0, react.useEffect)(() => {
|
|
1798
|
+
positionRef.current = active;
|
|
1799
|
+
setPosition(active);
|
|
1800
|
+
}, [active]);
|
|
1801
|
+
const preview = (next) => {
|
|
1802
|
+
positionRef.current = next;
|
|
1803
|
+
setPosition(next);
|
|
1804
|
+
};
|
|
1805
|
+
const commit = () => {
|
|
1806
|
+
if (disabled || busy) return;
|
|
1807
|
+
const row = rows[positionRef.current];
|
|
1808
|
+
if (row !== void 0) onPick(row.id === "" ? void 0 : row.id);
|
|
1809
|
+
};
|
|
1810
|
+
const max = Math.max(0, rows.length - 1);
|
|
1811
|
+
const percent = max > 0 ? Math.round(position / max * 100) : 0;
|
|
1812
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1813
|
+
className: "dsh-model-menu-sliderGroup",
|
|
1814
|
+
role: "group",
|
|
1815
|
+
"aria-label": label,
|
|
1816
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1817
|
+
className: "dsh-model-menu-sliderHead",
|
|
1818
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1819
|
+
className: "dsh-model-menu-sliderTitle",
|
|
1820
|
+
children: label
|
|
1821
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1822
|
+
className: "dsh-model-menu-sliderValue",
|
|
1823
|
+
children: rows[position]?.name
|
|
1824
|
+
})]
|
|
1825
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1826
|
+
className: "dsh-model-menu-sliderRail",
|
|
1827
|
+
style: { "--_p": percent },
|
|
1828
|
+
children: [
|
|
1829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1830
|
+
type: "range",
|
|
1831
|
+
className: "dsh-model-menu-sliderInput",
|
|
1832
|
+
min: 0,
|
|
1833
|
+
max,
|
|
1834
|
+
step: 1,
|
|
1835
|
+
value: position,
|
|
1836
|
+
disabled,
|
|
1837
|
+
"aria-label": label,
|
|
1838
|
+
"aria-disabled": disabled || busy || void 0,
|
|
1839
|
+
"aria-valuetext": rows[position]?.name,
|
|
1840
|
+
onChange: (event) => {
|
|
1841
|
+
const value = Number(event.target.value);
|
|
1842
|
+
if (Number.isFinite(value)) preview(value);
|
|
1843
|
+
},
|
|
1844
|
+
onPointerUp: commit,
|
|
1845
|
+
onKeyUp: commit,
|
|
1846
|
+
onBlur: commit
|
|
1847
|
+
}),
|
|
1848
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1849
|
+
className: "dsh-model-menu-sliderTrack",
|
|
1850
|
+
"aria-hidden": "true",
|
|
1851
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1852
|
+
className: "dsh-model-menu-sliderFill",
|
|
1853
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dsh-model-menu-sliderFillBar" })
|
|
1854
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1855
|
+
className: "dsh-model-menu-sliderNodes",
|
|
1856
|
+
children: rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1857
|
+
"data-covered": index <= position ? "" : void 0,
|
|
1858
|
+
className: "dsh-model-menu-sliderNode",
|
|
1859
|
+
style: { "--_n": max > 0 ? Math.round(index / max * 100) : 0 }
|
|
1860
|
+
}, row.id || "__default"))
|
|
1861
|
+
})]
|
|
1862
|
+
}),
|
|
1863
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1864
|
+
className: "dsh-model-menu-sliderThumb",
|
|
1865
|
+
"aria-hidden": "true"
|
|
1866
|
+
})
|
|
1867
|
+
]
|
|
1868
|
+
})]
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
/**
|
|
1705
1872
|
* ModelMenu styles, transcribed from the official ModelSelect.module.css (the
|
|
1706
1873
|
* published dsh-client-ui-model-selection bundle): card capped at
|
|
1707
1874
|
* min(360px, 100vh - 96px), 34px option cells at 13/18 with 5px 7px padding,
|
|
@@ -1760,6 +1927,7 @@ const modelMenuCss = `
|
|
|
1760
1927
|
overflow: hidden;
|
|
1761
1928
|
}
|
|
1762
1929
|
.dsh-model-menu-status, .dsh-model-menu-empty { color: var(--dsw-alias-label-tertiary); padding: 8px; font-size: 12px; line-height: 18px; }
|
|
1930
|
+
.dsh-model-menu-header { flex: none; padding: 2px 2px 4px; }
|
|
1763
1931
|
.dsh-model-menu-searchRow { flex-shrink: 0; margin: 2px 0 3px; position: relative; }
|
|
1764
1932
|
.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; }
|
|
1765
1933
|
.dsh-model-menu-searchRow .dsh-model-menu-search:focus-within { border-color: transparent; }
|
|
@@ -1838,8 +2006,236 @@ const modelMenuCss = `
|
|
|
1838
2006
|
.dsh-model-menu-cellLabel { white-space: nowrap; flex: none; }
|
|
1839
2007
|
.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; }
|
|
1840
2008
|
.dsh-model-menu-cellChevron { width: 12px; height: 12px; color: var(--dsw-alias-menu-icon); flex: none; }
|
|
2009
|
+
/* Discrete reasoning-effort slider under the model list (enhanced mode).
|
|
2010
|
+
* The painted control is layered over a fully functional native range input:
|
|
2011
|
+
* the input stays on top, transparent, and owns pointer/keyboard/ARIA, while
|
|
2012
|
+
* the decorative track, fill and thumb render beneath it at pointer-events:
|
|
2013
|
+
* none. Position lives in one custom property (--_p, 0–100) on the rail, so
|
|
2014
|
+
* the only animated work a level change triggers is a clip-path/left
|
|
2015
|
+
* transition — no keyframes, no element churn. */
|
|
2016
|
+
.dsh-model-menu-sliderGroup { border-top: 0.5px solid var(--dsw-alias-border-l2); flex: none; margin-top: 4px; padding: 6px 8px 8px; }
|
|
2017
|
+
.dsh-model-menu-sliderHead { align-items: baseline; justify-content: space-between; gap: 8px; margin: 0 2px 4px; display: flex; }
|
|
2018
|
+
.dsh-model-menu-sliderTitle { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; }
|
|
2019
|
+
.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); }
|
|
2020
|
+
.dsh-model-menu-sliderRail { position: relative; direction: ltr; height: 38px; margin-top: 2px; }
|
|
2021
|
+
.dsh-model-menu-sliderRail:has(:disabled) { opacity: 0.45; }
|
|
2022
|
+
.dsh-model-menu-sliderInput {
|
|
2023
|
+
appearance: none;
|
|
2024
|
+
-webkit-appearance: none;
|
|
2025
|
+
box-sizing: border-box;
|
|
2026
|
+
border: 0;
|
|
2027
|
+
cursor: pointer;
|
|
2028
|
+
direction: ltr;
|
|
2029
|
+
opacity: 0;
|
|
2030
|
+
margin: 0;
|
|
2031
|
+
padding: 0;
|
|
2032
|
+
width: 100%;
|
|
2033
|
+
height: 100%;
|
|
2034
|
+
position: absolute;
|
|
2035
|
+
inset: 0;
|
|
2036
|
+
z-index: 1;
|
|
2037
|
+
touch-action: pan-y;
|
|
2038
|
+
}
|
|
2039
|
+
.dsh-model-menu-sliderInput:disabled { cursor: default; }
|
|
2040
|
+
/* The transparent input must keep the painted thumb's geometry: an
|
|
2041
|
+
* appearance:none webkit thumb defaults to 16px, which would shift the
|
|
2042
|
+
* end-point positions 7px off the decorative 30px disc and misplace the
|
|
2043
|
+
* native drag hit target. Track gets the full rail height so the whole band
|
|
2044
|
+
* maps to values identically. */
|
|
2045
|
+
.dsh-model-menu-sliderInput::-webkit-slider-runnable-track { width: 100%; height: 100%; }
|
|
2046
|
+
.dsh-model-menu-sliderInput::-webkit-slider-thumb { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; margin: 0; }
|
|
2047
|
+
.dsh-model-menu-sliderInput::-moz-range-track { width: 100%; height: 100%; }
|
|
2048
|
+
.dsh-model-menu-sliderInput::-moz-range-thumb { width: 30px; height: 30px; margin: 0; border: 0; }
|
|
2049
|
+
.dsh-model-menu-sliderTrack {
|
|
2050
|
+
pointer-events: none;
|
|
2051
|
+
border-radius: 999px;
|
|
2052
|
+
width: 100%;
|
|
2053
|
+
height: 26px;
|
|
2054
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
2055
|
+
box-shadow: inset 0 0 0 0.5px var(--dsw-alias-border-l2);
|
|
2056
|
+
position: absolute;
|
|
2057
|
+
inset-inline: 0;
|
|
2058
|
+
top: 50%;
|
|
2059
|
+
transform: translateY(-50%);
|
|
2060
|
+
/* No overflow:hidden — it would clip the fill's drop-shadow glow; the bar's
|
|
2061
|
+
* own clip-path already supplies the rounded reveal shape. */
|
|
2062
|
+
}
|
|
2063
|
+
/* The accent ramp paints the full track width once and is revealed by an
|
|
2064
|
+
* animated clip-path, so raising the effort unwraps more of the spectrum
|
|
2065
|
+
* instead of squashing the same gradient into a narrower bar. The glow lives
|
|
2066
|
+
* on the un-clipped parent: a filter on the clipped element itself would be
|
|
2067
|
+
* clipped away with it. */
|
|
2068
|
+
.dsh-model-menu-sliderFill {
|
|
2069
|
+
display: block;
|
|
2070
|
+
height: 100%;
|
|
2071
|
+
filter: drop-shadow(0 0 5px var(--dsw-alias-state-business-primary));
|
|
2072
|
+
transition: filter var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2073
|
+
}
|
|
2074
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill,
|
|
2075
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderTrack .dsh-model-menu-sliderFill {
|
|
2076
|
+
filter: drop-shadow(0 0 9px var(--dsw-alias-state-business-primary));
|
|
2077
|
+
}
|
|
2078
|
+
/* The fill stops at the thumb's center, not at the track edge — at a 30px
|
|
2079
|
+
* thumb the end stops sit 15px in from each side, matching the node dots. */
|
|
2080
|
+
.dsh-model-menu-sliderFillBar {
|
|
2081
|
+
display: block;
|
|
2082
|
+
height: 100%;
|
|
2083
|
+
background: linear-gradient(90deg,
|
|
2084
|
+
var(--dsw-alias-state-business-primary),
|
|
2085
|
+
var(--dsw-alias-state-success-primary),
|
|
2086
|
+
var(--dsw-alias-state-warn-primary),
|
|
2087
|
+
var(--dsw-alias-state-error-primary));
|
|
2088
|
+
clip-path: inset(0 calc(100% - 15px - (100% - 30px) * var(--_p, 0) / 100) 0 0 round 999px);
|
|
2089
|
+
transition: clip-path var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2090
|
+
}
|
|
2091
|
+
/* Stop dots: one per declared row (default + every advertised effort), evenly
|
|
2092
|
+
* spread across the band between the two thumb-center extremes (inset 15px,
|
|
2093
|
+
* i.e. half the 30px thumb). --_n is 0-100 along that band. A covered dot
|
|
2094
|
+
* reads as a pale pin through the gradient; an uncovered one stays a neutral
|
|
2095
|
+
* notch on the empty track. */
|
|
2096
|
+
.dsh-model-menu-sliderNodes {
|
|
2097
|
+
pointer-events: none;
|
|
2098
|
+
display: block;
|
|
2099
|
+
position: absolute;
|
|
2100
|
+
inset-inline: 15px;
|
|
2101
|
+
inset-block: 0;
|
|
2102
|
+
}
|
|
2103
|
+
.dsh-model-menu-sliderNode {
|
|
2104
|
+
pointer-events: none;
|
|
2105
|
+
box-sizing: border-box;
|
|
2106
|
+
width: 4px;
|
|
2107
|
+
height: 4px;
|
|
2108
|
+
border-radius: 50%;
|
|
2109
|
+
background: color-mix(in srgb, var(--dsw-alias-label-secondary) 35%, transparent);
|
|
2110
|
+
position: absolute;
|
|
2111
|
+
left: calc(var(--_n, 0) * 1%);
|
|
2112
|
+
top: 50%;
|
|
2113
|
+
transform: translate(-50%, -50%);
|
|
2114
|
+
transition: background-color var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
2115
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2116
|
+
}
|
|
2117
|
+
.dsh-model-menu-sliderNode[data-covered] { background: color-mix(in srgb, var(--dsw-alias-label-primary) 60%, transparent); }
|
|
2118
|
+
.dsh-model-menu-sliderThumb {
|
|
2119
|
+
pointer-events: none;
|
|
2120
|
+
box-sizing: border-box;
|
|
2121
|
+
border-radius: 50%;
|
|
2122
|
+
width: 30px;
|
|
2123
|
+
height: 30px;
|
|
2124
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
2125
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
2126
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover), var(--dsw-elevation-soft);
|
|
2127
|
+
position: absolute;
|
|
2128
|
+
left: calc((100% - 30px) * var(--_p, 0) / 100);
|
|
2129
|
+
top: 50%;
|
|
2130
|
+
transform: translateY(-50%) scale(0.92);
|
|
2131
|
+
transition: left var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
2132
|
+
transform var(--ds-transition-duration) var(--ds-ease-in-out),
|
|
2133
|
+
box-shadow var(--ds-transition-duration) var(--ds-ease-in-out);
|
|
2134
|
+
}
|
|
2135
|
+
.dsh-model-menu-sliderInput:hover:not(:disabled) ~ .dsh-model-menu-sliderThumb { transform: translateY(-50%) scale(1); }
|
|
2136
|
+
.dsh-model-menu-sliderInput:active:not(:disabled) ~ .dsh-model-menu-sliderThumb {
|
|
2137
|
+
transform: translateY(-50%) scale(1.1);
|
|
2138
|
+
box-shadow: 0 0 0 4px var(--dsw-alias-interactive-bg-hover), 0 0 12px var(--dsw-alias-state-business-primary);
|
|
2139
|
+
}
|
|
2140
|
+
.dsh-model-menu-sliderInput:focus-visible ~ .dsh-model-menu-sliderThumb {
|
|
2141
|
+
box-shadow: 0 0 0 3px var(--dsw-alias-interactive-bg-hover),
|
|
2142
|
+
0 0 0 calc(3px + var(--dsw-focus-ring-width)) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
2143
|
+
}
|
|
2144
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2145
|
+
.dsh-model-menu-sliderFill, .dsh-model-menu-sliderFillBar, .dsh-model-menu-sliderThumb,
|
|
2146
|
+
.dsh-model-menu-sliderNode, .dsh-model-menu-sliderValue { transition: none; }
|
|
2147
|
+
}
|
|
1841
2148
|
`;
|
|
1842
2149
|
//#endregion
|
|
2150
|
+
//#region ../dsh-plugin-kit/src/model-picker.tsx
|
|
2151
|
+
/**
|
|
2152
|
+
* Optional shared model picker (`klarkxy.model-picker` Factory).
|
|
2153
|
+
*
|
|
2154
|
+
* `@klarkxy/dsh-model-picker`, when installed, registers one root-scoped
|
|
2155
|
+
* Component Factory under `MODEL_PICKER_FACTORY`: an enhanced model menu with
|
|
2156
|
+
* a searchable provider-grouped list and a reasoning-effort slider, built on
|
|
2157
|
+
* the same ModelMenu recipe. Consumers keep their own ModelMenu as the
|
|
2158
|
+
* `fallback`; when no definition is registered the renderer resolves that
|
|
2159
|
+
* fallback verbatim, so the shared picker is strictly opt-in and every saved
|
|
2160
|
+
* value keeps its existing write path.
|
|
2161
|
+
*
|
|
2162
|
+
* `renderFactorySlot` is a framework-composed prop, never a context: the
|
|
2163
|
+
* component a plugin registers into any slot receives it automatically, but
|
|
2164
|
+
* the plugin must pass it down to whichever child renders the menu (or to the
|
|
2165
|
+
* render callback handed to registerSessionSettingsItem).
|
|
2166
|
+
*
|
|
2167
|
+
* The Factory manages its own open state; a consumer's `open`/`onOpenChange`
|
|
2168
|
+
* pair belongs to the fallback menu only and must not be fed to both.
|
|
2169
|
+
*/
|
|
2170
|
+
/**
|
|
2171
|
+
* The trigger every consumer used to hand its own ModelMenu, rebuilt as the
|
|
2172
|
+
* factory's `anchor` local component: identical classes and chevron, the menu
|
|
2173
|
+
* state coming from the occurrence's props instead of local open state.
|
|
2174
|
+
*
|
|
2175
|
+
* `ariaLabel` and `title` are the whole accessible name and tooltip (the
|
|
2176
|
+
* caller formats "field: current route" itself because the factory's resolved
|
|
2177
|
+
* caption may intentionally be a short name). The component identity stays
|
|
2178
|
+
* put across label changes — a new function would unmount the trigger, and
|
|
2179
|
+
* its cleanup would close a menu that `closeOnPick={false}` is keeping open.
|
|
2180
|
+
* The latest label, title, and `onClose` are read from refs.
|
|
2181
|
+
*/
|
|
2182
|
+
function useModelMenuAnchor(ariaLabel, title, onClose) {
|
|
2183
|
+
const closeRef = (0, react.useRef)(onClose);
|
|
2184
|
+
const labelRef = (0, react.useRef)(ariaLabel);
|
|
2185
|
+
const titleRef = (0, react.useRef)(title);
|
|
2186
|
+
closeRef.current = onClose;
|
|
2187
|
+
labelRef.current = ariaLabel;
|
|
2188
|
+
titleRef.current = title;
|
|
2189
|
+
return (0, react.useMemo)(() => {
|
|
2190
|
+
const ModelPickerTrigger = (props) => {
|
|
2191
|
+
(0, react.useEffect)(() => () => closeRef.current?.(), []);
|
|
2192
|
+
const record = props;
|
|
2193
|
+
const injected = {};
|
|
2194
|
+
for (const [key, value] of Object.entries(record)) if (key.startsWith("aria-")) injected[key] = value;
|
|
2195
|
+
const extraClass = typeof record.className === "string" ? ` ${record.className}` : "";
|
|
2196
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2197
|
+
type: "button",
|
|
2198
|
+
className: `dsh-model-menu-trigger${extraClass}`,
|
|
2199
|
+
disabled: props.disabled,
|
|
2200
|
+
"aria-haspopup": "menu",
|
|
2201
|
+
"aria-expanded": props.open,
|
|
2202
|
+
...injected,
|
|
2203
|
+
"aria-label": labelRef.current,
|
|
2204
|
+
title: titleRef.current,
|
|
2205
|
+
children: [
|
|
2206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2207
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2208
|
+
children: props.label
|
|
2209
|
+
}),
|
|
2210
|
+
props.effortLabel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2211
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2212
|
+
children: props.effortLabel
|
|
2213
|
+
}) : null,
|
|
2214
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${props.open ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2215
|
+
]
|
|
2216
|
+
});
|
|
2217
|
+
};
|
|
2218
|
+
return ModelPickerTrigger;
|
|
2219
|
+
}, []);
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Render the shared model picker while it is registered; `options.fallback`
|
|
2223
|
+
* whenever it is not (plugin absent, disabled, or unloaded).
|
|
2224
|
+
*
|
|
2225
|
+
* `options.anchor` is optional: the factory ships a default trigger, so only
|
|
2226
|
+
* consumers that need their own chrome (custom aria-label, tooltip, class)
|
|
2227
|
+
* pass one built with {@link useModelMenuAnchor}. The returned node is never
|
|
2228
|
+
* assumed to be the factory's own output — a defensive `?? fallback` covers a
|
|
2229
|
+
* renderer that yields null for an unregistered definition.
|
|
2230
|
+
*/
|
|
2231
|
+
function renderModelPicker(renderFactorySlot, props, options) {
|
|
2232
|
+
if (typeof renderFactorySlot !== "function") return options.fallback;
|
|
2233
|
+
return renderFactorySlot("klarkxy.model-picker", props, {
|
|
2234
|
+
...options.anchor ? { slots: { ["anchor"]: options.anchor } } : {},
|
|
2235
|
+
fallback: options.fallback
|
|
2236
|
+
}) ?? options.fallback;
|
|
2237
|
+
}
|
|
2238
|
+
//#endregion
|
|
1843
2239
|
//#region ../dsh-plugin-kit/src/auxiliary-model-client.tsx
|
|
1844
2240
|
const AUXILIARY_MODEL_PACKAGE = "@klarkxy/dsh-plugin-kit";
|
|
1845
2241
|
const AUXILIARY_MODEL_SEAT_ID = "auxiliary-model";
|
|
@@ -1917,7 +2313,7 @@ async function callAuxiliaryRpc(client, endpoint, payload = {}) {
|
|
|
1917
2313
|
* The shared card. All state comes from the shared host service over the
|
|
1918
2314
|
* plugin-kit RPC channel; the card only renders the stored row.
|
|
1919
2315
|
*/
|
|
1920
|
-
function AuxiliaryModelCard({ client, subscribe }) {
|
|
2316
|
+
function AuxiliaryModelCard({ client, subscribe, renderFactorySlot }) {
|
|
1921
2317
|
const language = useAuxiliaryLocale(client);
|
|
1922
2318
|
const text = auxiliaryCopy[language];
|
|
1923
2319
|
const [settings, setSettings] = (0, react.useState)();
|
|
@@ -2038,6 +2434,17 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2038
2434
|
refreshCatalog
|
|
2039
2435
|
]);
|
|
2040
2436
|
const locked = busy;
|
|
2437
|
+
const current = settings?.model ?? {
|
|
2438
|
+
provider: "",
|
|
2439
|
+
model: ""
|
|
2440
|
+
};
|
|
2441
|
+
const follow = !current.provider || !current.model;
|
|
2442
|
+
const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
|
|
2443
|
+
const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
|
|
2444
|
+
const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
|
|
2445
|
+
const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
|
|
2446
|
+
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
|
|
2447
|
+
const anchor = useModelMenuAnchor(`${text.model}: ${settings ? currentLabel : ""}`, void 0, () => setMenuOpen(false));
|
|
2041
2448
|
if (!settings) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2042
2449
|
className: "ams-root dsh-ui-card",
|
|
2043
2450
|
"data-testid": "auxiliary-model-settings",
|
|
@@ -2076,16 +2483,6 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2076
2483
|
})
|
|
2077
2484
|
]
|
|
2078
2485
|
});
|
|
2079
|
-
const current = settings.model ?? {
|
|
2080
|
-
provider: "",
|
|
2081
|
-
model: ""
|
|
2082
|
-
};
|
|
2083
|
-
const follow = !current.provider || !current.model;
|
|
2084
|
-
const selected = follow ? void 0 : choices.find((item) => item.provider === current.provider && item.model === current.model);
|
|
2085
|
-
const efforts = follow ? [] : modelMenuEffortOptions(selected, current.reasoningEffort);
|
|
2086
|
-
const currentLabel = follow ? text.modelDefault : selected?.label ?? `${current.provider} / ${current.model}`;
|
|
2087
|
-
const selectedEffort = efforts.find((item) => item.id === (current.reasoningEffort || ""));
|
|
2088
|
-
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, language) : void 0;
|
|
2089
2486
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2090
2487
|
className: "ams-root dsh-ui-card",
|
|
2091
2488
|
"data-testid": "auxiliary-model-settings",
|
|
@@ -2109,28 +2506,12 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2109
2506
|
state: catalogState,
|
|
2110
2507
|
locale: language
|
|
2111
2508
|
}),
|
|
2112
|
-
|
|
2509
|
+
renderModelPicker(renderFactorySlot, {
|
|
2113
2510
|
locale: language,
|
|
2114
|
-
|
|
2511
|
+
disabled: locked,
|
|
2512
|
+
busy: locked,
|
|
2115
2513
|
onOpenChange: (next) => setMenuOpen(next && !locked),
|
|
2116
2514
|
listClassName: "ams-root ams-menu-portal",
|
|
2117
|
-
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2118
|
-
type: "button",
|
|
2119
|
-
className: "dsh-model-menu-trigger",
|
|
2120
|
-
disabled: locked,
|
|
2121
|
-
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2122
|
-
children: [
|
|
2123
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2124
|
-
className: "dsh-model-menu-triggerLabel",
|
|
2125
|
-
children: currentLabel
|
|
2126
|
-
}),
|
|
2127
|
-
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2128
|
-
className: "dsh-model-menu-triggerEffort",
|
|
2129
|
-
children: effortName ?? text.effortDefault
|
|
2130
|
-
}) : null,
|
|
2131
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2132
|
-
]
|
|
2133
|
-
}),
|
|
2134
2515
|
choices,
|
|
2135
2516
|
selected: follow ? void 0 : {
|
|
2136
2517
|
provider: current.provider,
|
|
@@ -2153,9 +2534,61 @@ function AuxiliaryModelCard({ client, subscribe }) {
|
|
|
2153
2534
|
model: current.model,
|
|
2154
2535
|
...effort ? { reasoningEffort: effort } : {}
|
|
2155
2536
|
}),
|
|
2537
|
+
triggerLabel: currentLabel,
|
|
2538
|
+
triggerEffortLabel: !follow && efforts.length ? effortName ?? text.effortDefault : void 0,
|
|
2156
2539
|
modelLabel: text.model,
|
|
2157
2540
|
modelValue: currentLabel,
|
|
2158
2541
|
defaultEffortLabel: text.effortDefault
|
|
2542
|
+
}, {
|
|
2543
|
+
anchor,
|
|
2544
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
2545
|
+
locale: language,
|
|
2546
|
+
open: menuOpen && !locked,
|
|
2547
|
+
onOpenChange: (next) => setMenuOpen(next && !locked),
|
|
2548
|
+
listClassName: "ams-root ams-menu-portal",
|
|
2549
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2550
|
+
type: "button",
|
|
2551
|
+
className: "dsh-model-menu-trigger",
|
|
2552
|
+
disabled: locked,
|
|
2553
|
+
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2554
|
+
children: [
|
|
2555
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2556
|
+
className: "dsh-model-menu-triggerLabel",
|
|
2557
|
+
children: currentLabel
|
|
2558
|
+
}),
|
|
2559
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2560
|
+
className: "dsh-model-menu-triggerEffort",
|
|
2561
|
+
children: effortName ?? text.effortDefault
|
|
2562
|
+
}) : null,
|
|
2563
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen && !locked ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2564
|
+
]
|
|
2565
|
+
}),
|
|
2566
|
+
choices,
|
|
2567
|
+
selected: follow ? void 0 : {
|
|
2568
|
+
provider: current.provider,
|
|
2569
|
+
model: current.model
|
|
2570
|
+
},
|
|
2571
|
+
onPick: (picked) => save({
|
|
2572
|
+
provider: picked.provider,
|
|
2573
|
+
model: picked.model
|
|
2574
|
+
}),
|
|
2575
|
+
leading: [{
|
|
2576
|
+
id: "default",
|
|
2577
|
+
label: text.modelDefault,
|
|
2578
|
+
selected: follow
|
|
2579
|
+
}],
|
|
2580
|
+
onPickLeading: () => save(null),
|
|
2581
|
+
efforts,
|
|
2582
|
+
selectedEffort: current.reasoningEffort || "",
|
|
2583
|
+
onPickEffort: (effort) => save(follow ? null : {
|
|
2584
|
+
provider: current.provider,
|
|
2585
|
+
model: current.model,
|
|
2586
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
2587
|
+
}),
|
|
2588
|
+
modelLabel: text.model,
|
|
2589
|
+
modelValue: currentLabel,
|
|
2590
|
+
defaultEffortLabel: text.effortDefault
|
|
2591
|
+
})
|
|
2159
2592
|
}),
|
|
2160
2593
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2161
2594
|
className: "dsh-ui-hint",
|
|
@@ -2257,9 +2690,10 @@ function acquireAuxiliaryModelCard(root, ctx) {
|
|
|
2257
2690
|
let unprovide;
|
|
2258
2691
|
const watchLease = acquireWatchCoordinator(root).mount();
|
|
2259
2692
|
const subscriber = createModelSettingsSubscriber(ctx.root, AUXILIARY_MODEL_PACKAGE, AUXILIARY_MODEL_CHANGES_IDENTITY, (ns) => ns === AUXILIARY_MODEL_NS);
|
|
2260
|
-
const render = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
|
|
2693
|
+
const render = (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AuxiliaryModelCard, {
|
|
2261
2694
|
client: root,
|
|
2262
|
-
subscribe: subscriber.subscribe
|
|
2695
|
+
subscribe: subscriber.subscribe,
|
|
2696
|
+
renderFactorySlot: props.renderFactorySlot
|
|
2263
2697
|
});
|
|
2264
2698
|
const service = {
|
|
2265
2699
|
retain() {
|
|
@@ -2721,6 +3155,7 @@ function TitleSettings(props) {
|
|
|
2721
3155
|
const currentLabel = follow ? text.modelDefault : selectedModel?.label ?? `${savedModel.provider} / ${savedModel.model}`;
|
|
2722
3156
|
const selectedEffort = efforts.find((item) => item.id === (savedModel.reasoningEffort || ""));
|
|
2723
3157
|
const effortName = selectedEffort ? modelMenuEffortLabel(selectedEffort, seat.locale) : void 0;
|
|
3158
|
+
const anchor = useModelMenuAnchor(`${text.model}: ${currentLabel}`, void 0, () => setMenuOpen(false));
|
|
2724
3159
|
const title = status?.session?.title;
|
|
2725
3160
|
const pinned = status?.session?.pinned === true;
|
|
2726
3161
|
const weOwn = status?.support?.weOwn === true;
|
|
@@ -2932,28 +3367,12 @@ function TitleSettings(props) {
|
|
|
2932
3367
|
state: catalogState,
|
|
2933
3368
|
locale: seat.locale
|
|
2934
3369
|
}),
|
|
2935
|
-
|
|
3370
|
+
renderModelPicker(props.renderFactorySlot, {
|
|
2936
3371
|
locale: seat.locale,
|
|
2937
|
-
|
|
3372
|
+
disabled: busy || loadingSettings || !status,
|
|
3373
|
+
busy: busy || loadingSettings,
|
|
2938
3374
|
onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
|
|
2939
3375
|
listClassName: "ct-root",
|
|
2940
|
-
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2941
|
-
type: "button",
|
|
2942
|
-
className: "dsh-model-menu-trigger",
|
|
2943
|
-
disabled: busy || loadingSettings || !status,
|
|
2944
|
-
"aria-label": `${text.model}: ${currentLabel}`,
|
|
2945
|
-
children: [
|
|
2946
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2947
|
-
className: "dsh-model-menu-triggerLabel",
|
|
2948
|
-
children: currentLabel
|
|
2949
|
-
}),
|
|
2950
|
-
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2951
|
-
className: "dsh-model-menu-triggerEffort",
|
|
2952
|
-
children: effortName ?? text.modelDefault
|
|
2953
|
-
}) : null,
|
|
2954
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
2955
|
-
]
|
|
2956
|
-
}),
|
|
2957
3376
|
choices,
|
|
2958
3377
|
selected: follow ? void 0 : {
|
|
2959
3378
|
provider: savedModel.provider,
|
|
@@ -2982,9 +3401,67 @@ function TitleSettings(props) {
|
|
|
2982
3401
|
model: savedModel.model,
|
|
2983
3402
|
...effort ? { reasoningEffort: effort } : {}
|
|
2984
3403
|
} }),
|
|
3404
|
+
triggerLabel: currentLabel,
|
|
3405
|
+
triggerEffortLabel: !follow && efforts.length ? effortName ?? text.modelDefault : void 0,
|
|
2985
3406
|
modelLabel: text.model,
|
|
2986
3407
|
modelValue: currentLabel,
|
|
2987
3408
|
defaultEffortLabel: text.modelDefault
|
|
3409
|
+
}, {
|
|
3410
|
+
anchor,
|
|
3411
|
+
fallback: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelMenu, {
|
|
3412
|
+
locale: seat.locale,
|
|
3413
|
+
open: menuOpen && Boolean(status) && !busy && !loadingSettings,
|
|
3414
|
+
onOpenChange: (next) => setMenuOpen(next && Boolean(status) && !busy && !loadingSettings),
|
|
3415
|
+
listClassName: "ct-root",
|
|
3416
|
+
anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
3417
|
+
type: "button",
|
|
3418
|
+
className: "dsh-model-menu-trigger",
|
|
3419
|
+
disabled: busy || loadingSettings || !status,
|
|
3420
|
+
"aria-label": `${text.model}: ${currentLabel}`,
|
|
3421
|
+
children: [
|
|
3422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3423
|
+
className: "dsh-model-menu-triggerLabel",
|
|
3424
|
+
children: currentLabel
|
|
3425
|
+
}),
|
|
3426
|
+
!follow && efforts.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3427
|
+
className: "dsh-model-menu-triggerEffort",
|
|
3428
|
+
children: effortName ?? text.modelDefault
|
|
3429
|
+
}) : null,
|
|
3430
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `dsh-model-menu-chevron${menuOpen ? " dsh-model-menu-chevronOpen" : ""}` })
|
|
3431
|
+
]
|
|
3432
|
+
}),
|
|
3433
|
+
choices,
|
|
3434
|
+
selected: follow ? void 0 : {
|
|
3435
|
+
provider: savedModel.provider,
|
|
3436
|
+
model: savedModel.model
|
|
3437
|
+
},
|
|
3438
|
+
onPick: (picked) => saveSettings({ model: {
|
|
3439
|
+
provider: picked.provider,
|
|
3440
|
+
model: picked.model
|
|
3441
|
+
} }),
|
|
3442
|
+
leading: [{
|
|
3443
|
+
id: "default",
|
|
3444
|
+
label: text.modelDefault,
|
|
3445
|
+
selected: follow
|
|
3446
|
+
}],
|
|
3447
|
+
onPickLeading: () => saveSettings({ model: {
|
|
3448
|
+
provider: "",
|
|
3449
|
+
model: ""
|
|
3450
|
+
} }),
|
|
3451
|
+
efforts,
|
|
3452
|
+
selectedEffort: savedModel.reasoningEffort || "",
|
|
3453
|
+
onPickEffort: (effort) => saveSettings({ model: follow ? {
|
|
3454
|
+
provider: "",
|
|
3455
|
+
model: ""
|
|
3456
|
+
} : {
|
|
3457
|
+
provider: savedModel.provider,
|
|
3458
|
+
model: savedModel.model,
|
|
3459
|
+
...effort ? { reasoningEffort: effort } : {}
|
|
3460
|
+
} }),
|
|
3461
|
+
modelLabel: text.model,
|
|
3462
|
+
modelValue: currentLabel,
|
|
3463
|
+
defaultEffortLabel: text.modelDefault
|
|
3464
|
+
})
|
|
2988
3465
|
}),
|
|
2989
3466
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2990
3467
|
className: "dsh-ui-meta",
|
|
@@ -3052,7 +3529,8 @@ function TitleSettingsItem(props) {
|
|
|
3052
3529
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
|
|
3053
3530
|
client: props.client,
|
|
3054
3531
|
marker: props.marker,
|
|
3055
|
-
subscribeSettings: props.subscribeSettings
|
|
3532
|
+
subscribeSettings: props.subscribeSettings,
|
|
3533
|
+
renderFactorySlot: props.renderFactorySlot
|
|
3056
3534
|
})
|
|
3057
3535
|
}) : null]
|
|
3058
3536
|
});
|
|
@@ -3090,10 +3568,11 @@ function apply(ctx) {
|
|
|
3090
3568
|
ctx.effect(() => registerSessionSettingsItem(ctx, {
|
|
3091
3569
|
id: "current-title",
|
|
3092
3570
|
order: 70
|
|
3093
|
-
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
|
|
3571
|
+
}, (slotProps) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettingsItem, {
|
|
3094
3572
|
client,
|
|
3095
3573
|
marker,
|
|
3096
|
-
subscribeSettings
|
|
3574
|
+
subscribeSettings,
|
|
3575
|
+
renderFactorySlot: slotProps?.renderFactorySlot
|
|
3097
3576
|
})), "current-title.settings");
|
|
3098
3577
|
ctx.effect(() => registerAuxiliaryModelSettings(ctx), "current-title.auxiliary-model");
|
|
3099
3578
|
ctx.effect(() => {
|