@lynn123411/dsh-model-submenu 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +620 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +5 -0
- package/package.json +84 -0
- package/src/client/ModelSubmenu.module.css +247 -0
- package/src/client/ModelSubmenu.tsx +426 -0
- package/src/client/index.ts +59 -0
- package/src/client/locales.ts +76 -0
- package/src/css-modules.d.ts +4 -0
- package/src/index.ts +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @lynn123411/dsh-model-submenu
|
|
2
|
+
|
|
3
|
+
给输入框里的模型选择器加上供应商层级:先在供应商里挑,再挑该供应商的模型。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **一级仍是原生两行**:根面板保持内置弹层的两行结构,「供应商」取代原来的「模型」,右侧显示当前供应商名;「推理等级」行原样保留。
|
|
8
|
+
- **第二级按供应商**:点「供应商」进入供应商列表;当前供应商那行显示它正在使用的模型名并带勾选,其余行显示该供应商的模型数量,顺序沿用 Host 给出的顺序。
|
|
9
|
+
- **第三级按模型**:点某个供应商,同一面板内换成它的模型列表,面板顶部是返回行;点模型立即提交并关闭。
|
|
10
|
+
- **推理等级照原生**:根面板的「推理等级」行进入当前模型的等级列表,选中即提交,「Default」行表示跟随供应商默认;当前模型没有推理等级时不显示该行。
|
|
11
|
+
- **失败与加载态照旧**:目录加载中显示刷新提示,加载失败与供应商加载失败都在供应商列表顶部给出提示与「重新加载」;提交被拒时弹出与内置同款的提示气泡。
|
|
12
|
+
- **键鼠行为照旧**:方向键在行间移动、Tab 等同回车、Escape 与 Shift+Tab 先退回上一层、在根层才关闭;输入框过窄时触发器只显示图标。
|
|
13
|
+
- **接管内置座位**:以 `priority: -100` 占据 `conversation.input.model`,外观沿用内置弹层与 `--dsw-*` 主题变量;`/model` 命令面板与设置页不受影响。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
dsh plugin --profile web add @lynn123411/dsh-model-submenu
|
|
19
|
+
```
|
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@lynn123411/dsh-model-submenu",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_dom = require("react-dom");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
//#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-label-caption);flex-shrink:1000;overflow:hidden}.FOsK3a_triggerIcon{flex:none;display:none}@container (width<=360px){.FOsK3a_triggerIcon{display:block}.FOsK3a_triggerLabel,.FOsK3a_triggerEffort{display:none}}.FOsK3a_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}.FOsK3a_chevronOpen{transform:rotate(180deg)}.FOsK3a_menu{z-index:1100;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);width:max-content;min-width:min(240px,100vw - 32px);max-width:min(420px,100vw - 32px);max-height:min(360px,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;border-radius:20px;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}.FOsK3a_status,.FOsK3a_empty{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}.FOsK3a_error,.FOsK3a_warning{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}.FOsK3a_warning{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-state-warn-label)}.FOsK3a_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}.FOsK3a_groups{min-height:0;overflow-y:auto}.FOsK3a_option{box-sizing:border-box;width:auto;min-width:100%;min-height:38px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:8px;padding:6px 8px;display:flex}.FOsK3a_option:hover:not(:disabled),.FOsK3a_option:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_selected{background:0 0}.FOsK3a_option:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_optionCopy{flex-direction:column;flex:1;min-width:0;display:flex}.FOsK3a_modelName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:14px;font-weight:500;line-height:20px;overflow:hidden}.FOsK3a_check{color:var(--dsw-alias-label-primary);flex:0 0 18px;place-items:center;display:grid}.FOsK3a_cell{box-sizing:border-box;width:auto;min-width:100%;height:40px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:0 10px;font-size:14px;line-height:22px;display:flex}.FOsK3a_cell:hover{background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_cellLabel{white-space:nowrap;flex:none}.FOsK3a_cellValue{text-overflow:ellipsis;white-space:nowrap;text-align:right;min-width:0;color:var(--dsw-alias-label-tertiary);flex:auto;overflow:hidden}.FOsK3a_cellChevron{color:var(--dsw-alias-label-tertiary);flex:none}";
|
|
13
|
+
const tagId = "@lynn123411/dsh-model-submenu/ModelSubmenu.module.css";
|
|
14
|
+
if (typeof document !== "undefined") {
|
|
15
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
16
|
+
if (tag === null) {
|
|
17
|
+
tag = document.createElement("style");
|
|
18
|
+
tag.dataset.plugin = "@lynn123411/dsh-model-submenu";
|
|
19
|
+
tag.dataset.pluginCss = tagId;
|
|
20
|
+
document.head.appendChild(tag);
|
|
21
|
+
}
|
|
22
|
+
tag.textContent = css;
|
|
23
|
+
}
|
|
24
|
+
var ModelSubmenu_module_css_default = {
|
|
25
|
+
"cell": "FOsK3a_cell",
|
|
26
|
+
"cellChevron": "FOsK3a_cellChevron",
|
|
27
|
+
"cellLabel": "FOsK3a_cellLabel",
|
|
28
|
+
"cellValue": "FOsK3a_cellValue",
|
|
29
|
+
"check": "FOsK3a_check",
|
|
30
|
+
"chevron": "FOsK3a_chevron",
|
|
31
|
+
"chevronOpen": "FOsK3a_chevronOpen",
|
|
32
|
+
"empty": "FOsK3a_empty",
|
|
33
|
+
"error": "FOsK3a_error",
|
|
34
|
+
"groups": "FOsK3a_groups",
|
|
35
|
+
"menu": "FOsK3a_menu",
|
|
36
|
+
"modelName": "FOsK3a_modelName",
|
|
37
|
+
"option": "FOsK3a_option",
|
|
38
|
+
"optionCopy": "FOsK3a_optionCopy",
|
|
39
|
+
"retry": "FOsK3a_retry",
|
|
40
|
+
"root": "FOsK3a_root",
|
|
41
|
+
"selected": "FOsK3a_selected",
|
|
42
|
+
"status": "FOsK3a_status",
|
|
43
|
+
"trigger": "FOsK3a_trigger",
|
|
44
|
+
"triggerEffort": "FOsK3a_triggerEffort",
|
|
45
|
+
"triggerIcon": "FOsK3a_triggerIcon",
|
|
46
|
+
"triggerLabel": "FOsK3a_triggerLabel",
|
|
47
|
+
"warning": "FOsK3a_warning"
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/client/ModelSubmenu.tsx
|
|
51
|
+
/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real. */
|
|
52
|
+
const MEASURE_STYLE = {
|
|
53
|
+
visibility: "hidden",
|
|
54
|
+
left: 0,
|
|
55
|
+
top: 0
|
|
56
|
+
};
|
|
57
|
+
const cls = (...parts) => parts.filter((part) => part !== false && part !== void 0).join(" ");
|
|
58
|
+
/**
|
|
59
|
+
* The composer's model seat (`conversation.input.model`), taken over from the
|
|
60
|
+
* built-in occupant with a provider level: the root pane keeps the built-in's
|
|
61
|
+
* two cells — 供应商 in place of 模型, plus the native 推理等级 — the provider
|
|
62
|
+
* cell drills into the catalog's providers, and a provider drills into its
|
|
63
|
+
* models before submitting. The trigger, the menu surface, the keyboard
|
|
64
|
+
* contract (arrows move, Tab settles, Escape and Shift+Tab leave a drilled pane
|
|
65
|
+
* first), the effort list and the failure surfaces are the built-in ones.
|
|
66
|
+
*/
|
|
67
|
+
function ModelSubmenu({ locked, available, directory, load, select, t }) {
|
|
68
|
+
const state = (0, react.useSyncExternalStore)((fn) => directory.subscribe(fn), () => directory.getSnapshot());
|
|
69
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
70
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
71
|
+
const [providerId, setProviderId] = (0, react.useState)(null);
|
|
72
|
+
const lastActionRef = (0, react.useRef)("load");
|
|
73
|
+
const [toast, setToast] = (0, react.useState)(null);
|
|
74
|
+
const toastSeq = (0, react.useRef)(0);
|
|
75
|
+
const rootRef = (0, react.useRef)(null);
|
|
76
|
+
const triggerRef = (0, react.useRef)(null);
|
|
77
|
+
const menuRef = (0, react.useRef)(null);
|
|
78
|
+
const [menuPos, setMenuPos] = (0, react.useState)(null);
|
|
79
|
+
const itemRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
80
|
+
const focusIntent = (0, react.useRef)(null);
|
|
81
|
+
const id = (0, react.useId)();
|
|
82
|
+
const groups = state.groups;
|
|
83
|
+
const current = state.current;
|
|
84
|
+
const currentGroup = current === null ? void 0 : groups.find((group) => group.id === current.provider);
|
|
85
|
+
const currentModel = currentGroup?.models.find((model) => model.id === current?.model);
|
|
86
|
+
const reasoning = currentModel?.reasoning;
|
|
87
|
+
const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
88
|
+
const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
|
|
89
|
+
const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
|
|
90
|
+
key: "provider-default",
|
|
91
|
+
effort: void 0,
|
|
92
|
+
label: t("effort.providerDefault")
|
|
93
|
+
}] : [], ...reasoning.efforts.map((level) => ({
|
|
94
|
+
key: `effort:${level.id}`,
|
|
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;
|
|
99
|
+
const busy = state.status === "selecting";
|
|
100
|
+
const reload = () => {
|
|
101
|
+
lastActionRef.current = "load";
|
|
102
|
+
load();
|
|
103
|
+
};
|
|
104
|
+
(0, react.useEffect)(() => {
|
|
105
|
+
if (!open) return;
|
|
106
|
+
const closeOutside = (event) => {
|
|
107
|
+
if (rootRef.current?.contains(event.target) === true) return;
|
|
108
|
+
if (menuRef.current?.contains(event.target) === true) return;
|
|
109
|
+
setOpen(false);
|
|
110
|
+
setPane("root");
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener("mousedown", closeOutside);
|
|
113
|
+
return () => {
|
|
114
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
115
|
+
};
|
|
116
|
+
}, [open]);
|
|
117
|
+
(0, react.useEffect)(() => {
|
|
118
|
+
const intent = focusIntent.current;
|
|
119
|
+
focusIntent.current = null;
|
|
120
|
+
if (!open || intent === null) return;
|
|
121
|
+
if (intent === "initial") {
|
|
122
|
+
const checked = menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled]), [role=\"menuitem\"][aria-current=\"true\"]:not([disabled])");
|
|
123
|
+
const first = [...itemRefs.current].find(([key]) => !key.startsWith("back:"))?.[1];
|
|
124
|
+
(checked ?? first ?? triggerRef.current)?.focus();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
(itemRefs.current.get(intent) ?? triggerRef.current)?.focus();
|
|
128
|
+
}, [open, pane]);
|
|
129
|
+
(0, react.useLayoutEffect)(() => {
|
|
130
|
+
if (!open) {
|
|
131
|
+
setMenuPos(null);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const place = () => {
|
|
135
|
+
const rect = triggerRef.current?.getBoundingClientRect();
|
|
136
|
+
if (rect === void 0) return;
|
|
137
|
+
const MARGIN = 12;
|
|
138
|
+
const lw = menuRef.current?.offsetWidth ?? 0;
|
|
139
|
+
const lh = menuRef.current?.offsetHeight ?? 0;
|
|
140
|
+
let x = rect.right - lw;
|
|
141
|
+
let y = rect.top - 8 - lh;
|
|
142
|
+
if (lw > 0) x = Math.min(Math.max(x, MARGIN), window.innerWidth - lw - MARGIN);
|
|
143
|
+
if (lh > 0) y = Math.min(Math.max(y, MARGIN), window.innerHeight - lh - MARGIN);
|
|
144
|
+
setMenuPos({
|
|
145
|
+
left: x,
|
|
146
|
+
top: y
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
place();
|
|
150
|
+
window.addEventListener("scroll", place, true);
|
|
151
|
+
window.addEventListener("resize", place);
|
|
152
|
+
return () => {
|
|
153
|
+
window.removeEventListener("scroll", place, true);
|
|
154
|
+
window.removeEventListener("resize", place);
|
|
155
|
+
};
|
|
156
|
+
}, [
|
|
157
|
+
open,
|
|
158
|
+
pane,
|
|
159
|
+
state
|
|
160
|
+
]);
|
|
161
|
+
if (!available) return null;
|
|
162
|
+
const show = () => {
|
|
163
|
+
setPane("root");
|
|
164
|
+
setProviderId(null);
|
|
165
|
+
setOpen(true);
|
|
166
|
+
reload();
|
|
167
|
+
};
|
|
168
|
+
const close = (restoreFocus = false) => {
|
|
169
|
+
setOpen(false);
|
|
170
|
+
setPane("root");
|
|
171
|
+
setProviderId(null);
|
|
172
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
173
|
+
triggerRef.current?.focus();
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
const drill = (next) => {
|
|
177
|
+
focusIntent.current = "initial";
|
|
178
|
+
setPane(next);
|
|
179
|
+
};
|
|
180
|
+
/** Leave the shown pane for its parent, handing the keyboard back to the row that opened it. */
|
|
181
|
+
const back = () => {
|
|
182
|
+
if (pane === "models") {
|
|
183
|
+
focusIntent.current = providerId === null ? null : `provider:${providerId}`;
|
|
184
|
+
setPane("providers");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
focusIntent.current = pane === "effort" ? "root:effort" : "root:provider";
|
|
188
|
+
setPane("root");
|
|
189
|
+
};
|
|
190
|
+
const moveFocus = (offset) => {
|
|
191
|
+
const items = [...itemRefs.current.values()];
|
|
192
|
+
if (items.length === 0) return;
|
|
193
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
194
|
+
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
195
|
+
};
|
|
196
|
+
const onRootKeyDown = (event) => {
|
|
197
|
+
if (event.key === "Escape" && open) {
|
|
198
|
+
event.preventDefault();
|
|
199
|
+
if (pane !== "root") back();
|
|
200
|
+
else close(true);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!open) return;
|
|
204
|
+
if (event.key === "Tab") {
|
|
205
|
+
if (event.shiftKey) {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
if (pane !== "root") back();
|
|
208
|
+
else close(true);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const focused = document.activeElement;
|
|
212
|
+
const rows = [...itemRefs.current.values()];
|
|
213
|
+
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
214
|
+
event.preventDefault();
|
|
215
|
+
focused.click();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (focused !== triggerRef.current) return;
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled]), [role=\"menuitem\"][aria-current=\"true\"]:not([disabled])") ?? rows.find((item) => !item.disabled))?.focus();
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
224
|
+
event.preventDefault();
|
|
225
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const onBlur = (event) => {
|
|
229
|
+
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return;
|
|
230
|
+
close();
|
|
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
|
+
});
|
|
259
|
+
};
|
|
260
|
+
const chooseEffort = (effort) => {
|
|
261
|
+
if (current === null) return;
|
|
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
|
+
});
|
|
271
|
+
};
|
|
272
|
+
const waiting = current === null && state.status === "loading";
|
|
273
|
+
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
|
+
const triggerAria = waiting ? t("trigger.loading") : current === null ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: modelLabel }) : t("trigger.ariaEffort", {
|
|
276
|
+
model: modelLabel,
|
|
277
|
+
effort: effortLabel
|
|
278
|
+
});
|
|
279
|
+
itemRefs.current = /* @__PURE__ */ new Map();
|
|
280
|
+
const itemRef = (key) => (node) => {
|
|
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.IconChevronLeftOutline14, { 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
|
+
});
|
|
295
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
296
|
+
ref: rootRef,
|
|
297
|
+
className: ModelSubmenu_module_css_default.root,
|
|
298
|
+
onKeyDown: onRootKeyDown,
|
|
299
|
+
onBlur,
|
|
300
|
+
children: [
|
|
301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
302
|
+
ref: triggerRef,
|
|
303
|
+
type: "button",
|
|
304
|
+
className: ModelSubmenu_module_css_default.trigger,
|
|
305
|
+
"aria-label": triggerAria,
|
|
306
|
+
"aria-haspopup": "menu",
|
|
307
|
+
"aria-expanded": open,
|
|
308
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
309
|
+
title: triggerLabel,
|
|
310
|
+
disabled: locked,
|
|
311
|
+
onClick: () => {
|
|
312
|
+
if (open) close();
|
|
313
|
+
else show();
|
|
314
|
+
},
|
|
315
|
+
children: [
|
|
316
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutline16, {
|
|
317
|
+
className: ModelSubmenu_module_css_default.triggerIcon,
|
|
318
|
+
size: 16
|
|
319
|
+
}),
|
|
320
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
321
|
+
className: ModelSubmenu_module_css_default.triggerLabel,
|
|
322
|
+
children: modelLabel
|
|
323
|
+
}),
|
|
324
|
+
effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
325
|
+
className: ModelSubmenu_module_css_default.triggerEffort,
|
|
326
|
+
children: effortLabel
|
|
327
|
+
}),
|
|
328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: cls(ModelSubmenu_module_css_default.chevron, open && ModelSubmenu_module_css_default.chevronOpen) })
|
|
329
|
+
]
|
|
330
|
+
}),
|
|
331
|
+
open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
332
|
+
ref: menuRef,
|
|
333
|
+
id: `${id}-menu`,
|
|
334
|
+
className: ModelSubmenu_module_css_default.menu,
|
|
335
|
+
style: menuPos ?? MEASURE_STYLE,
|
|
336
|
+
role: "menu",
|
|
337
|
+
"aria-label": pane === "models" && shownGroup !== void 0 ? shownGroup.name : pane === "effort" ? t("menu.effort") : t("menu.aria"),
|
|
338
|
+
"aria-busy": state.status === "loading" || busy,
|
|
339
|
+
children: [
|
|
340
|
+
pane === "root" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
341
|
+
ref: itemRef("root:provider"),
|
|
342
|
+
type: "button",
|
|
343
|
+
role: "menuitem",
|
|
344
|
+
className: ModelSubmenu_module_css_default.cell,
|
|
345
|
+
onClick: () => drill("providers"),
|
|
346
|
+
children: [
|
|
347
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
348
|
+
className: ModelSubmenu_module_css_default.cellLabel,
|
|
349
|
+
children: t("menu.provider")
|
|
350
|
+
}),
|
|
351
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
352
|
+
className: ModelSubmenu_module_css_default.cellValue,
|
|
353
|
+
children: currentGroup?.name ?? current?.provider ?? ""
|
|
354
|
+
}),
|
|
355
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { className: ModelSubmenu_module_css_default.cellChevron })
|
|
356
|
+
]
|
|
357
|
+
}), reasoning !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
358
|
+
ref: itemRef("root:effort"),
|
|
359
|
+
type: "button",
|
|
360
|
+
role: "menuitem",
|
|
361
|
+
className: ModelSubmenu_module_css_default.cell,
|
|
362
|
+
onClick: () => drill("effort"),
|
|
363
|
+
children: [
|
|
364
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
365
|
+
className: ModelSubmenu_module_css_default.cellLabel,
|
|
366
|
+
children: t("menu.effort")
|
|
367
|
+
}),
|
|
368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
369
|
+
className: ModelSubmenu_module_css_default.cellValue,
|
|
370
|
+
children: effortLabel
|
|
371
|
+
}),
|
|
372
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { className: ModelSubmenu_module_css_default.cellChevron })
|
|
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}`),
|
|
411
|
+
type: "button",
|
|
412
|
+
role: "menuitem",
|
|
413
|
+
"aria-current": mine ? "true" : void 0,
|
|
414
|
+
className: ModelSubmenu_module_css_default.cell,
|
|
415
|
+
title: group.name,
|
|
416
|
+
onClick: () => {
|
|
417
|
+
setProviderId(group.id);
|
|
418
|
+
drill("models");
|
|
419
|
+
},
|
|
420
|
+
children: [
|
|
421
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
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.IconCheckOutline16, {})
|
|
432
|
+
}),
|
|
433
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, { 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.IconCheckOutline16, {}) : 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")
|
|
474
|
+
})]
|
|
475
|
+
}),
|
|
476
|
+
effortChoices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
477
|
+
className: ModelSubmenu_module_css_default.empty,
|
|
478
|
+
children: t("empty.efforts")
|
|
479
|
+
}) : effortChoices.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
480
|
+
ref: itemRef(`effort:${level.key}`),
|
|
481
|
+
type: "button",
|
|
482
|
+
role: "menuitemradio",
|
|
483
|
+
"aria-checked": effectiveEffort === level.effort,
|
|
484
|
+
className: cls(ModelSubmenu_module_css_default.option, effectiveEffort === level.effort && ModelSubmenu_module_css_default.selected),
|
|
485
|
+
disabled: busy,
|
|
486
|
+
onClick: () => chooseEffort(level.effort),
|
|
487
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
488
|
+
className: ModelSubmenu_module_css_default.optionCopy,
|
|
489
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
490
|
+
className: ModelSubmenu_module_css_default.modelName,
|
|
491
|
+
children: level.label
|
|
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.IconCheckOutline16, {}) : null
|
|
496
|
+
})]
|
|
497
|
+
}, level.key))
|
|
498
|
+
] })
|
|
499
|
+
]
|
|
500
|
+
}), document.body),
|
|
501
|
+
toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
502
|
+
text: toast.text,
|
|
503
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
|
|
504
|
+
anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
|
|
505
|
+
onDone: () => {
|
|
506
|
+
setToast(null);
|
|
507
|
+
}
|
|
508
|
+
}, toast.seq)
|
|
509
|
+
]
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/client/locales.ts
|
|
514
|
+
/** Locale bundles for the composer model seat this plugin takes over. */
|
|
515
|
+
/** Dictionary namespace owned by this plugin. */
|
|
516
|
+
const NS = "modelSubmenu";
|
|
517
|
+
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
518
|
+
const zh = {
|
|
519
|
+
"back": "返回",
|
|
520
|
+
"provider.modelCount": "{count} 个模型",
|
|
521
|
+
"action.reload": "重新加载",
|
|
522
|
+
"trigger.fallback": "选择模型",
|
|
523
|
+
"trigger.loading": "正在加载模型…",
|
|
524
|
+
"trigger.selectAria": "选择模型",
|
|
525
|
+
"trigger.aria": "选择模型,当前 {model}",
|
|
526
|
+
"trigger.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
|
|
527
|
+
"menu.aria": "供应商与推理等级",
|
|
528
|
+
"menu.provider": "供应商",
|
|
529
|
+
"menu.effort": "推理等级",
|
|
530
|
+
"effort.providerDefault": "Default",
|
|
531
|
+
"status.loading": "正在刷新模型列表…",
|
|
532
|
+
"error.action": "模型操作失败:{message}",
|
|
533
|
+
"error.sessionInUse": "当前会话已被占用,可能是其他正在运行的 DSH 导致的(如其他 dsh web、桌面端),请退出其他正在运行的 DSH 后重试。",
|
|
534
|
+
"warning.groupLoad": "{name} 加载失败:{message}",
|
|
535
|
+
"empty.models": "没有可用的模型。",
|
|
536
|
+
"empty.efforts": "当前模型未提供推理等级。"
|
|
537
|
+
};
|
|
538
|
+
/** English dictionary, checked complete against the zh key set. */
|
|
539
|
+
const en = {
|
|
540
|
+
"back": "Back",
|
|
541
|
+
"provider.modelCount": "{count} models",
|
|
542
|
+
"action.reload": "Reload",
|
|
543
|
+
"trigger.fallback": "Select model",
|
|
544
|
+
"trigger.loading": "Loading models…",
|
|
545
|
+
"trigger.selectAria": "Select model",
|
|
546
|
+
"trigger.aria": "Select model, current {model}",
|
|
547
|
+
"trigger.ariaEffort": "Select model, current {model}, reasoning effort {effort}",
|
|
548
|
+
"menu.aria": "Provider and reasoning effort",
|
|
549
|
+
"menu.provider": "Provider",
|
|
550
|
+
"menu.effort": "Effort",
|
|
551
|
+
"effort.providerDefault": "Default",
|
|
552
|
+
"status.loading": "Refreshing model list…",
|
|
553
|
+
"error.action": "Model operation failed: {message}",
|
|
554
|
+
"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
|
+
"warning.groupLoad": "{name} failed to load: {message}",
|
|
556
|
+
"empty.models": "No models available.",
|
|
557
|
+
"empty.efforts": "This model provides no reasoning effort levels."
|
|
558
|
+
};
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/client/index.ts
|
|
561
|
+
/**
|
|
562
|
+
* Cordis services required by the browser half. `slots` is the contribution
|
|
563
|
+
* registry, `sessions` tells whether a session may select a model, and
|
|
564
|
+
* `modelDirectories` owns the per-session catalog snapshot this seat renders
|
|
565
|
+
* from. `remote` / `remote.session` are declared because a service method runs
|
|
566
|
+
* on the calling context: `directoryFor` reads `this.ctx.remote.session` while
|
|
567
|
+
* building the directory, so the seat must carry that service too.
|
|
568
|
+
*/
|
|
569
|
+
const inject = [
|
|
570
|
+
"locale",
|
|
571
|
+
"modelDirectories",
|
|
572
|
+
"remote",
|
|
573
|
+
"remote.session",
|
|
574
|
+
"sessions",
|
|
575
|
+
"slots"
|
|
576
|
+
];
|
|
577
|
+
/**
|
|
578
|
+
* Client plugin body: register this plugin's dictionaries, then take over the
|
|
579
|
+
* composer model seat below the built-in occupant's priority so this menu
|
|
580
|
+
* renders in its place.
|
|
581
|
+
* @param ctx - client root context.
|
|
582
|
+
*/
|
|
583
|
+
function apply(ctx) {
|
|
584
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
585
|
+
zh,
|
|
586
|
+
en
|
|
587
|
+
}), "dsh-model-submenu: dictionaries");
|
|
588
|
+
ctx.inject(["slots", "modelDirectories"], (scope) => {
|
|
589
|
+
const models = scope.modelDirectories;
|
|
590
|
+
const sessions = scope.sessions;
|
|
591
|
+
scope.slots.inject("conversation.input.model", () => scope.slots.register({
|
|
592
|
+
name: "conversation.input.model",
|
|
593
|
+
priority: -100,
|
|
594
|
+
registrant: "@lynn123411/dsh-model-submenu",
|
|
595
|
+
locale: NS,
|
|
596
|
+
inject: (sessionId) => {
|
|
597
|
+
const key = sessionId;
|
|
598
|
+
const directory = models.directoryFor(key);
|
|
599
|
+
const available = sessions.subagentAddress(key) === void 0;
|
|
600
|
+
return {
|
|
601
|
+
available,
|
|
602
|
+
directory: directory.store,
|
|
603
|
+
load: () => {
|
|
604
|
+
if (available) directory.load().catch(() => {});
|
|
605
|
+
},
|
|
606
|
+
select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0)
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}, ModelSubmenu));
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
613
|
+
exports.ModelSubmenu = ModelSubmenu;
|
|
614
|
+
exports.apply = apply;
|
|
615
|
+
exports.inject = inject;
|
|
616
|
+
return module.exports;
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
//# sourceMappingURL=client.js.map
|