@lynn123411/dsh-model-submenu 0.2.5 → 0.3.1
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/DETAILS.md +9 -3
- package/README.md +10 -8
- package/lib/client.js +1158 -326
- package/lib/client.js.map +1 -1
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +26 -20
- package/src/client/ModelPicker.tsx +305 -0
- package/src/client/ModelRow.tsx +127 -0
- package/src/client/ModelSubmenu.module.css +338 -97
- package/src/client/ModelSubmenu.tsx +237 -233
- package/src/client/effort-memory.ts +90 -0
- package/src/client/facts-source.ts +93 -0
- package/src/client/favorites.ts +103 -0
- package/src/client/icons.tsx +36 -0
- package/src/client/index.ts +29 -8
- package/src/client/locales.ts +66 -33
- package/src/client/model-facts.ts +198 -0
- package/src/client/rows.ts +125 -0
- package/src/client/selection.ts +112 -0
package/lib/client.js
CHANGED
|
@@ -8,8 +8,38 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react_dom = require("react-dom");
|
|
9
9
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
//#region src/client/icons.tsx
|
|
12
|
+
/**
|
|
13
|
+
* The one glyph this plugin draws itself.
|
|
14
|
+
*
|
|
15
|
+
* The shared icon library carries no star, so the favorite mark is defined here:
|
|
16
|
+
* a five-point star whose outline and fill use the same path, which keeps the
|
|
17
|
+
* hover-only outline and the standing filled state identical in geometry.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Render the favorite star.
|
|
21
|
+
* @param props.filled - draw the solid star; otherwise the outline.
|
|
22
|
+
* @param props.size - square edge in pixels, matching the shared icon set's default.
|
|
23
|
+
* @param props.className - extra class for colour and placement.
|
|
24
|
+
* @returns the inline SVG.
|
|
25
|
+
*/
|
|
26
|
+
function StarIcon({ filled, size = 14, className }) {
|
|
27
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
28
|
+
width: size,
|
|
29
|
+
height: size,
|
|
30
|
+
viewBox: "0 0 24 24",
|
|
31
|
+
fill: filled ? "currentColor" : "none",
|
|
32
|
+
stroke: "currentColor",
|
|
33
|
+
strokeWidth: filled ? 0 : 1.6,
|
|
34
|
+
strokeLinejoin: "round",
|
|
35
|
+
"aria-hidden": "true",
|
|
36
|
+
className,
|
|
37
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 3.2l2.7 5.6 6.1.85-4.45 4.3 1.08 6.05L12 17.1l-5.43 2.9 1.08-6.05L3.2 9.65l6.1-.85z" })
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
11
41
|
//#region \0dsh-css:/Users/tny/Desktop/work/my-dsh/plugins/dsh-model-submenu/src/client/ModelSubmenu.module.css.mjs
|
|
12
|
-
const css = ".FOsK3a_root{min-width:0;position:relative}.FOsK3a_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}.FOsK3a_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.FOsK3a_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.FOsK3a_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-
|
|
42
|
+
const css = ".FOsK3a_root{min-width:0;display:flex;position:relative}.FOsK3a_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}.FOsK3a_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.FOsK3a_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:var(--dsh-composer-model-text-display,block);overflow:hidden}.FOsK3a_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-state-business-primary);display:var(--dsh-composer-model-text-display,block);flex-shrink:1000;overflow:hidden}.FOsK3a_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}.FOsK3a_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}.FOsK3a_chevronOpen{transform:rotate(180deg)}.FOsK3a_menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-sizing:border-box;width:min(252px,100vw - 24px);max-height:min(70vh,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}.FOsK3a_providerName{text-overflow:ellipsis;white-space:nowrap;min-height:24px;color:var(--dsw-alias-label-tertiary);align-items:center;padding:3px 8px;font-size:12px;font-weight:500;line-height:18px;display:flex;overflow:hidden}.FOsK3a_groups{min-height:0;overflow-y:auto}.FOsK3a_row{align-items:center;gap:2px;display:flex}.FOsK3a_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;padding-inline:6px;background:0 0;border:none;border-radius:10px;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.FOsK3a_rowPick:hover:not(:disabled),.FOsK3a_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.FOsK3a_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.FOsK3a_menu .FOsK3a_rowName{font-weight:600}.FOsK3a_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.FOsK3a_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.FOsK3a_starStatic{color:var(--dsw-alias-state-warn-primary);flex:none;place-items:center;width:14px;display:grid}.FOsK3a_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;background:0 0;border:none;border-radius:8px;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.FOsK3a_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_row:hover .FOsK3a_starButton,.FOsK3a_row:focus-within .FOsK3a_starButton{opacity:1}.FOsK3a_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.FOsK3a_starButton{opacity:1}}.FOsK3a_star{flex:none}.FOsK3a_divider{background:var(--dsw-alias-border-l2);flex:none;height:.5px;margin:3px 2px}.FOsK3a_effortRow{align-items:center;gap:6px;padding:3px 6px;display:flex}.FOsK3a_effortHeading{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px}.FOsK3a_effortTrack{background:var(--dsw-alias-interactive-bg-hover);scrollbar-width:none;border-radius:999px;align-items:center;gap:1px;min-width:0;margin-left:auto;padding:2px;display:flex;overflow-x:auto}.FOsK3a_effortTrack::-webkit-scrollbar{display:none}.FOsK3a_effortSegment{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:999px;outline:none;flex:none;padding:2px 7px;font-size:11px;line-height:16px}.FOsK3a_effortSegment:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.FOsK3a_effortSegment[aria-checked=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);font-weight:600}.FOsK3a_effortSegment:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_effortSegment:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:-2px}.FOsK3a_hint{color:var(--dsw-alias-label-tertiary);margin:0;padding:6px 8px;font-size:11px;line-height:16px}.FOsK3a_moreRow{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:6px;min-height:32px;padding:5px 8px;font-size:12px;line-height:18px;display:flex}.FOsK3a_moreRow:hover,.FOsK3a_moreRow:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_moreIcon{color:var(--dsw-alias-label-tertiary);flex:none}.FOsK3a_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.FOsK3a_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.FOsK3a_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.FOsK3a_pickerContent,.FOsK3a_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.FOsK3a_pickerSearch{padding:0 20px 12px}.FOsK3a_pickerSearchInput{width:100%}.FOsK3a_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);flex-wrap:wrap;gap:2px;padding:0 16px;display:flex}.FOsK3a_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;outline:none;padding:6px 10px;font-size:13px;line-height:18px}.FOsK3a_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.FOsK3a_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.FOsK3a_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.FOsK3a_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.FOsK3a_footerDot{color:var(--dsw-alias-label-dimmed)}";
|
|
13
43
|
const tagId = "@lynn123411/dsh-model-submenu/ModelSubmenu.module.css";
|
|
14
44
|
if (typeof document !== "undefined") {
|
|
15
45
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
@@ -22,31 +52,786 @@ window.__ModuleLoader__.load({
|
|
|
22
52
|
tag.textContent = css;
|
|
23
53
|
}
|
|
24
54
|
var ModelSubmenu_module_css_default = {
|
|
25
|
-
"cell": "FOsK3a_cell",
|
|
26
|
-
"cellChevron": "FOsK3a_cellChevron",
|
|
27
|
-
"cellLabel": "FOsK3a_cellLabel",
|
|
28
|
-
"cellValue": "FOsK3a_cellValue",
|
|
29
55
|
"check": "FOsK3a_check",
|
|
30
56
|
"chevron": "FOsK3a_chevron",
|
|
31
57
|
"chevronOpen": "FOsK3a_chevronOpen",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
58
|
+
"divider": "FOsK3a_divider",
|
|
59
|
+
"effortHeading": "FOsK3a_effortHeading",
|
|
60
|
+
"effortRow": "FOsK3a_effortRow",
|
|
61
|
+
"effortSegment": "FOsK3a_effortSegment",
|
|
62
|
+
"effortTrack": "FOsK3a_effortTrack",
|
|
63
|
+
"footerDot": "FOsK3a_footerDot",
|
|
64
|
+
"footerHint": "FOsK3a_footerHint",
|
|
34
65
|
"groups": "FOsK3a_groups",
|
|
66
|
+
"hint": "FOsK3a_hint",
|
|
35
67
|
"menu": "FOsK3a_menu",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
68
|
+
"moreIcon": "FOsK3a_moreIcon",
|
|
69
|
+
"moreRow": "FOsK3a_moreRow",
|
|
70
|
+
"notice": "FOsK3a_notice",
|
|
71
|
+
"pickerContent": "FOsK3a_pickerContent",
|
|
72
|
+
"pickerDialog": "FOsK3a_pickerDialog",
|
|
73
|
+
"pickerFooter": "FOsK3a_pickerFooter",
|
|
74
|
+
"pickerList": "FOsK3a_pickerList",
|
|
75
|
+
"pickerSearch": "FOsK3a_pickerSearch",
|
|
76
|
+
"pickerSearchInput": "FOsK3a_pickerSearchInput",
|
|
77
|
+
"pickerTab": "FOsK3a_pickerTab",
|
|
78
|
+
"pickerTabs": "FOsK3a_pickerTabs",
|
|
79
|
+
"providerName": "FOsK3a_providerName",
|
|
39
80
|
"retry": "FOsK3a_retry",
|
|
40
81
|
"root": "FOsK3a_root",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
82
|
+
"row": "FOsK3a_row",
|
|
83
|
+
"rowCopy": "FOsK3a_rowCopy",
|
|
84
|
+
"rowMeta": "FOsK3a_rowMeta",
|
|
85
|
+
"rowName": "FOsK3a_rowName",
|
|
86
|
+
"rowPick": "FOsK3a_rowPick",
|
|
87
|
+
"star": "FOsK3a_star",
|
|
88
|
+
"starButton": "FOsK3a_starButton",
|
|
89
|
+
"starStatic": "FOsK3a_starStatic",
|
|
43
90
|
"trigger": "FOsK3a_trigger",
|
|
44
91
|
"triggerEffort": "FOsK3a_triggerEffort",
|
|
45
92
|
"triggerIcon": "FOsK3a_triggerIcon",
|
|
46
|
-
"triggerLabel": "FOsK3a_triggerLabel"
|
|
47
|
-
"warning": "FOsK3a_warning"
|
|
93
|
+
"triggerLabel": "FOsK3a_triggerLabel"
|
|
48
94
|
};
|
|
49
95
|
//#endregion
|
|
96
|
+
//#region src/client/ModelRow.tsx
|
|
97
|
+
/**
|
|
98
|
+
* Render one model row.
|
|
99
|
+
* @param props - the row's content and behaviour.
|
|
100
|
+
* @returns the row element.
|
|
101
|
+
*/
|
|
102
|
+
function ModelRow({ name, selected, busy, star, favorited, meta, starLabel, starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children }) {
|
|
103
|
+
const copy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
104
|
+
className: ModelSubmenu_module_css_default.rowCopy,
|
|
105
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
106
|
+
className: ModelSubmenu_module_css_default.rowName,
|
|
107
|
+
children: name
|
|
108
|
+
}), meta !== void 0 && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
109
|
+
className: ModelSubmenu_module_css_default.rowMeta,
|
|
110
|
+
children: meta
|
|
111
|
+
})]
|
|
112
|
+
}) });
|
|
113
|
+
const mark = favorited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
|
|
114
|
+
filled: true,
|
|
115
|
+
className: ModelSubmenu_module_css_default.star
|
|
116
|
+
}) : null;
|
|
117
|
+
if (star === "static") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
118
|
+
type: "button",
|
|
119
|
+
role: "menuitemradio",
|
|
120
|
+
"aria-checked": selected,
|
|
121
|
+
tabIndex: rowTabIndex,
|
|
122
|
+
className: ModelSubmenu_module_css_default.rowPick,
|
|
123
|
+
title: name,
|
|
124
|
+
disabled: busy,
|
|
125
|
+
onClick: onPick,
|
|
126
|
+
onFocus: onFocusRow,
|
|
127
|
+
"data-selected": selected ? "" : void 0,
|
|
128
|
+
"data-favorited": favorited ? "" : void 0,
|
|
129
|
+
children: [
|
|
130
|
+
copy,
|
|
131
|
+
mark !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
132
|
+
className: ModelSubmenu_module_css_default.starStatic,
|
|
133
|
+
"aria-hidden": "true",
|
|
134
|
+
children: mark
|
|
135
|
+
}),
|
|
136
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
137
|
+
className: ModelSubmenu_module_css_default.check,
|
|
138
|
+
"aria-hidden": !selected,
|
|
139
|
+
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
140
|
+
}),
|
|
141
|
+
children
|
|
142
|
+
]
|
|
143
|
+
});
|
|
144
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
145
|
+
className: ModelSubmenu_module_css_default.row,
|
|
146
|
+
"data-selected": selected ? "" : void 0,
|
|
147
|
+
"data-favorited": favorited ? "" : void 0,
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
150
|
+
type: "button",
|
|
151
|
+
role: "menuitemradio",
|
|
152
|
+
"aria-checked": selected,
|
|
153
|
+
tabIndex: rowTabIndex,
|
|
154
|
+
className: ModelSubmenu_module_css_default.rowPick,
|
|
155
|
+
title: name,
|
|
156
|
+
disabled: busy,
|
|
157
|
+
onClick: onPick,
|
|
158
|
+
onFocus: onFocusRow,
|
|
159
|
+
children: copy
|
|
160
|
+
}),
|
|
161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
162
|
+
type: "button",
|
|
163
|
+
className: ModelSubmenu_module_css_default.starButton,
|
|
164
|
+
"aria-pressed": favorited,
|
|
165
|
+
"aria-label": starLabel,
|
|
166
|
+
tabIndex: starTabStop ? 0 : -1,
|
|
167
|
+
disabled: busy,
|
|
168
|
+
onClick: onToggleFavorite,
|
|
169
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
|
|
170
|
+
filled: favorited,
|
|
171
|
+
className: ModelSubmenu_module_css_default.star
|
|
172
|
+
})
|
|
173
|
+
}),
|
|
174
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
175
|
+
className: ModelSubmenu_module_css_default.check,
|
|
176
|
+
"aria-hidden": !selected,
|
|
177
|
+
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
178
|
+
}),
|
|
179
|
+
children
|
|
180
|
+
]
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/client/favorites.ts
|
|
185
|
+
/** The single browser-wide storage key holding the whole table. */
|
|
186
|
+
const FAVORITES_KEY = "dsh-model-submenu.favorites";
|
|
187
|
+
/**
|
|
188
|
+
* Read the table, keeping only rows that name both halves.
|
|
189
|
+
* @param storage - browser storage, or undefined when the browser withholds it.
|
|
190
|
+
* @returns the stored favorites in stored order; empty when unreadable.
|
|
191
|
+
*/
|
|
192
|
+
function readFavorites(storage) {
|
|
193
|
+
const raw = getItem$1(storage);
|
|
194
|
+
if (raw === null) return [];
|
|
195
|
+
let parsed;
|
|
196
|
+
try {
|
|
197
|
+
parsed = JSON.parse(raw);
|
|
198
|
+
} catch {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
if (!Array.isArray(parsed)) return [];
|
|
202
|
+
return parsed.filter((row) => typeof row === "object" && row !== null && typeof row.provider === "string" && typeof row.model === "string");
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Add or remove one model, writing the whole table back.
|
|
206
|
+
* @param storage - browser storage, or undefined when writes are unavailable.
|
|
207
|
+
* @param provider - the provider route key.
|
|
208
|
+
* @param model - the model id inside that provider.
|
|
209
|
+
* @returns the table after the change, whether or not the write landed.
|
|
210
|
+
*/
|
|
211
|
+
function toggleFavorite(storage, provider, model) {
|
|
212
|
+
const current = readFavorites(storage);
|
|
213
|
+
const next = isFavorite(current, provider, model) ? current.filter((row) => row.provider !== provider || row.model !== model) : [...current, {
|
|
214
|
+
provider,
|
|
215
|
+
model
|
|
216
|
+
}];
|
|
217
|
+
try {
|
|
218
|
+
storage?.setItem(FAVORITES_KEY, JSON.stringify(next));
|
|
219
|
+
} catch {}
|
|
220
|
+
return next;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* @param favorites - the table to search.
|
|
224
|
+
* @param provider - the provider route key.
|
|
225
|
+
* @param model - the model id inside that provider.
|
|
226
|
+
* @returns whether that exact pair is favorited.
|
|
227
|
+
*/
|
|
228
|
+
function isFavorite(favorites, provider, model) {
|
|
229
|
+
return favorites.some((row) => row.provider === provider && row.model === model);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Stable-partition a catalog so favorited rows lead, each part keeping the
|
|
233
|
+
* catalog's own order.
|
|
234
|
+
* @param rows - the catalog in its display order.
|
|
235
|
+
* @param favorited - predicate answering whether one row is favorited.
|
|
236
|
+
* @returns a new list; the input order when nothing is favorited.
|
|
237
|
+
*/
|
|
238
|
+
function orderWithFavorites(rows, favorited) {
|
|
239
|
+
const starred = rows.filter(favorited);
|
|
240
|
+
if (starred.length === 0) return rows;
|
|
241
|
+
return [...starred, ...rows.filter((row) => !favorited(row))];
|
|
242
|
+
}
|
|
243
|
+
function getItem$1(storage) {
|
|
244
|
+
try {
|
|
245
|
+
return storage?.getItem("dsh-model-submenu.favorites") ?? null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
251
|
+
//#region src/client/model-facts.ts
|
|
252
|
+
/**
|
|
253
|
+
* The model knowledge a shipped adapter carries for its own account route.
|
|
254
|
+
*
|
|
255
|
+
* The account route has no `models` list on purpose: the account listing
|
|
256
|
+
* supplies the catalog and settings only carry per-model overrides. The same
|
|
257
|
+
* adapter family ships a default catalog for its API-key route, and the account
|
|
258
|
+
* route serves the same models, so those entries answer what the account
|
|
259
|
+
* profile cannot. Ids and facts mirror the adapter's own table.
|
|
260
|
+
*/
|
|
261
|
+
const FAMILY_DEFAULTS = { "deepseek-account": [{
|
|
262
|
+
id: "deepseek-flash",
|
|
263
|
+
contextWindow: 1e6,
|
|
264
|
+
images: true
|
|
265
|
+
}, {
|
|
266
|
+
id: "deepseek-v4-pro",
|
|
267
|
+
contextWindow: 1e6,
|
|
268
|
+
images: false
|
|
269
|
+
}] };
|
|
270
|
+
/**
|
|
271
|
+
* Build the fact lookup from the settings answer.
|
|
272
|
+
* @param providers - provider directory rows giving each route's settings address.
|
|
273
|
+
* @param namespaces - the describe answer's namespace views.
|
|
274
|
+
* @returns a lookup answering both fields for one provider and model.
|
|
275
|
+
*/
|
|
276
|
+
function buildModelFacts(providers, namespaces) {
|
|
277
|
+
const byName = new Map(namespaces.map((row) => [row.ns, row.value]));
|
|
278
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const row of providers) {
|
|
280
|
+
const profile = readPath(byName.get(row.settingsNs), row.settingsPath);
|
|
281
|
+
if (isRecord(profile)) profiles.set(row.provider, profile);
|
|
282
|
+
}
|
|
283
|
+
return (provider, model) => {
|
|
284
|
+
const profile = profiles.get(provider);
|
|
285
|
+
const entry = profile?.models === void 0 ? void 0 : profile.models.find((candidate) => candidate.id === model);
|
|
286
|
+
if (profile !== void 0 && entry !== void 0) return {
|
|
287
|
+
contextWindow: typeof entry.contextWindow === "number" && entry.contextWindow > 0 ? entry.contextWindow : void 0,
|
|
288
|
+
images: imageAnswer(entry, profile)
|
|
289
|
+
};
|
|
290
|
+
const override = readOverride(profile, model);
|
|
291
|
+
if (override !== void 0) return {
|
|
292
|
+
contextWindow: typeof override.contextWindow === "number" && override.contextWindow > 0 ? override.contextWindow : void 0,
|
|
293
|
+
images: Array.isArray(override.inputModalities) ? containsImage(override.inputModalities) : "unknown"
|
|
294
|
+
};
|
|
295
|
+
const family = FAMILY_DEFAULTS[provider]?.find((candidate) => candidate.id === model);
|
|
296
|
+
return family === void 0 ? {
|
|
297
|
+
contextWindow: void 0,
|
|
298
|
+
images: "unknown"
|
|
299
|
+
} : {
|
|
300
|
+
contextWindow: family.contextWindow,
|
|
301
|
+
images: family.images ? "yes" : "no"
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Read one model entry's accepted image modalities.
|
|
307
|
+
*
|
|
308
|
+
* The two adapter families disagree about an omission. The Messages family
|
|
309
|
+
* names the field `inputModalities`, and leaving it out means text only. The
|
|
310
|
+
* pi-ai family names it `input`, and an empty or absent list defers to the
|
|
311
|
+
* route's `defaultInput`; with no route default the answer stays unknown.
|
|
312
|
+
*/
|
|
313
|
+
function imageAnswer(entry, profile) {
|
|
314
|
+
if (Array.isArray(entry.inputModalities)) return containsImage(entry.inputModalities);
|
|
315
|
+
const defaultInput = Array.isArray(profile.defaultInput) ? profile.defaultInput : void 0;
|
|
316
|
+
if (Array.isArray(entry.input)) {
|
|
317
|
+
if (entry.input.length > 0) return containsImage(entry.input);
|
|
318
|
+
return defaultInput === void 0 ? "unknown" : containsImage(defaultInput);
|
|
319
|
+
}
|
|
320
|
+
return defaultInput === void 0 ? "no" : containsImage(defaultInput);
|
|
321
|
+
}
|
|
322
|
+
/** One model's declared override, when the route keeps a `modelOverrides` table. */
|
|
323
|
+
function readOverride(profile, model) {
|
|
324
|
+
const overrides = profile?.modelOverrides;
|
|
325
|
+
if (!isRecord(overrides)) return void 0;
|
|
326
|
+
const entry = overrides[model];
|
|
327
|
+
return isRecord(entry) ? entry : void 0;
|
|
328
|
+
}
|
|
329
|
+
function containsImage(modalities) {
|
|
330
|
+
return modalities.includes("image") ? "yes" : "no";
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Abbreviate one capacity the way the metadata line shows it.
|
|
334
|
+
* @param tokens - exact advertised capacity in tokens.
|
|
335
|
+
* @returns e.g. `1M ctx`, `1.5M ctx`, `272k ctx`, or `512 ctx` below a thousand.
|
|
336
|
+
*/
|
|
337
|
+
function formatContextWindow(tokens) {
|
|
338
|
+
if (tokens >= 1e6) return `${trimNumber(tokens / 1e6)}M ctx`;
|
|
339
|
+
if (tokens >= 1e3) return `${Math.round(tokens / 1e3)}k ctx`;
|
|
340
|
+
return `${tokens} ctx`;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Build the metadata line's segments.
|
|
344
|
+
* @param input.providerName - the provider's display name, omitted inside that provider's own tab.
|
|
345
|
+
* @param input.facts - what the settings document answered.
|
|
346
|
+
* @param input.thinks - whether the catalog advertises reasoning levels for this model.
|
|
347
|
+
* @param input.effortLabel - the localized word for the reasoning segment.
|
|
348
|
+
* @param input.imageLabel - the localized word for the image segment.
|
|
349
|
+
* @returns the segments to join; an empty list means show no line.
|
|
350
|
+
*/
|
|
351
|
+
function metaSegments(input) {
|
|
352
|
+
const segments = [];
|
|
353
|
+
if (input.providerName !== void 0) segments.push(input.providerName);
|
|
354
|
+
if (input.facts.contextWindow !== void 0) segments.push(formatContextWindow(input.facts.contextWindow));
|
|
355
|
+
if (input.thinks) segments.push(input.effortLabel);
|
|
356
|
+
if (input.facts.images === "yes") segments.push(input.imageLabel);
|
|
357
|
+
return segments;
|
|
358
|
+
}
|
|
359
|
+
function trimNumber(value) {
|
|
360
|
+
const rounded = Math.round(value * 10) / 10;
|
|
361
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
362
|
+
}
|
|
363
|
+
function readPath(value, path) {
|
|
364
|
+
let cursor = value;
|
|
365
|
+
for (const key of path) {
|
|
366
|
+
if (!isRecord(cursor)) return void 0;
|
|
367
|
+
cursor = cursor[key];
|
|
368
|
+
}
|
|
369
|
+
return cursor;
|
|
370
|
+
}
|
|
371
|
+
function isRecord(value) {
|
|
372
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Flatten the catalog to one row per model.
|
|
376
|
+
* @param groups - the shared directory's provider groups, in display order.
|
|
377
|
+
* @returns the rows in catalog order.
|
|
378
|
+
*/
|
|
379
|
+
function flattenGroups(groups) {
|
|
380
|
+
return groups.flatMap((group) => group.models.map((model) => ({
|
|
381
|
+
provider: group.id,
|
|
382
|
+
providerName: group.name,
|
|
383
|
+
model: model.id,
|
|
384
|
+
entry: model
|
|
385
|
+
})));
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Build the picker's tabs: the all-tab, then one per provider that has a model.
|
|
389
|
+
* @param groups - the shared directory's provider groups.
|
|
390
|
+
* @param allLabel - localized label for the all-tab.
|
|
391
|
+
* @returns the tabs in catalog order.
|
|
392
|
+
*/
|
|
393
|
+
function pickerTabs(groups, allLabel = "All") {
|
|
394
|
+
return [{
|
|
395
|
+
id: "*",
|
|
396
|
+
label: allLabel
|
|
397
|
+
}, ...groups.filter((group) => group.models.length > 0).map((group) => ({
|
|
398
|
+
id: group.id,
|
|
399
|
+
label: group.name
|
|
400
|
+
}))];
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* @param rows - the flat catalog.
|
|
404
|
+
* @param tab - the tab id; the all-tab keeps every row.
|
|
405
|
+
* @returns the rows the tab shows, in their current order.
|
|
406
|
+
*/
|
|
407
|
+
function rowsForTab(rows, tab) {
|
|
408
|
+
return tab === "*" ? rows : rows.filter((row) => row.provider === tab);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Shape rows as the shell's ranker candidates: the model name is the name, the
|
|
412
|
+
* provider display name is the second key, so a query reaches either.
|
|
413
|
+
* @param rows - the rows to search.
|
|
414
|
+
* @returns candidates in row order.
|
|
415
|
+
*/
|
|
416
|
+
function searchCandidates(rows) {
|
|
417
|
+
return rows.map((row) => ({
|
|
418
|
+
name: row.entry.name,
|
|
419
|
+
label: row.providerName,
|
|
420
|
+
row
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Read the reasoning metadata the catalog advertises for one model.
|
|
425
|
+
* @param groups - the shared directory's provider groups.
|
|
426
|
+
* @returns the metadata, or undefined when that model advertises none.
|
|
427
|
+
*/
|
|
428
|
+
function reasoningOf(groups) {
|
|
429
|
+
return (provider, model) => groups.find((group) => group.id === provider)?.models.find((candidate) => candidate.id === model)?.reasoning;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Filter rows by a query over the model name and the provider name.
|
|
433
|
+
* @param rows - the rows to search.
|
|
434
|
+
* @param query - the typed text; an empty or blank query keeps every row.
|
|
435
|
+
* @param rank - the shell's own ranker, so this search behaves like every other one.
|
|
436
|
+
* @returns the matching rows.
|
|
437
|
+
*/
|
|
438
|
+
function searchRows(rows, query, rank) {
|
|
439
|
+
const trimmed = query.trim();
|
|
440
|
+
if (trimmed === "") return rows;
|
|
441
|
+
return rank(searchCandidates(rows), trimmed).map((match) => match.row);
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/client/selection.ts
|
|
445
|
+
/**
|
|
446
|
+
* Build the segments for one model's reasoning row.
|
|
447
|
+
* @param reasoning - the catalog's reasoning metadata, undefined when the model has none.
|
|
448
|
+
* @param providerDefaultLabel - the localized label for the "follow the provider" segment.
|
|
449
|
+
* @returns the segments in catalog order; the provider-default one leads when the model declares no default.
|
|
450
|
+
*/
|
|
451
|
+
function effortChoices(reasoning, providerDefaultLabel) {
|
|
452
|
+
if (reasoning === void 0) return [];
|
|
453
|
+
return [...reasoning.defaultEffort === void 0 ? [{
|
|
454
|
+
key: "provider-default",
|
|
455
|
+
effort: void 0,
|
|
456
|
+
label: providerDefaultLabel
|
|
457
|
+
}] : [], ...reasoning.efforts.map((level) => ({
|
|
458
|
+
key: level.id,
|
|
459
|
+
effort: level.id,
|
|
460
|
+
label: level.name
|
|
461
|
+
}))];
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The selection a level click submits.
|
|
465
|
+
* @param live - the selection in force right now.
|
|
466
|
+
* @param effort - the clicked level, undefined meaning the provider default.
|
|
467
|
+
* @returns the selection to submit, or null when the level already is the live one.
|
|
468
|
+
*/
|
|
469
|
+
function planEffortCommit(live, effort) {
|
|
470
|
+
if (sameEffort(live.reasoningEffort, effort)) return null;
|
|
471
|
+
return {
|
|
472
|
+
provider: live.provider,
|
|
473
|
+
model: live.model,
|
|
474
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The selection a model click submits.
|
|
479
|
+
* @param live - the selection in force right now, null before a first selection resolves.
|
|
480
|
+
* @param provider - the clicked model's provider.
|
|
481
|
+
* @param model - the clicked model id.
|
|
482
|
+
* @param reasoningOf - the catalog's reasoning metadata for one provider and model.
|
|
483
|
+
* @param remembered - the level this browser last used for the target model, if any.
|
|
484
|
+
* @returns the selection to submit, or null when the click changes nothing.
|
|
485
|
+
*/
|
|
486
|
+
function planModelCommit(live, provider, model, reasoningOf, remembered) {
|
|
487
|
+
if (live !== null && sameModel(live, provider, model)) return null;
|
|
488
|
+
const offered = reasoningOf(provider, model);
|
|
489
|
+
return {
|
|
490
|
+
provider,
|
|
491
|
+
model,
|
|
492
|
+
...offered !== void 0 && remembered !== void 0 && offered.efforts.some((level) => level.id === remembered) ? { reasoningEffort: remembered } : {}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* @param live - one selection.
|
|
497
|
+
* @param provider - the provider to compare.
|
|
498
|
+
* @param model - the model id to compare.
|
|
499
|
+
* @returns whether both name the same model.
|
|
500
|
+
*/
|
|
501
|
+
function sameModel(live, provider, model) {
|
|
502
|
+
return live.provider === provider && live.model === model;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* @param liveEffort - the level in force, undefined meaning the provider default.
|
|
506
|
+
* @param effort - the level in question.
|
|
507
|
+
* @returns whether they name the same level.
|
|
508
|
+
*/
|
|
509
|
+
function sameEffort(liveEffort, effort) {
|
|
510
|
+
return liveEffort === effort;
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/client/ModelPicker.tsx
|
|
514
|
+
/**
|
|
515
|
+
* The model picker: the centered dialog opened from the menu's last row.
|
|
516
|
+
*
|
|
517
|
+
* It is the one place every provider is visible at once — a search field, a tab
|
|
518
|
+
* per provider with a model, and one flat list with each model's metadata line.
|
|
519
|
+
* Favorites lead the list and can be toggled from a row's star without
|
|
520
|
+
* selecting that row. A click commits at once; a rejected commit keeps the
|
|
521
|
+
* dialog open so the user can retry or pick another model.
|
|
522
|
+
*/
|
|
523
|
+
/**
|
|
524
|
+
* Render the picker.
|
|
525
|
+
* @param props - the catalog, the live selection, and the picker's verbs.
|
|
526
|
+
* @returns the dialog; nothing while closed.
|
|
527
|
+
*/
|
|
528
|
+
function ModelPicker({ open, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t }) {
|
|
529
|
+
const id = (0, react.useId)();
|
|
530
|
+
const searchRef = (0, react.useRef)(null);
|
|
531
|
+
const listRef = (0, react.useRef)(null);
|
|
532
|
+
const [query, setQuery] = (0, react.useState)("");
|
|
533
|
+
const [tab, setTab] = (0, react.useState)("*");
|
|
534
|
+
const [highlight, setHighlight] = (0, react.useState)(0);
|
|
535
|
+
const tabRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
536
|
+
const tabs = (0, react.useMemo)(() => pickerTabs(state.groups, t("picker.all")), [state.groups, t]);
|
|
537
|
+
const reason = (0, react.useMemo)(() => reasoningOf(state.groups), [state.groups]);
|
|
538
|
+
const rows = (0, react.useMemo)(() => orderWithFavorites(rowsForTab(flattenGroups(state.groups), tab), (row) => isFavorite(favorites, row.provider, row.model)), [
|
|
539
|
+
state.groups,
|
|
540
|
+
tab,
|
|
541
|
+
favorites
|
|
542
|
+
]);
|
|
543
|
+
const visible = (0, react.useMemo)(() => orderWithFavorites(searchRows(rows, query, _deepseek_ai_dsh_client_ui_primitives.rankByName), (row) => isFavorite(favorites, row.provider, row.model)), [
|
|
544
|
+
rows,
|
|
545
|
+
query,
|
|
546
|
+
favorites
|
|
547
|
+
]);
|
|
548
|
+
(0, react.useEffect)(() => {
|
|
549
|
+
if (!open) return;
|
|
550
|
+
setQuery("");
|
|
551
|
+
setTab("*");
|
|
552
|
+
setHighlight(0);
|
|
553
|
+
if (listRef.current !== null) listRef.current.scrollTop = 0;
|
|
554
|
+
const frame = requestAnimationFrame(() => {
|
|
555
|
+
searchRef.current?.focus();
|
|
556
|
+
});
|
|
557
|
+
return () => {
|
|
558
|
+
cancelAnimationFrame(frame);
|
|
559
|
+
};
|
|
560
|
+
}, [open]);
|
|
561
|
+
(0, react.useEffect)(() => {
|
|
562
|
+
setHighlight((current) => visible.length === 0 ? 0 : Math.min(current, visible.length - 1));
|
|
563
|
+
}, [visible.length]);
|
|
564
|
+
(0, react.useEffect)(() => {
|
|
565
|
+
(listRef.current?.querySelector(`[data-row-index="${highlight}"]`))?.scrollIntoView({ block: "nearest" });
|
|
566
|
+
}, [highlight]);
|
|
567
|
+
if (!open) return null;
|
|
568
|
+
const activeRow = visible[highlight];
|
|
569
|
+
const activeId = activeRow === void 0 ? void 0 : `${id}-row-${highlight}`;
|
|
570
|
+
const onSearchKeyDown = (event) => {
|
|
571
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
572
|
+
event.preventDefault();
|
|
573
|
+
if (visible.length === 0) return;
|
|
574
|
+
setHighlight((current) => {
|
|
575
|
+
return (current + (event.key === "ArrowDown" ? 1 : -1) + visible.length) % visible.length;
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
if (event.key === "Enter" && activeRow !== void 0 && !busy) {
|
|
580
|
+
event.preventDefault();
|
|
581
|
+
pick(activeRow);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
const pick = (row) => {
|
|
585
|
+
const selection = planModelCommit(live, row.provider, row.model, reason);
|
|
586
|
+
if (selection === null) {
|
|
587
|
+
onClose();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
onPick(selection);
|
|
591
|
+
};
|
|
592
|
+
/** Move to another tab and hand it the keyboard, in one step. */
|
|
593
|
+
const goTab = (next) => {
|
|
594
|
+
if (next === void 0 || next.id === tab) return;
|
|
595
|
+
setTab(next.id);
|
|
596
|
+
setHighlight(0);
|
|
597
|
+
tabRefs.current.get(next.id)?.focus();
|
|
598
|
+
};
|
|
599
|
+
const onTabKeyDown = (event) => {
|
|
600
|
+
const index = tabs.findIndex((candidate) => candidate.id === tab);
|
|
601
|
+
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
|
602
|
+
event.preventDefault();
|
|
603
|
+
const step = event.key === "ArrowRight" ? 1 : -1;
|
|
604
|
+
goTab(tabs[((index === -1 ? 0 : index) + step + tabs.length) % tabs.length]);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (event.key === "Home" || event.key === "End") {
|
|
608
|
+
event.preventDefault();
|
|
609
|
+
goTab(tabs[event.key === "Home" ? 0 : tabs.length - 1]);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
const failures = state.failures.filter((failure) => tab === "*" || failure.id === tab);
|
|
613
|
+
const failed = state.error !== null && state.status === "error";
|
|
614
|
+
const loading = state.status === "loading";
|
|
615
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
|
|
616
|
+
open,
|
|
617
|
+
onClose,
|
|
618
|
+
title: t("picker.title"),
|
|
619
|
+
closeLabel: t("picker.close"),
|
|
620
|
+
className: ModelSubmenu_module_css_default.pickerDialog ?? "",
|
|
621
|
+
contentClassName: ModelSubmenu_module_css_default.pickerContent ?? "",
|
|
622
|
+
children: [
|
|
623
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
624
|
+
className: ModelSubmenu_module_css_default.pickerSearch,
|
|
625
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
626
|
+
ref: searchRef,
|
|
627
|
+
className: ModelSubmenu_module_css_default.pickerSearchInput ?? "",
|
|
628
|
+
type: "text",
|
|
629
|
+
role: "searchbox",
|
|
630
|
+
"aria-label": t("picker.search"),
|
|
631
|
+
"aria-controls": `${id}-list`,
|
|
632
|
+
"aria-activedescendant": activeId,
|
|
633
|
+
placeholder: t("picker.search"),
|
|
634
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }),
|
|
635
|
+
value: query,
|
|
636
|
+
readOnly: busy,
|
|
637
|
+
onChange: (event) => {
|
|
638
|
+
setQuery(event.target.value);
|
|
639
|
+
},
|
|
640
|
+
onKeyDown: onSearchKeyDown,
|
|
641
|
+
"data-modal-autofocus": ""
|
|
642
|
+
})
|
|
643
|
+
}),
|
|
644
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
645
|
+
className: ModelSubmenu_module_css_default.pickerTabs,
|
|
646
|
+
role: "tablist",
|
|
647
|
+
"aria-label": t("picker.title"),
|
|
648
|
+
children: tabs.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
649
|
+
ref: (node) => {
|
|
650
|
+
if (node === null) tabRefs.current.delete(entry.id);
|
|
651
|
+
else tabRefs.current.set(entry.id, node);
|
|
652
|
+
},
|
|
653
|
+
type: "button",
|
|
654
|
+
role: "tab",
|
|
655
|
+
id: `${id}-tab-${entry.id}`,
|
|
656
|
+
"aria-selected": entry.id === tab,
|
|
657
|
+
"aria-controls": `${id}-list`,
|
|
658
|
+
tabIndex: 0,
|
|
659
|
+
className: ModelSubmenu_module_css_default.pickerTab,
|
|
660
|
+
onClick: () => {
|
|
661
|
+
setTab(entry.id);
|
|
662
|
+
setHighlight(0);
|
|
663
|
+
},
|
|
664
|
+
onKeyDown: onTabKeyDown,
|
|
665
|
+
onFocus: () => {
|
|
666
|
+
if (entry.id !== tab) {
|
|
667
|
+
setTab(entry.id);
|
|
668
|
+
setHighlight(0);
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
children: entry.label
|
|
672
|
+
}, entry.id))
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
675
|
+
ref: listRef,
|
|
676
|
+
id: `${id}-list`,
|
|
677
|
+
className: ModelSubmenu_module_css_default.pickerList,
|
|
678
|
+
role: "menu",
|
|
679
|
+
"aria-label": t("picker.title"),
|
|
680
|
+
children: [
|
|
681
|
+
loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
682
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
683
|
+
children: t("status.loading")
|
|
684
|
+
}),
|
|
685
|
+
failed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
686
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
687
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.failed", { message: state.error ?? "" }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
688
|
+
type: "button",
|
|
689
|
+
className: ModelSubmenu_module_css_default.retry,
|
|
690
|
+
onClick: onReload,
|
|
691
|
+
children: t("action.reload")
|
|
692
|
+
})]
|
|
693
|
+
}),
|
|
694
|
+
failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
695
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
696
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.providerFailed", {
|
|
697
|
+
name: failure.name,
|
|
698
|
+
message: failure.message
|
|
699
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
700
|
+
type: "button",
|
|
701
|
+
className: ModelSubmenu_module_css_default.retry,
|
|
702
|
+
onClick: onReload,
|
|
703
|
+
children: t("action.reload")
|
|
704
|
+
})]
|
|
705
|
+
}, failure.id)),
|
|
706
|
+
visible.length === 0 && !loading && !failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
707
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
708
|
+
children: state.groups.length === 0 ? t("status.empty") : t("picker.searchEmpty")
|
|
709
|
+
}),
|
|
710
|
+
visible.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
711
|
+
"data-row-index": index,
|
|
712
|
+
id: `${id}-row-${index}`,
|
|
713
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
|
|
714
|
+
name: row.entry.name,
|
|
715
|
+
selected: live?.provider === row.provider && live.model === row.model,
|
|
716
|
+
busy,
|
|
717
|
+
star: "interactive",
|
|
718
|
+
favorited: isFavorite(favorites, row.provider, row.model),
|
|
719
|
+
meta: metaLine({
|
|
720
|
+
row,
|
|
721
|
+
facts: facts(row.provider, row.model),
|
|
722
|
+
providerName: tab === "*" ? row.providerName : void 0,
|
|
723
|
+
thinks: row.entry.reasoning !== void 0,
|
|
724
|
+
effortLabel: t("meta.effort"),
|
|
725
|
+
imageLabel: t("meta.images")
|
|
726
|
+
}),
|
|
727
|
+
starLabel: isFavorite(favorites, row.provider, row.model) ? t("picker.unfavorite", { model: row.entry.name }) : t("picker.favorite", { model: row.entry.name }),
|
|
728
|
+
starTabStop: index === highlight,
|
|
729
|
+
rowTabIndex: -1,
|
|
730
|
+
onPick: () => {
|
|
731
|
+
pick(row);
|
|
732
|
+
},
|
|
733
|
+
onFocusRow: () => {
|
|
734
|
+
setHighlight(index);
|
|
735
|
+
},
|
|
736
|
+
onToggleFavorite: () => {
|
|
737
|
+
onToggleFavorite(row.provider, row.model);
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
}, `${row.provider}/${row.model}`))
|
|
741
|
+
]
|
|
742
|
+
}),
|
|
743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
744
|
+
className: ModelSubmenu_module_css_default.pickerFooter,
|
|
745
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
746
|
+
className: ModelSubmenu_module_css_default.footerHint,
|
|
747
|
+
children: [
|
|
748
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["↑", "↓"] }),
|
|
749
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintNavigate") }),
|
|
750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
751
|
+
className: ModelSubmenu_module_css_default.footerDot,
|
|
752
|
+
children: "·"
|
|
753
|
+
}),
|
|
754
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Enter"] }),
|
|
755
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintSelect") }),
|
|
756
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
757
|
+
className: ModelSubmenu_module_css_default.footerDot,
|
|
758
|
+
children: "·"
|
|
759
|
+
}),
|
|
760
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Esc"] }),
|
|
761
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintClose") })
|
|
762
|
+
]
|
|
763
|
+
})
|
|
764
|
+
})
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function metaLine(input) {
|
|
769
|
+
return metaSegments({
|
|
770
|
+
...input.providerName === void 0 ? {} : { providerName: input.providerName },
|
|
771
|
+
facts: input.facts,
|
|
772
|
+
thinks: input.thinks,
|
|
773
|
+
effortLabel: input.effortLabel,
|
|
774
|
+
imageLabel: input.imageLabel
|
|
775
|
+
}).join(" · ");
|
|
776
|
+
}
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/client/effort-memory.ts
|
|
779
|
+
/** The single browser-wide storage key holding the whole memory. */
|
|
780
|
+
const EFFORTS_KEY = "dsh-model-submenu.efforts";
|
|
781
|
+
/**
|
|
782
|
+
* Read the memory, keeping only rows naming both halves and a level.
|
|
783
|
+
* @param storage - browser storage, or undefined when the browser withholds it.
|
|
784
|
+
* @returns the remembered rows; empty when unreadable.
|
|
785
|
+
*/
|
|
786
|
+
function readEfforts(storage) {
|
|
787
|
+
const raw = getItem(storage);
|
|
788
|
+
if (raw === null) return [];
|
|
789
|
+
let parsed;
|
|
790
|
+
try {
|
|
791
|
+
parsed = JSON.parse(raw);
|
|
792
|
+
} catch {
|
|
793
|
+
return [];
|
|
794
|
+
}
|
|
795
|
+
if (!Array.isArray(parsed)) return [];
|
|
796
|
+
return parsed.filter((row) => typeof row === "object" && row !== null && typeof row.provider === "string" && typeof row.model === "string" && typeof row.effort === "string" && row.effort !== "");
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* @param memory - the rows to search.
|
|
800
|
+
* @param provider - the provider route key.
|
|
801
|
+
* @param model - the model id inside that provider.
|
|
802
|
+
* @returns the level last chosen for that exact model, or undefined.
|
|
803
|
+
*/
|
|
804
|
+
function effortFor(memory, provider, model) {
|
|
805
|
+
return memory.find((row) => row.provider === provider && row.model === model)?.effort;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Remember one model's level, or forget it when the choice is the provider default.
|
|
809
|
+
* @param storage - browser storage, or undefined when writes are unavailable.
|
|
810
|
+
* @param provider - the provider route key.
|
|
811
|
+
* @param model - the model id inside that provider.
|
|
812
|
+
* @param effort - the chosen level, undefined meaning the provider default.
|
|
813
|
+
* @returns the memory after the change, whether or not the write landed.
|
|
814
|
+
*/
|
|
815
|
+
function rememberEffort(storage, provider, model, effort) {
|
|
816
|
+
const rest = readEfforts(storage).filter((row) => row.provider !== provider || row.model !== model);
|
|
817
|
+
const next = effort === void 0 ? rest : [...rest, {
|
|
818
|
+
provider,
|
|
819
|
+
model,
|
|
820
|
+
effort
|
|
821
|
+
}];
|
|
822
|
+
try {
|
|
823
|
+
storage?.setItem(EFFORTS_KEY, JSON.stringify(next));
|
|
824
|
+
} catch {}
|
|
825
|
+
return next;
|
|
826
|
+
}
|
|
827
|
+
function getItem(storage) {
|
|
828
|
+
try {
|
|
829
|
+
return storage?.getItem("dsh-model-submenu.efforts") ?? null;
|
|
830
|
+
} catch {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
//#endregion
|
|
50
835
|
//#region src/client/ModelSubmenu.tsx
|
|
51
836
|
/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real. */
|
|
52
837
|
const MEASURE_STYLE = {
|
|
@@ -57,27 +842,26 @@ window.__ModuleLoader__.load({
|
|
|
57
842
|
const cls = (...parts) => parts.filter((part) => part !== false && part !== void 0).join(" ");
|
|
58
843
|
/**
|
|
59
844
|
* The composer's model seat (`conversation.input.model`), taken over from the
|
|
60
|
-
* built-in occupant with a provider
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
845
|
+
* built-in occupant with a provider-scoped menu: one provider's models above,
|
|
846
|
+
* the reasoning row as segments, the cache warning, and the door to the picker
|
|
847
|
+
* that lists every provider.
|
|
848
|
+
*
|
|
849
|
+
* Every click commits at once: a model, a reasoning level, or the picker door.
|
|
850
|
+
* Closing the menu never writes anything, and the picker reads the level the
|
|
851
|
+
* Host already accepted, so its "current" reading is never stale.
|
|
852
|
+
*/
|
|
853
|
+
function ModelSubmenu({ locked, available, directory, load, select, t, factsSource }) {
|
|
68
854
|
const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
|
|
69
855
|
const [open, setOpen] = (0, react.useState)(false);
|
|
70
|
-
const [
|
|
71
|
-
const [
|
|
72
|
-
const
|
|
856
|
+
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
857
|
+
const [favorites, setFavorites] = (0, react.useState)(() => readFavorites(storageOf()));
|
|
858
|
+
const [memory, setMemory] = (0, react.useState)(() => readEfforts(storageOf()));
|
|
73
859
|
const [toast, setToast] = (0, react.useState)(null);
|
|
74
860
|
const toastSeq = (0, react.useRef)(0);
|
|
75
861
|
const rootRef = (0, react.useRef)(null);
|
|
76
862
|
const triggerRef = (0, react.useRef)(null);
|
|
77
863
|
const menuRef = (0, react.useRef)(null);
|
|
78
864
|
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
79
|
-
const itemRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
80
|
-
const focusIntent = (0, react.useRef)(null);
|
|
81
865
|
const id = (0, react.useId)();
|
|
82
866
|
const groups = state.groups;
|
|
83
867
|
const current = state.current;
|
|
@@ -85,47 +869,71 @@ window.__ModuleLoader__.load({
|
|
|
85
869
|
const currentModel = currentGroup?.models.find((model) => model.id === current?.model);
|
|
86
870
|
const reasoning = currentModel?.reasoning;
|
|
87
871
|
const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
effort: level.id,
|
|
96
|
-
label: level.name
|
|
97
|
-
}))], [reasoning, t]);
|
|
98
|
-
const shownGroup = pane === "models" && providerId !== null ? groups.find((group) => group.id === providerId) : void 0;
|
|
872
|
+
/**
|
|
873
|
+
* The trigger's caption. A model that advertises no levels still says so in
|
|
874
|
+
* the same place: the blue caption beside the name reads 默认 rather than
|
|
875
|
+
* disappearing, so the shape of the control never changes with the model.
|
|
876
|
+
*/
|
|
877
|
+
const effortLabel = reasoning === void 0 || effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
|
|
878
|
+
const choices = (0, react.useMemo)(() => effortChoices(reasoning, t("effort.providerDefault")), [reasoning, t]);
|
|
99
879
|
const busy = state.status === "selecting";
|
|
880
|
+
const live = (0, react.useMemo)(() => selectionOf(state), [state]);
|
|
881
|
+
const reason = (0, react.useMemo)(() => reasoningOf(groups), [groups]);
|
|
882
|
+
const facts = (0, react.useSyncExternalStore)((fn) => factsSource.subscribe(fn), () => factsSource.getSnapshot());
|
|
883
|
+
const menuRows = (0, react.useMemo)(() => currentGroup === void 0 ? [] : orderWithFavorites(currentGroup.models, (model) => isFavorite(favorites, currentGroup.id, model.id)), [currentGroup, favorites]);
|
|
884
|
+
const raiseToast = (text) => {
|
|
885
|
+
toastSeq.current += 1;
|
|
886
|
+
setToast({
|
|
887
|
+
seq: toastSeq.current,
|
|
888
|
+
text
|
|
889
|
+
});
|
|
890
|
+
};
|
|
100
891
|
const reload = () => {
|
|
101
|
-
lastActionRef.current = "load";
|
|
102
892
|
load();
|
|
103
893
|
};
|
|
894
|
+
const submit = (selection, onDone, onRejected) => {
|
|
895
|
+
select(selection).then((result) => {
|
|
896
|
+
if (result === void 0 || result.ok) {
|
|
897
|
+
remember(selection.provider, selection.model, selection.reasoningEffort);
|
|
898
|
+
onDone?.();
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
onRejected?.();
|
|
902
|
+
raiseToast(result.error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${result.error.code}: ${result.error.message}` }));
|
|
903
|
+
});
|
|
904
|
+
};
|
|
905
|
+
/** Close the menu. Closing writes nothing: every choice already committed on click. */
|
|
906
|
+
const closeMenu = (0, react.useCallback)((restoreFocus) => {
|
|
907
|
+
setOpen(false);
|
|
908
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
909
|
+
triggerRef.current?.focus();
|
|
910
|
+
});
|
|
911
|
+
}, []);
|
|
104
912
|
(0, react.useEffect)(() => {
|
|
105
913
|
if (!open) return;
|
|
106
914
|
const closeOutside = (event) => {
|
|
107
915
|
if (rootRef.current?.contains(event.target) === true) return;
|
|
108
916
|
if (menuRef.current?.contains(event.target) === true) return;
|
|
109
|
-
|
|
110
|
-
setPane("root");
|
|
917
|
+
closeMenu(false);
|
|
111
918
|
};
|
|
112
919
|
document.addEventListener("mousedown", closeOutside);
|
|
113
920
|
return () => {
|
|
114
921
|
document.removeEventListener("mousedown", closeOutside);
|
|
115
922
|
};
|
|
116
|
-
}, [open]);
|
|
923
|
+
}, [open, closeMenu]);
|
|
117
924
|
(0, react.useEffect)(() => {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
925
|
+
if (!open) return;
|
|
926
|
+
const frame = requestAnimationFrame(() => {
|
|
927
|
+
const checked = menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])");
|
|
928
|
+
const first = menuRef.current?.querySelector("[role=\"menuitemradio\"]:not([disabled])");
|
|
929
|
+
const target = checked ?? first ?? triggerRef.current;
|
|
930
|
+
target?.scrollIntoView({ block: "nearest" });
|
|
931
|
+
target?.focus();
|
|
932
|
+
});
|
|
933
|
+
return () => {
|
|
934
|
+
cancelAnimationFrame(frame);
|
|
935
|
+
};
|
|
936
|
+
}, [open]);
|
|
129
937
|
(0, react.useLayoutEffect)(() => {
|
|
130
938
|
if (!open) {
|
|
131
939
|
setMenuPos(null);
|
|
@@ -153,63 +961,72 @@ window.__ModuleLoader__.load({
|
|
|
153
961
|
window.removeEventListener("scroll", place, true);
|
|
154
962
|
window.removeEventListener("resize", place);
|
|
155
963
|
};
|
|
156
|
-
}, [
|
|
157
|
-
open,
|
|
158
|
-
pane,
|
|
159
|
-
state
|
|
160
|
-
]);
|
|
964
|
+
}, [open, state]);
|
|
161
965
|
if (!available) return null;
|
|
162
966
|
const show = () => {
|
|
163
|
-
setPane("root");
|
|
164
|
-
setProviderId(null);
|
|
165
967
|
setOpen(true);
|
|
166
968
|
reload();
|
|
167
969
|
};
|
|
168
|
-
|
|
970
|
+
/** The picker door: the level already landed on click, so only the surface changes. */
|
|
971
|
+
const openPicker = () => {
|
|
169
972
|
setOpen(false);
|
|
170
|
-
|
|
171
|
-
setProviderId(null);
|
|
172
|
-
if (restoreFocus) queueMicrotask(() => {
|
|
173
|
-
triggerRef.current?.focus();
|
|
174
|
-
});
|
|
973
|
+
setPickerOpen(true);
|
|
175
974
|
};
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
975
|
+
/** Record one model's level, so switching away and back restores it. */
|
|
976
|
+
const remember = (provider, model, effort) => {
|
|
977
|
+
setMemory(rememberEffort(storageOf(), provider, model, effort));
|
|
179
978
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
979
|
+
const chooseModel = (provider, model) => {
|
|
980
|
+
const picked = planModelCommit(live, provider, model.id, reason, effortFor(memory, provider, model.id));
|
|
981
|
+
if (picked === null) {
|
|
982
|
+
setOpen(false);
|
|
983
|
+
queueMicrotask(() => {
|
|
984
|
+
triggerRef.current?.focus();
|
|
985
|
+
});
|
|
185
986
|
return;
|
|
186
987
|
}
|
|
187
|
-
|
|
188
|
-
|
|
988
|
+
setOpen(false);
|
|
989
|
+
submit(picked, () => {
|
|
990
|
+
queueMicrotask(() => {
|
|
991
|
+
triggerRef.current?.focus();
|
|
992
|
+
});
|
|
993
|
+
}, () => {
|
|
994
|
+
setOpen(true);
|
|
995
|
+
});
|
|
189
996
|
};
|
|
997
|
+
const rowButtons = () => [...menuRef.current?.querySelectorAll("[role=\"menuitemradio\"]:not([disabled])") ?? []];
|
|
190
998
|
const moveFocus = (offset) => {
|
|
191
|
-
const items =
|
|
999
|
+
const items = rowButtons();
|
|
192
1000
|
if (items.length === 0) return;
|
|
193
1001
|
const active = items.findIndex((item) => item === document.activeElement);
|
|
194
1002
|
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
195
1003
|
};
|
|
1004
|
+
const onEffortKeyDown = (event) => {
|
|
1005
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
1006
|
+
if (choices.length === 0 || live === null) return;
|
|
1007
|
+
event.preventDefault();
|
|
1008
|
+
const index = choices.findIndex((choice) => choice.effort === effectiveEffort);
|
|
1009
|
+
const step = event.key === "ArrowRight" ? 1 : -1;
|
|
1010
|
+
const next = choices[((index === -1 ? 0 : index) + step + choices.length) % choices.length];
|
|
1011
|
+
if (next === void 0) return;
|
|
1012
|
+
const pending = planEffortCommit(live, next.effort);
|
|
1013
|
+
if (pending !== null) submit(pending, null);
|
|
1014
|
+
};
|
|
196
1015
|
const onRootKeyDown = (event) => {
|
|
197
1016
|
if (event.key === "Escape" && open) {
|
|
198
1017
|
event.preventDefault();
|
|
199
|
-
|
|
200
|
-
else close(true);
|
|
1018
|
+
closeMenu(true);
|
|
201
1019
|
return;
|
|
202
1020
|
}
|
|
203
1021
|
if (!open) return;
|
|
204
1022
|
if (event.key === "Tab") {
|
|
205
1023
|
if (event.shiftKey) {
|
|
206
1024
|
event.preventDefault();
|
|
207
|
-
|
|
208
|
-
else close(true);
|
|
1025
|
+
closeMenu(true);
|
|
209
1026
|
return;
|
|
210
1027
|
}
|
|
211
1028
|
const focused = document.activeElement;
|
|
212
|
-
const rows =
|
|
1029
|
+
const rows = rowButtons();
|
|
213
1030
|
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
214
1031
|
event.preventDefault();
|
|
215
1032
|
focused.click();
|
|
@@ -217,7 +1034,7 @@ window.__ModuleLoader__.load({
|
|
|
217
1034
|
}
|
|
218
1035
|
if (focused !== triggerRef.current) return;
|
|
219
1036
|
event.preventDefault();
|
|
220
|
-
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])
|
|
1037
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? rows[0])?.focus();
|
|
221
1038
|
return;
|
|
222
1039
|
}
|
|
223
1040
|
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
@@ -225,73 +1042,28 @@ window.__ModuleLoader__.load({
|
|
|
225
1042
|
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
226
1043
|
}
|
|
227
1044
|
};
|
|
1045
|
+
/**
|
|
1046
|
+
* The keyboard leaving the seat closes the menu — but that is not one of the
|
|
1047
|
+
* four closing gestures that write a mark, so it closes without committing.
|
|
1048
|
+
* Otherwise clicking the notice text or a divider inside the menu, which moves
|
|
1049
|
+
* focus without unmounting it, would land a level nobody asked to commit.
|
|
1050
|
+
*/
|
|
228
1051
|
const onBlur = (event) => {
|
|
1052
|
+
if (pickerOpen) return;
|
|
229
1053
|
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return;
|
|
230
|
-
|
|
231
|
-
};
|
|
232
|
-
const settleSelection = (result) => {
|
|
233
|
-
if (result === void 0) return;
|
|
234
|
-
if (result.ok) {
|
|
235
|
-
if (rootRef.current !== null) close(true);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
const { error } = result;
|
|
239
|
-
toastSeq.current += 1;
|
|
240
|
-
setToast({
|
|
241
|
-
seq: toastSeq.current,
|
|
242
|
-
text: error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${error.code}: ${error.message}` })
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
const submit = (selection) => {
|
|
246
|
-
lastActionRef.current = "select";
|
|
247
|
-
select(selection).then(settleSelection);
|
|
248
|
-
};
|
|
249
|
-
/** A model commits outright; its reasoning levels stay behind the root's effort cell. */
|
|
250
|
-
const chooseModel = (provider, model) => {
|
|
251
|
-
if (current?.provider === provider && current.model === model.id) {
|
|
252
|
-
close(true);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
submit({
|
|
256
|
-
provider,
|
|
257
|
-
model: model.id
|
|
258
|
-
});
|
|
1054
|
+
setOpen(false);
|
|
259
1055
|
};
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
if (effectiveEffort === effort) {
|
|
263
|
-
close(true);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
submit({
|
|
267
|
-
provider: current.provider,
|
|
268
|
-
model: current.model,
|
|
269
|
-
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
270
|
-
});
|
|
1056
|
+
const toggleFavoriteOf = (provider, model) => {
|
|
1057
|
+
setFavorites(toggleFavorite(storageOf(), provider, model));
|
|
271
1058
|
};
|
|
272
1059
|
const waiting = current === null && state.status === "loading";
|
|
273
1060
|
const modelLabel = waiting ? t("trigger.loading") : currentModel?.name ?? (current === null ? t("trigger.fallback") : `${current.provider}/${current.model}`);
|
|
274
|
-
const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
|
|
275
1061
|
const triggerAria = waiting ? t("trigger.loading") : current === null ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: modelLabel }) : t("trigger.ariaEffort", {
|
|
276
1062
|
model: modelLabel,
|
|
277
1063
|
effort: effortLabel
|
|
278
1064
|
});
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
if (node === null) itemRefs.current.delete(key);
|
|
282
|
-
else itemRefs.current.set(key, node);
|
|
283
|
-
};
|
|
284
|
-
const backRow = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
285
|
-
ref: itemRef("back"),
|
|
286
|
-
type: "button",
|
|
287
|
-
role: "menuitem",
|
|
288
|
-
className: ModelSubmenu_module_css_default.cell,
|
|
289
|
-
onClick: back,
|
|
290
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutlineRegular, { className: ModelSubmenu_module_css_default.cellChevron }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
291
|
-
className: ModelSubmenu_module_css_default.cellLabel,
|
|
292
|
-
children: t("back")
|
|
293
|
-
})]
|
|
294
|
-
});
|
|
1065
|
+
const providerName = currentGroup?.name ?? state.failures.find((failure) => failure.id === current?.provider)?.name ?? current?.provider ?? "";
|
|
1066
|
+
const providerFailure = current === null ? void 0 : state.failures.find((failure) => failure.id === current.provider);
|
|
295
1067
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
296
1068
|
ref: rootRef,
|
|
297
1069
|
className: ModelSubmenu_module_css_default.root,
|
|
@@ -306,14 +1078,14 @@ window.__ModuleLoader__.load({
|
|
|
306
1078
|
"aria-haspopup": "menu",
|
|
307
1079
|
"aria-expanded": open,
|
|
308
1080
|
"aria-controls": open ? `${id}-menu` : void 0,
|
|
309
|
-
title:
|
|
1081
|
+
title: effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`,
|
|
310
1082
|
disabled: locked,
|
|
311
1083
|
onClick: () => {
|
|
312
|
-
if (open)
|
|
1084
|
+
if (open) closeMenu(false);
|
|
313
1085
|
else show();
|
|
314
1086
|
},
|
|
315
1087
|
children: [
|
|
316
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.
|
|
1088
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {
|
|
317
1089
|
className: ModelSubmenu_module_css_default.triggerIcon,
|
|
318
1090
|
size: 16
|
|
319
1091
|
}),
|
|
@@ -333,171 +1105,127 @@ window.__ModuleLoader__.load({
|
|
|
333
1105
|
id: `${id}-menu`,
|
|
334
1106
|
className: ModelSubmenu_module_css_default.menu,
|
|
335
1107
|
style: menuPos ?? MEASURE_STYLE,
|
|
336
|
-
role: "
|
|
337
|
-
"aria-label":
|
|
1108
|
+
role: "group",
|
|
1109
|
+
"aria-label": t("menu.aria"),
|
|
338
1110
|
"aria-busy": state.status === "loading" || busy,
|
|
339
1111
|
children: [
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
]
|
|
357
|
-
}),
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
1112
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1113
|
+
className: ModelSubmenu_module_css_default.providerName,
|
|
1114
|
+
title: providerName,
|
|
1115
|
+
children: providerName
|
|
1116
|
+
}),
|
|
1117
|
+
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1118
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
1119
|
+
children: t("status.loading")
|
|
1120
|
+
}),
|
|
1121
|
+
state.error !== null && state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1122
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
1123
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.failed", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1124
|
+
type: "button",
|
|
1125
|
+
className: ModelSubmenu_module_css_default.retry,
|
|
1126
|
+
onClick: reload,
|
|
1127
|
+
children: t("action.reload")
|
|
1128
|
+
})]
|
|
1129
|
+
}),
|
|
1130
|
+
providerFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1131
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
1132
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.providerFailed", {
|
|
1133
|
+
name: providerFailure.name,
|
|
1134
|
+
message: providerFailure.message
|
|
1135
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1136
|
+
type: "button",
|
|
1137
|
+
className: ModelSubmenu_module_css_default.retry,
|
|
1138
|
+
onClick: reload,
|
|
1139
|
+
children: t("action.reload")
|
|
1140
|
+
})]
|
|
1141
|
+
}),
|
|
1142
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1143
|
+
className: cls(ModelSubmenu_module_css_default.groups, "scrollable"),
|
|
1144
|
+
role: "menu",
|
|
1145
|
+
"aria-label": t("menu.aria"),
|
|
1146
|
+
children: [menuRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
|
|
1147
|
+
name: model.name,
|
|
1148
|
+
selected: current !== null && currentGroup !== void 0 && current.provider === currentGroup.id && current.model === model.id,
|
|
1149
|
+
busy,
|
|
1150
|
+
star: "static",
|
|
1151
|
+
favorited: currentGroup !== void 0 && isFavorite(favorites, currentGroup.id, model.id),
|
|
1152
|
+
onPick: () => {
|
|
1153
|
+
if (currentGroup !== void 0) chooseModel(currentGroup.id, model);
|
|
1154
|
+
}
|
|
1155
|
+
}, model.id)), menuRows.length === 0 && state.status === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1156
|
+
className: ModelSubmenu_module_css_default.notice,
|
|
1157
|
+
children: t("status.empty")
|
|
1158
|
+
})]
|
|
1159
|
+
}),
|
|
1160
|
+
choices.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider }),
|
|
1162
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1163
|
+
className: ModelSubmenu_module_css_default.effortRow,
|
|
1164
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1165
|
+
className: ModelSubmenu_module_css_default.effortHeading,
|
|
366
1166
|
children: t("menu.effort")
|
|
367
|
-
}),
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
]
|
|
374
|
-
})] }),
|
|
375
|
-
pane === "providers" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
|
|
376
|
-
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
377
|
-
className: ModelSubmenu_module_css_default.status,
|
|
378
|
-
children: t("status.loading")
|
|
379
|
-
}),
|
|
380
|
-
state.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
381
|
-
className: ModelSubmenu_module_css_default.error,
|
|
382
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
383
|
-
type: "button",
|
|
384
|
-
className: ModelSubmenu_module_css_default.retry,
|
|
385
|
-
onClick: reload,
|
|
386
|
-
children: t("action.reload")
|
|
387
|
-
})]
|
|
388
|
-
}),
|
|
389
|
-
state.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
390
|
-
className: ModelSubmenu_module_css_default.warning,
|
|
391
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("warning.groupLoad", {
|
|
392
|
-
name: failure.name,
|
|
393
|
-
message: failure.message
|
|
394
|
-
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
395
|
-
type: "button",
|
|
396
|
-
className: ModelSubmenu_module_css_default.retry,
|
|
397
|
-
onClick: reload,
|
|
398
|
-
children: t("action.reload")
|
|
399
|
-
})]
|
|
400
|
-
}, failure.id)),
|
|
401
|
-
state.status === "ready" && groups.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
402
|
-
className: ModelSubmenu_module_css_default.empty,
|
|
403
|
-
children: t("empty.models")
|
|
404
|
-
}),
|
|
405
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
406
|
-
className: cls(ModelSubmenu_module_css_default.groups, "scrollable"),
|
|
407
|
-
children: groups.map((group) => {
|
|
408
|
-
const mine = current?.provider === group.id;
|
|
409
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
410
|
-
ref: itemRef(`provider:${group.id}`),
|
|
1167
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1168
|
+
className: ModelSubmenu_module_css_default.effortTrack,
|
|
1169
|
+
role: "radiogroup",
|
|
1170
|
+
"aria-label": t("menu.effort"),
|
|
1171
|
+
onKeyDown: onEffortKeyDown,
|
|
1172
|
+
children: choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
411
1173
|
type: "button",
|
|
412
|
-
role: "
|
|
413
|
-
"aria-
|
|
414
|
-
className: ModelSubmenu_module_css_default.
|
|
415
|
-
|
|
1174
|
+
role: "radio",
|
|
1175
|
+
"aria-checked": choice.effort === effectiveEffort,
|
|
1176
|
+
className: ModelSubmenu_module_css_default.effortSegment,
|
|
1177
|
+
disabled: busy,
|
|
416
1178
|
onClick: () => {
|
|
417
|
-
|
|
418
|
-
|
|
1179
|
+
if (live !== null) {
|
|
1180
|
+
const pending = planEffortCommit(live, choice.effort);
|
|
1181
|
+
if (pending !== null) submit(pending, null);
|
|
1182
|
+
}
|
|
419
1183
|
},
|
|
420
|
-
children:
|
|
421
|
-
|
|
422
|
-
className: ModelSubmenu_module_css_default.cellLabel,
|
|
423
|
-
children: group.name
|
|
424
|
-
}),
|
|
425
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
426
|
-
className: ModelSubmenu_module_css_default.cellValue,
|
|
427
|
-
children: mine ? currentModel?.name ?? current?.model ?? "" : t("provider.modelCount", { count: String(group.models.length) })
|
|
428
|
-
}),
|
|
429
|
-
mine && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
430
|
-
className: ModelSubmenu_module_css_default.check,
|
|
431
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {})
|
|
432
|
-
}),
|
|
433
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { className: ModelSubmenu_module_css_default.cellChevron })
|
|
434
|
-
]
|
|
435
|
-
}, group.id);
|
|
436
|
-
})
|
|
437
|
-
})
|
|
438
|
-
] }),
|
|
439
|
-
pane === "models" && shownGroup !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [backRow(), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
440
|
-
className: cls(ModelSubmenu_module_css_default.groups, "scrollable"),
|
|
441
|
-
children: shownGroup.models.map((model) => {
|
|
442
|
-
const selected = current?.provider === shownGroup.id && current.model === model.id;
|
|
443
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
444
|
-
ref: itemRef(`model:${shownGroup.id}/${model.id}`),
|
|
445
|
-
type: "button",
|
|
446
|
-
role: "menuitemradio",
|
|
447
|
-
"aria-checked": selected,
|
|
448
|
-
className: cls(ModelSubmenu_module_css_default.option, selected && ModelSubmenu_module_css_default.selected),
|
|
449
|
-
title: model.name,
|
|
450
|
-
disabled: busy,
|
|
451
|
-
onClick: () => chooseModel(shownGroup.id, model),
|
|
452
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
453
|
-
className: ModelSubmenu_module_css_default.optionCopy,
|
|
454
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
455
|
-
className: ModelSubmenu_module_css_default.modelName,
|
|
456
|
-
children: model.name
|
|
457
|
-
})
|
|
458
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
459
|
-
className: ModelSubmenu_module_css_default.check,
|
|
460
|
-
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
461
|
-
})]
|
|
462
|
-
}, model.id);
|
|
463
|
-
})
|
|
464
|
-
})] }),
|
|
465
|
-
pane === "effort" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
|
|
466
|
-
backRow(),
|
|
467
|
-
state.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
468
|
-
className: ModelSubmenu_module_css_default.error,
|
|
469
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
470
|
-
type: "button",
|
|
471
|
-
className: ModelSubmenu_module_css_default.retry,
|
|
472
|
-
onClick: reload,
|
|
473
|
-
children: t("action.reload")
|
|
1184
|
+
children: choice.label
|
|
1185
|
+
}, choice.key))
|
|
474
1186
|
})]
|
|
475
1187
|
}),
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
494
|
-
className: ModelSubmenu_module_css_default.check,
|
|
495
|
-
children: effectiveEffort === level.effort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
496
|
-
})]
|
|
497
|
-
}, level.key))
|
|
498
|
-
] })
|
|
1188
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider })
|
|
1189
|
+
] }),
|
|
1190
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1191
|
+
className: ModelSubmenu_module_css_default.hint,
|
|
1192
|
+
children: t("menu.hint")
|
|
1193
|
+
}),
|
|
1194
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider }),
|
|
1195
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1196
|
+
type: "button",
|
|
1197
|
+
role: "menuitem",
|
|
1198
|
+
className: ModelSubmenu_module_css_default.moreRow,
|
|
1199
|
+
onClick: openPicker,
|
|
1200
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {
|
|
1201
|
+
className: ModelSubmenu_module_css_default.moreIcon,
|
|
1202
|
+
size: 16
|
|
1203
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("menu.more") })]
|
|
1204
|
+
})
|
|
499
1205
|
]
|
|
500
1206
|
}), document.body),
|
|
1207
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
1208
|
+
open: pickerOpen,
|
|
1209
|
+
state,
|
|
1210
|
+
live,
|
|
1211
|
+
facts,
|
|
1212
|
+
favorites,
|
|
1213
|
+
busy,
|
|
1214
|
+
t,
|
|
1215
|
+
onPick: (selection) => {
|
|
1216
|
+
submit(selection, () => {
|
|
1217
|
+
setPickerOpen(false);
|
|
1218
|
+
});
|
|
1219
|
+
},
|
|
1220
|
+
onToggleFavorite: toggleFavoriteOf,
|
|
1221
|
+
onReload: reload,
|
|
1222
|
+
onClose: () => {
|
|
1223
|
+
setPickerOpen(false);
|
|
1224
|
+
queueMicrotask(() => {
|
|
1225
|
+
triggerRef.current?.focus();
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}),
|
|
501
1229
|
toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
502
1230
|
text: toast.text,
|
|
503
1231
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
|
|
@@ -509,67 +1237,160 @@ window.__ModuleLoader__.load({
|
|
|
509
1237
|
]
|
|
510
1238
|
});
|
|
511
1239
|
}
|
|
1240
|
+
/** The selection in force, or null while the host has not resolved one. */
|
|
1241
|
+
function selectionOf(state) {
|
|
1242
|
+
if (state.current === null) return null;
|
|
1243
|
+
return state.current.reasoningEffort === void 0 ? {
|
|
1244
|
+
provider: state.current.provider,
|
|
1245
|
+
model: state.current.model
|
|
1246
|
+
} : {
|
|
1247
|
+
provider: state.current.provider,
|
|
1248
|
+
model: state.current.model,
|
|
1249
|
+
reasoningEffort: state.current.reasoningEffort
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
/** Isolate the browser's storage so a locked-down browser reads as "no favorites". */
|
|
1253
|
+
function storageOf() {
|
|
1254
|
+
try {
|
|
1255
|
+
return globalThis.localStorage;
|
|
1256
|
+
} catch {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/client/facts-source.ts
|
|
1262
|
+
const EMPTY = () => ({
|
|
1263
|
+
contextWindow: void 0,
|
|
1264
|
+
images: "unknown"
|
|
1265
|
+
});
|
|
1266
|
+
/**
|
|
1267
|
+
* Join the settings mirror with the provider directory.
|
|
1268
|
+
* @param deps.describe - the settings mirror's describe face.
|
|
1269
|
+
* @param deps.listProviders - reads the LLM provider directory once.
|
|
1270
|
+
* @returns the source, plus `start` which wires both reads and returns its disposer.
|
|
1271
|
+
*/
|
|
1272
|
+
function createFactsSource(deps) {
|
|
1273
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1274
|
+
let providers = [];
|
|
1275
|
+
let lookup = EMPTY;
|
|
1276
|
+
const rebuild = () => {
|
|
1277
|
+
const namespaces = deps.describe.getSnapshot().view?.namespaces ?? [];
|
|
1278
|
+
lookup = providers.length === 0 || namespaces.length === 0 ? EMPTY : buildModelFacts(providers, namespaces);
|
|
1279
|
+
for (const listener of [...listeners]) listener();
|
|
1280
|
+
};
|
|
1281
|
+
return {
|
|
1282
|
+
source: {
|
|
1283
|
+
getSnapshot: () => lookup,
|
|
1284
|
+
subscribe: (listener) => {
|
|
1285
|
+
listeners.add(listener);
|
|
1286
|
+
return () => {
|
|
1287
|
+
listeners.delete(listener);
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1291
|
+
start: () => {
|
|
1292
|
+
const off = deps.describe.subscribe(rebuild);
|
|
1293
|
+
deps.describe.ensure().catch(() => {});
|
|
1294
|
+
deps.listProviders().then((answer) => {
|
|
1295
|
+
providers = answer.ok && answer.value !== void 0 ? answer.value : [];
|
|
1296
|
+
rebuild();
|
|
1297
|
+
}, () => {});
|
|
1298
|
+
rebuild();
|
|
1299
|
+
return () => {
|
|
1300
|
+
off();
|
|
1301
|
+
listeners.clear();
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
512
1306
|
//#endregion
|
|
513
1307
|
//#region src/client/locales.ts
|
|
514
|
-
/** Locale bundles for the composer model
|
|
1308
|
+
/** Locale bundles for the composer model menu and the picker it opens. */
|
|
515
1309
|
/** Dictionary namespace owned by this plugin. */
|
|
516
1310
|
const NS = "modelSubmenu";
|
|
517
1311
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
518
1312
|
const zh = {
|
|
519
|
-
"back": "返回",
|
|
520
|
-
"provider.modelCount": "{count} 个模型",
|
|
521
|
-
"action.reload": "重新加载",
|
|
522
1313
|
"trigger.fallback": "选择模型",
|
|
523
1314
|
"trigger.loading": "正在加载模型…",
|
|
524
1315
|
"trigger.selectAria": "选择模型",
|
|
525
1316
|
"trigger.aria": "选择模型,当前 {model}",
|
|
526
|
-
"trigger.ariaEffort": "选择模型,当前 {model}
|
|
527
|
-
"menu.aria": "
|
|
528
|
-
"menu.
|
|
529
|
-
"menu.
|
|
530
|
-
"
|
|
531
|
-
"
|
|
1317
|
+
"trigger.ariaEffort": "选择模型,当前 {model},思考等级 {effort}",
|
|
1318
|
+
"menu.aria": "模型与思考等级",
|
|
1319
|
+
"menu.effort": "思考",
|
|
1320
|
+
"menu.more": "更多模型…",
|
|
1321
|
+
"menu.hint": "提示:切换模型或思考程度会使既有的提示词缓存失效。建议新建会话,避免额外的 token 消耗。",
|
|
1322
|
+
"effort.providerDefault": "默认",
|
|
1323
|
+
"status.loading": "正在加载模型…",
|
|
1324
|
+
"status.empty": "没有可用的模型。",
|
|
1325
|
+
"status.failed": "模型列表加载失败:{message}",
|
|
1326
|
+
"status.providerFailed": "{name} 加载失败:{message}",
|
|
1327
|
+
"action.reload": "重新加载",
|
|
532
1328
|
"error.action": "模型操作失败:{message}",
|
|
533
1329
|
"error.sessionInUse": "当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。",
|
|
534
|
-
"
|
|
535
|
-
"
|
|
536
|
-
"
|
|
1330
|
+
"picker.title": "切换模型",
|
|
1331
|
+
"picker.close": "关闭",
|
|
1332
|
+
"picker.search": "搜索模型或提供商…",
|
|
1333
|
+
"picker.searchEmpty": "没有匹配的模型。",
|
|
1334
|
+
"picker.all": "全部",
|
|
1335
|
+
"picker.favorite": "收藏 {model}",
|
|
1336
|
+
"picker.unfavorite": "取消收藏 {model}",
|
|
1337
|
+
"picker.hintNavigate": "导航",
|
|
1338
|
+
"picker.hintSelect": "选择",
|
|
1339
|
+
"picker.hintClose": "关闭",
|
|
1340
|
+
"meta.effort": "思考",
|
|
1341
|
+
"meta.images": "图片输入"
|
|
537
1342
|
};
|
|
538
1343
|
/** English dictionary, checked complete against the zh key set. */
|
|
539
1344
|
const en = {
|
|
540
|
-
"back": "Back",
|
|
541
|
-
"provider.modelCount": "{count} models",
|
|
542
|
-
"action.reload": "Reload",
|
|
543
1345
|
"trigger.fallback": "Select model",
|
|
544
1346
|
"trigger.loading": "Loading models…",
|
|
545
1347
|
"trigger.selectAria": "Select model",
|
|
546
1348
|
"trigger.aria": "Select model, current {model}",
|
|
547
|
-
"trigger.ariaEffort": "Select model, current {model},
|
|
548
|
-
"menu.aria": "
|
|
549
|
-
"menu.provider": "Provider",
|
|
1349
|
+
"trigger.ariaEffort": "Select model, current {model}, effort {effort}",
|
|
1350
|
+
"menu.aria": "Model and effort",
|
|
550
1351
|
"menu.effort": "Effort",
|
|
1352
|
+
"menu.more": "More models…",
|
|
1353
|
+
"menu.hint": "Changing the model or the effort invalidates the existing prompt cache. Start a new session to avoid the extra token cost.",
|
|
551
1354
|
"effort.providerDefault": "Default",
|
|
552
|
-
"status.loading": "
|
|
1355
|
+
"status.loading": "Loading models…",
|
|
1356
|
+
"status.empty": "No models available.",
|
|
1357
|
+
"status.failed": "Model list failed to load: {message}",
|
|
1358
|
+
"status.providerFailed": "{name} failed to load: {message}",
|
|
1359
|
+
"action.reload": "Reload",
|
|
553
1360
|
"error.action": "Model operation failed: {message}",
|
|
554
1361
|
"error.sessionInUse": "This session is already in use, possibly by another running DSH instance (such as dsh web or the desktop app). Quit other running DSH instances and try again.",
|
|
555
|
-
"
|
|
556
|
-
"
|
|
557
|
-
"
|
|
1362
|
+
"picker.title": "Switch model",
|
|
1363
|
+
"picker.close": "Close",
|
|
1364
|
+
"picker.search": "Search models or providers…",
|
|
1365
|
+
"picker.searchEmpty": "No matching models.",
|
|
1366
|
+
"picker.all": "All",
|
|
1367
|
+
"picker.favorite": "Favorite {model}",
|
|
1368
|
+
"picker.unfavorite": "Remove {model} from favorites",
|
|
1369
|
+
"picker.hintNavigate": "navigate",
|
|
1370
|
+
"picker.hintSelect": "select",
|
|
1371
|
+
"picker.hintClose": "close",
|
|
1372
|
+
"meta.effort": "Effort",
|
|
1373
|
+
"meta.images": "Images"
|
|
558
1374
|
};
|
|
559
1375
|
//#endregion
|
|
560
1376
|
//#region src/client/index.ts
|
|
561
1377
|
/**
|
|
562
1378
|
* Cordis services required by the browser half. `slots` is the contribution
|
|
563
|
-
* registry, `sessions` tells whether a session may select a model,
|
|
1379
|
+
* registry, `sessions` tells whether a session may select a model,
|
|
564
1380
|
* `modelDirectories` owns the per-session catalog snapshot this seat renders
|
|
565
|
-
* from
|
|
566
|
-
*
|
|
567
|
-
*
|
|
1381
|
+
* from, and `configForms` carries the settings provider's shared describe
|
|
1382
|
+
* mirror, which answers each model's context window and accepted modalities.
|
|
1383
|
+
* `remote`, `remote.llm`, and `remote.session` are declared because a service
|
|
1384
|
+
* method runs on the calling context: `directoryFor` reads
|
|
1385
|
+
* `this.ctx.remote.session`, and the provider directory that names each
|
|
1386
|
+
* settings address is read through `this.ctx.remote.llm`.
|
|
568
1387
|
*/
|
|
569
1388
|
const inject = [
|
|
1389
|
+
"configForms",
|
|
570
1390
|
"locale",
|
|
571
1391
|
"modelDirectories",
|
|
572
1392
|
"remote",
|
|
1393
|
+
"remote.llm",
|
|
573
1394
|
"remote.session",
|
|
574
1395
|
"sessions",
|
|
575
1396
|
"slots"
|
|
@@ -585,9 +1406,19 @@ window.__ModuleLoader__.load({
|
|
|
585
1406
|
zh,
|
|
586
1407
|
en
|
|
587
1408
|
}), "dsh-model-submenu: dictionaries");
|
|
588
|
-
ctx.inject([
|
|
1409
|
+
ctx.inject([
|
|
1410
|
+
"slots",
|
|
1411
|
+
"modelDirectories",
|
|
1412
|
+
"configForms",
|
|
1413
|
+
"remote"
|
|
1414
|
+
], (scope) => {
|
|
589
1415
|
const models = scope.modelDirectories;
|
|
590
1416
|
const sessions = scope.sessions;
|
|
1417
|
+
const facts = createFactsSource({
|
|
1418
|
+
describe: scope.configForms.describe(),
|
|
1419
|
+
listProviders: () => scope.remote.llm.listConfigurableProviders()
|
|
1420
|
+
});
|
|
1421
|
+
ctx.effect(() => facts.start(), "dsh-model-submenu: model metadata");
|
|
591
1422
|
scope.slots.inject("conversation.input.model", () => scope.slots.register({
|
|
592
1423
|
name: "conversation.input.model",
|
|
593
1424
|
priority: -100,
|
|
@@ -600,6 +1431,7 @@ window.__ModuleLoader__.load({
|
|
|
600
1431
|
return {
|
|
601
1432
|
available,
|
|
602
1433
|
directory: directory.store,
|
|
1434
|
+
factsSource: facts.source,
|
|
603
1435
|
load: () => {
|
|
604
1436
|
if (available) directory.load().catch(() => {});
|
|
605
1437
|
},
|