@bittersmilezzz/dsh-model-selector 0.1.18
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/LICENSE +21 -0
- package/README.md +74 -0
- package/assets/cover.png +0 -0
- package/assets/menu-default.png +0 -0
- package/assets/menu-search.png +0 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +2184 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +5 -0
- package/lib/index.js.map +1 -0
- package/lib/tsconfig.client.tsbuildinfo +1 -0
- package/lib/types/client/ModelSelect.d.ts +40 -0
- package/lib/types/client/ModelSelect.d.ts.map +1 -0
- package/lib/types/client/effort.d.ts +66 -0
- package/lib/types/client/effort.d.ts.map +1 -0
- package/lib/types/client/effortCanvas.d.ts +39 -0
- package/lib/types/client/effortCanvas.d.ts.map +1 -0
- package/lib/types/client/effortDrag.d.ts +39 -0
- package/lib/types/client/effortDrag.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +20 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +71 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/menuFit.d.ts +31 -0
- package/lib/types/client/menuFit.d.ts.map +1 -0
- package/lib/types/client/roving.d.ts +26 -0
- package/lib/types/client/roving.d.ts.map +1 -0
- package/lib/types/client/styles.d.ts +10 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/index.d.ts +13 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +126 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2184 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@bittersmilezzz/dsh-model-selector",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
29
|
+
let react = require("react");
|
|
30
|
+
react = __toESM(react, 1);
|
|
31
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
32
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
33
|
+
//#region src/client/locales.ts
|
|
34
|
+
/**
|
|
35
|
+
* `modelSelector` namespace dictionaries for the enhanced model seat.
|
|
36
|
+
*
|
|
37
|
+
* Simplified Chinese is the key-set source of truth; the English dictionary is
|
|
38
|
+
* checked complete against it. Product copy is Chinese-first per repo style.
|
|
39
|
+
*/
|
|
40
|
+
const zh = {
|
|
41
|
+
"trigger.fallback": "选择模型",
|
|
42
|
+
"trigger.loading": "正在加载模型…",
|
|
43
|
+
"trigger.selectAria": "选择模型",
|
|
44
|
+
"trigger.aria": "选择模型,当前 {model}",
|
|
45
|
+
"trigger.ariaEffort": "选择模型,当前 {model},推理等级 {effort}",
|
|
46
|
+
"menu.aria": "模型列表",
|
|
47
|
+
"menu.effort": "推理等级",
|
|
48
|
+
"search.placeholder": "搜索模型",
|
|
49
|
+
"search.clearAria": "清除搜索",
|
|
50
|
+
"search.noMatch": "没有匹配“{query}”的模型。",
|
|
51
|
+
"search.status": "{count} 个结果",
|
|
52
|
+
"search.more": "仅显示前 {shown} 条(共 {total} 条命中),请细化关键词。",
|
|
53
|
+
"group.toggleAria": "{name}({count} 个模型)",
|
|
54
|
+
"effort.providerDefault": "Default",
|
|
55
|
+
"status.loading": "正在刷新模型列表…",
|
|
56
|
+
"error.action": "模型操作失败:{message}",
|
|
57
|
+
"action.reload": "重新加载",
|
|
58
|
+
"warning.groupLoad": "{name} 加载失败:{message}",
|
|
59
|
+
"empty.models": "没有可用的模型。",
|
|
60
|
+
"empty.efforts": "当前模型未提供推理等级。",
|
|
61
|
+
"effort.failed": "档位切换未生效(后端未接受),已恢复原档位",
|
|
62
|
+
"effort.timeout": "档位切换超时,已恢复原档位",
|
|
63
|
+
"notice.already": "已是当前模型,无需切换",
|
|
64
|
+
"notice.selectFailed": "切换失败,请重试",
|
|
65
|
+
"toast.effortAuto": "已自动选到最强思考档:{effort}",
|
|
66
|
+
"badge.reasoning": "推理",
|
|
67
|
+
"badge.reasoningHint": "支持思考等级,切换后自动选到最大思考强度"
|
|
68
|
+
};
|
|
69
|
+
const en = {
|
|
70
|
+
"trigger.fallback": "Select model",
|
|
71
|
+
"trigger.loading": "Loading models…",
|
|
72
|
+
"trigger.selectAria": "Select model",
|
|
73
|
+
"trigger.aria": "Select model, current {model}",
|
|
74
|
+
"trigger.ariaEffort": "Select model, current {model}, reasoning effort {effort}",
|
|
75
|
+
"menu.aria": "Model list",
|
|
76
|
+
"menu.effort": "Effort",
|
|
77
|
+
"search.placeholder": "Search models",
|
|
78
|
+
"search.clearAria": "Clear search",
|
|
79
|
+
"search.noMatch": "No models match “{query}”.",
|
|
80
|
+
"search.status": "{count} results",
|
|
81
|
+
"search.more": "Showing first {shown} of {total} matches; refine your query.",
|
|
82
|
+
"group.toggleAria": "{name} ({count} models)",
|
|
83
|
+
"effort.providerDefault": "Default",
|
|
84
|
+
"status.loading": "Refreshing model list…",
|
|
85
|
+
"error.action": "Model operation failed: {message}",
|
|
86
|
+
"action.reload": "Reload",
|
|
87
|
+
"warning.groupLoad": "{name} failed to load: {message}",
|
|
88
|
+
"empty.models": "No models available.",
|
|
89
|
+
"empty.efforts": "This model provides no reasoning effort levels.",
|
|
90
|
+
"effort.failed": "Failed to switch effort (backend rejected); restored",
|
|
91
|
+
"effort.timeout": "Timed out switching effort; restored",
|
|
92
|
+
"notice.already": "Already the current model",
|
|
93
|
+
"notice.selectFailed": "Switch failed, please try again",
|
|
94
|
+
"toast.effortAuto": "Auto-selected the strongest reasoning level: {effort}",
|
|
95
|
+
"badge.reasoning": "Reasoning",
|
|
96
|
+
"badge.reasoningHint": "Supports reasoning levels; switches land on the strongest"
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/client/effort.ts
|
|
100
|
+
/** Canonical strength order of pi-ai thinking levels (strongest = highest). */
|
|
101
|
+
const EFFORT_RANK = {
|
|
102
|
+
off: 0,
|
|
103
|
+
minimal: 1,
|
|
104
|
+
low: 2,
|
|
105
|
+
medium: 3,
|
|
106
|
+
high: 4,
|
|
107
|
+
xhigh: 5,
|
|
108
|
+
max: 6
|
|
109
|
+
};
|
|
110
|
+
/** The strongest thinking level a model offers, or undefined for none. */
|
|
111
|
+
function maxEffortOf(reasoning) {
|
|
112
|
+
let best;
|
|
113
|
+
for (const effort of reasoning.efforts) {
|
|
114
|
+
const rank = EFFORT_RANK[effort.id] ?? 0;
|
|
115
|
+
if (best === void 0 || rank > best.rank) best = {
|
|
116
|
+
id: effort.id,
|
|
117
|
+
rank
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return best?.id;
|
|
121
|
+
}
|
|
122
|
+
function dmsEffortIndex(levels, id) {
|
|
123
|
+
return levels.findIndex((level) => level.id === id);
|
|
124
|
+
}
|
|
125
|
+
/** 索引钳位:档位数为 0 时返回 0,四舍五入后落在 [0, count-1]。 */
|
|
126
|
+
function dmsClampIndex(value, count) {
|
|
127
|
+
if (count <= 0) return 0;
|
|
128
|
+
return Math.max(0, Math.min(count - 1, Math.round(value)));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 指针水平位置 → 档位原始值(未四舍五入,拖动中途可落在两档之间;落点由
|
|
132
|
+
* 提交方 clamp)。输入条退化(宽度 ≤ 0 或档位数 < 2)时保持当前值不动。
|
|
133
|
+
*/
|
|
134
|
+
function dmsPointerRaw(clientX, left, width, levelCount, current) {
|
|
135
|
+
if (width <= 0 || levelCount < 2) return current;
|
|
136
|
+
return Math.max(0, Math.min(levelCount - 1, (clientX - left) / width * (levelCount - 1)));
|
|
137
|
+
}
|
|
138
|
+
function dmsCurrentModel(state) {
|
|
139
|
+
if (state.current === null) return void 0;
|
|
140
|
+
const current = state.current;
|
|
141
|
+
return state.groups.find((g) => g.id === current.provider)?.models.find((m) => m.id === current.model) ?? void 0;
|
|
142
|
+
}
|
|
143
|
+
/** 当前生效档位:用户已选优先,其次模型默认,最后取中间档。 */
|
|
144
|
+
function dmsEffectiveEffortIndex(levels, state) {
|
|
145
|
+
const reasoning = dmsCurrentModel(state)?.reasoning;
|
|
146
|
+
const current = dmsEffortIndex(levels, state.current?.reasoningEffort);
|
|
147
|
+
if (current >= 0) return current;
|
|
148
|
+
const fallback = dmsEffortIndex(levels, reasoning?.defaultEffort);
|
|
149
|
+
if (fallback >= 0) return fallback;
|
|
150
|
+
return Math.floor((levels.length - 1) / 2);
|
|
151
|
+
}
|
|
152
|
+
/** 滑块档位:少于两档(无法构成滑杆)时返回空数组。 */
|
|
153
|
+
function dmsSliderLevels(state) {
|
|
154
|
+
const efforts = dmsCurrentModel(state)?.reasoning?.efforts;
|
|
155
|
+
return efforts !== void 0 && efforts.length >= 2 ? efforts : [];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 滑杆是否处于忙态:自身提交中,或共享目录上有任一 select 在途
|
|
159
|
+
* (目录 select 是 last-writer-wins,模型切换在途时再发 effort RPC 会与
|
|
160
|
+
* 它交错——effort 打到旧模型上,与模型切换互相覆盖;choose() 按同一状态
|
|
161
|
+
* 拒绝了模型点击,滑杆必须同规则拒绝交互)。
|
|
162
|
+
*/
|
|
163
|
+
function dmsEffortBusy(committing, status) {
|
|
164
|
+
return committing || status === "selecting";
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 超时回滚后 select 迟到成功是否应采纳并同步 UI:
|
|
168
|
+
* 只有当自回滚以来没有任何新的提交改写 committedRef(仍等于回滚前的档位)、
|
|
169
|
+
* 提交纪元未推进(epoch === epochAtCommit,即期间没有发起过新提交——提交失败
|
|
170
|
+
* 回滚后 committedRef 会回到与上一轮回滚相同的值,值比较无法区分「无新提交」
|
|
171
|
+
* 与「新提交失败回滚」,纪元才能区分)、且当前无拖动/提交在途时,迟到结果才
|
|
172
|
+
* 仍对应当前唯一的意图链——此时 UI 处于「回滚但后端已生效」的错位态,应补
|
|
173
|
+
* 一次同步而不是保持回滚。否则以新操作链为准。
|
|
174
|
+
*/
|
|
175
|
+
function dmsShouldAdoptLateSuccess(committed, previous, dragging, committing, epoch, epochAtCommit) {
|
|
176
|
+
return committed === previous && epoch === epochAtCommit && !dragging && !committing;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* 拖动终止事件是否对应当前活动拖动:pointerActive 为假(拖动已结束/从未开始)
|
|
180
|
+
* 或事件 pointerId 与活动指针不一致(迟到/重复事件——典型是 pointercancel 终态
|
|
181
|
+
* 后平台补发的 pointerup)时返回 false,调用方应幂等跳过终止动作:不清状态、
|
|
182
|
+
* 不触发提交/回滚回调。pointerId 缺省(blur 兜底提交,无事件对象)时只要求
|
|
183
|
+
* 拖动仍在进行中。
|
|
184
|
+
*/
|
|
185
|
+
function dmsIsActiveDrag(pointerActive, activePointerId, pointerId) {
|
|
186
|
+
if (!pointerActive) return false;
|
|
187
|
+
if (pointerId !== void 0 && activePointerId !== pointerId) return false;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/client/effortCanvas.ts
|
|
192
|
+
/**
|
|
193
|
+
* dsh-model-selector — EffortSlider 的 canvas 辐射特效模块。
|
|
194
|
+
*
|
|
195
|
+
* 从 ModelSelect.tsx 原样搬出(原 EffortSlider 组件内的 330 行状态机里,
|
|
196
|
+
* 绘制与动画循环约占一半且与 React 状态解耦):
|
|
197
|
+
* - dmsDrawRadiation:纯绘制函数,state(progress/dragging)与主题都由参数
|
|
198
|
+
* 传入,不读组件闭包——同一帧可被 pointer 事件、rAF 循环、主题变化各路径复用。
|
|
199
|
+
* - useEffortCanvas:canvas 生命周期(尺寸/DPR、ResizeObserver、主题
|
|
200
|
+
* MutationObserver、visibilitychange、prefers-reduced-motion)与相位缓动
|
|
201
|
+
* 动画循环。外部通过 radiationRef 更新 target/dragging 驱动重绘,hook
|
|
202
|
+
* 返回的 redraw 注册到组件侧 ref(preview 变化时组件直接调它触发一帧)。
|
|
203
|
+
*/
|
|
204
|
+
/** 拖动时辐射推挤的缓动系数(越小越跟手),非拖拽时回弹系数。 */
|
|
205
|
+
const DRAG_EASE = .55;
|
|
206
|
+
const IDLE_EASE = .12;
|
|
207
|
+
const SETTLE_EPSILON = .002;
|
|
208
|
+
/**
|
|
209
|
+
* 纯绘制:把滑块进度画成拖尾辐射(列能量 + 径向光晕 + 粒子流 + 热核光斑)。
|
|
210
|
+
* 全部可变量(尺寸、相位、进度、拖拽态、深浅色)都来自参数——无模块级状态。
|
|
211
|
+
*/
|
|
212
|
+
function dmsDrawRadiation(context, width, height, time, state, isDark) {
|
|
213
|
+
const origin = state.progress * width;
|
|
214
|
+
const cell = 4;
|
|
215
|
+
const speed = state.dragging ? 2.8 : 1;
|
|
216
|
+
context.clearRect(0, 0, width, height);
|
|
217
|
+
if (origin <= 0) return;
|
|
218
|
+
context.save();
|
|
219
|
+
context.beginPath();
|
|
220
|
+
context.rect(0, 0, origin, height);
|
|
221
|
+
context.clip();
|
|
222
|
+
for (let x = 0; x < origin; x += cell) {
|
|
223
|
+
const delta = x + cell * .5 - origin;
|
|
224
|
+
const distance = Math.abs(delta);
|
|
225
|
+
const phaseA = distance / 10 - time * .0074 * speed;
|
|
226
|
+
const phaseB = distance / 23 - time * .0041 * speed + 1.7;
|
|
227
|
+
const phaseC = distance / 40 - time * .0022 * speed + 3.4;
|
|
228
|
+
const sinA = Math.max(0, Math.sin(phaseA));
|
|
229
|
+
const sinB = Math.max(0, Math.sin(phaseB));
|
|
230
|
+
const sinC = Math.max(0, Math.sin(phaseC));
|
|
231
|
+
const waveA = Math.pow(sinA, 2.6);
|
|
232
|
+
const waveB = Math.pow(sinB, 3.2);
|
|
233
|
+
const waveC = Math.pow(sinC, 4);
|
|
234
|
+
const crest = Math.pow(sinA, 15) + Math.pow(sinB, 18) * .78;
|
|
235
|
+
const wave = Math.min(1, waveA * .76 + waveB * .58 + waveC * .32);
|
|
236
|
+
const trail = .38 + .62 * Math.exp(-distance / 90);
|
|
237
|
+
const pillar = Math.pow(Math.max(0, Math.sin(x / 20 + time * .0016)), 3) * .27;
|
|
238
|
+
const columnEnergy = trail * (wave * 1.04 + pillar + crest * .32);
|
|
239
|
+
if (columnEnergy > .012) {
|
|
240
|
+
const nearness = Math.max(0, 1 - distance / 140);
|
|
241
|
+
context.fillStyle = `rgba(${isDark ? Math.round(42 + 124 * nearness + 75 * wave) : Math.round(28 + 58 * nearness + 15 * wave)}, ${isDark ? Math.round(56 + 58 * nearness + 44 * crest) : Math.round(88 + 72 * nearness + 30 * crest)}, ${isDark ? Math.round(175 + 72 * nearness + 8 * wave) : Math.round(182 + 62 * nearness)}, ${isDark ? Math.min(.88, columnEnergy * .72) : Math.min(.62, columnEnergy * .54)})`;
|
|
242
|
+
context.fillRect(x, 0, 3, height);
|
|
243
|
+
}
|
|
244
|
+
for (let y = 0; y < height; y += cell) {
|
|
245
|
+
const deltaY = y + cell * .5 - height * .5;
|
|
246
|
+
const radial = Math.hypot(delta / 38, deltaY / 11);
|
|
247
|
+
const halo = Math.exp(-radial * .96) * 1.08;
|
|
248
|
+
const verticalShape = .58 + .42 * Math.cos(deltaY / height * Math.PI);
|
|
249
|
+
const grain = .72 + .28 * Math.sin(x * .73 + y * 1.31 + time * .006);
|
|
250
|
+
const alpha = Math.min(.96, (columnEnergy * .88 + halo + crest * .19) * verticalShape * grain);
|
|
251
|
+
if (alpha < .035) continue;
|
|
252
|
+
const hot = Math.max(0, 1 - radial / 2.4);
|
|
253
|
+
context.fillStyle = `rgba(${isDark ? Math.round(54 + 148 * hot + 42 * wave + 35 * crest) : Math.round(25 + 72 * hot + 12 * wave)}, ${isDark ? Math.round(68 + 78 * hot + 46 * crest) : Math.round(98 + 72 * hot + 24 * crest)}, ${isDark ? Math.round(186 + 64 * hot) : Math.round(194 + 56 * hot)}, ${isDark ? alpha : alpha * .72})`;
|
|
254
|
+
context.fillRect(x, y, 3, 3);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (let i = 0; i < 14; i += 1) {
|
|
258
|
+
const particleX = origin - (time * (state.dragging ? .16 : .065) * (.78 + i % 5 * .09) + i * 23) % Math.max(30, origin + 64);
|
|
259
|
+
if (particleX < -24 || particleX > width + 16) continue;
|
|
260
|
+
const particleY = 3 + (i * 13 + Math.sin(time * .003 + i) * 5) % Math.max(7, height - 6);
|
|
261
|
+
const length = 4 + i % 4 * 4 + (state.dragging ? 6 : 0);
|
|
262
|
+
const alpha = .28 + i % 5 * .1;
|
|
263
|
+
const streak = context.createLinearGradient(particleX, 0, particleX + length, 0);
|
|
264
|
+
streak.addColorStop(0, isDark ? "rgba(72,118,255,0)" : "rgba(24,94,184,0)");
|
|
265
|
+
streak.addColorStop(.68, isDark ? `rgba(112,135,255,${alpha})` : `rgba(36,108,202,${alpha * .72})`);
|
|
266
|
+
streak.addColorStop(1, isDark ? `rgba(236,222,255,${Math.min(1, alpha + .26)})` : `rgba(103,175,248,${Math.min(.82, alpha + .18)})`);
|
|
267
|
+
context.fillStyle = streak;
|
|
268
|
+
context.fillRect(particleX, particleY, length, i % 3 === 0 ? 2 : 1);
|
|
269
|
+
}
|
|
270
|
+
const glow = context.createRadialGradient(origin, height / 2, 0, origin, height / 2, 24);
|
|
271
|
+
glow.addColorStop(0, isDark ? "rgba(255,255,255,.82)" : "rgba(255,255,255,.86)");
|
|
272
|
+
glow.addColorStop(.14, isDark ? "rgba(183,190,255,.54)" : "rgba(162,210,255,.48)");
|
|
273
|
+
glow.addColorStop(.44, isDark ? "rgba(103,74,255,.28)" : "rgba(37,112,207,.22)");
|
|
274
|
+
glow.addColorStop(1, isDark ? "rgba(86,31,210,0)" : "rgba(25,91,181,0)");
|
|
275
|
+
context.fillStyle = glow;
|
|
276
|
+
context.fillRect(origin - 26, 0, 52, height);
|
|
277
|
+
context.restore();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 辐射画布生命周期 hook:尺寸/DPR 自适应、主题变化监听、动画循环与相位时钟。
|
|
281
|
+
*
|
|
282
|
+
* 外部契约:
|
|
283
|
+
* - radiationRef.current.target / .dragging 由组件在 preview/忙态变化时更新,
|
|
284
|
+
* hook 的动画循环按 target 做指数缓动(拖动中每帧逼近 target,回弹时渐近);
|
|
285
|
+
* - 返回的 redraw 应注册到组件侧 ref,供事件驱动的重绘(拖动一帧、主题翻转)
|
|
286
|
+
* 即时调用;挂载时画首帧并把 progress 直接对齐 target(打开菜单不闪中间态)。
|
|
287
|
+
*
|
|
288
|
+
* 动画成本:拖拽时只画事件驱动帧(pointer 移动触发的单帧),循环空闲时
|
|
289
|
+
* 一帧即停——画布不常驻 rAF,CPU 占用只在缓动回弹窗口内存在。
|
|
290
|
+
*/
|
|
291
|
+
function useEffortCanvas(canvasRef, radiationRef) {
|
|
292
|
+
const redrawRef = react.useRef(null);
|
|
293
|
+
react.useEffect(() => {
|
|
294
|
+
const canvas = canvasRef.current;
|
|
295
|
+
if (canvas === null) return;
|
|
296
|
+
const context = canvas.getContext("2d");
|
|
297
|
+
if (context === null) return;
|
|
298
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
299
|
+
let width = 1;
|
|
300
|
+
let height = 1;
|
|
301
|
+
let frame = 0;
|
|
302
|
+
let phase = 0;
|
|
303
|
+
let lastFrameAt = 0;
|
|
304
|
+
const resize = () => {
|
|
305
|
+
const bounds = canvas.getBoundingClientRect();
|
|
306
|
+
const ratio = Math.min(window.devicePixelRatio || 1, 2);
|
|
307
|
+
width = Math.max(1, bounds.width);
|
|
308
|
+
height = Math.max(1, bounds.height);
|
|
309
|
+
canvas.width = Math.max(1, Math.round(width * ratio));
|
|
310
|
+
canvas.height = Math.max(1, Math.round(height * ratio));
|
|
311
|
+
context.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
312
|
+
};
|
|
313
|
+
const draw = () => {
|
|
314
|
+
const r = radiationRef.current;
|
|
315
|
+
if (r.target !== void 0) {
|
|
316
|
+
const k = r.dragging ? DRAG_EASE : IDLE_EASE;
|
|
317
|
+
r.progress += (r.target - r.progress) * k;
|
|
318
|
+
if (Math.abs(r.target - r.progress) < SETTLE_EPSILON) r.progress = r.target;
|
|
319
|
+
}
|
|
320
|
+
const isDark = document.body.hasAttribute("data-ds-dark-theme");
|
|
321
|
+
dmsDrawRadiation(context, width, height, phase, r, isDark);
|
|
322
|
+
if (r.dragging) return false;
|
|
323
|
+
return r.target !== void 0 && Math.abs(r.target - r.progress) > SETTLE_EPSILON;
|
|
324
|
+
};
|
|
325
|
+
const loop = (time) => {
|
|
326
|
+
if (lastFrameAt !== 0) phase += time - lastFrameAt;
|
|
327
|
+
lastFrameAt = time;
|
|
328
|
+
if (draw() && !document.hidden) frame = window.requestAnimationFrame(loop);
|
|
329
|
+
else {
|
|
330
|
+
frame = 0;
|
|
331
|
+
lastFrameAt = 0;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const redraw = () => {
|
|
335
|
+
if (reducedMotion.matches) {
|
|
336
|
+
draw();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (frame === 0 && !document.hidden) {
|
|
340
|
+
lastFrameAt = 0;
|
|
341
|
+
frame = window.requestAnimationFrame(loop);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
345
|
+
resize();
|
|
346
|
+
draw();
|
|
347
|
+
});
|
|
348
|
+
const themeObserver = new MutationObserver(() => draw());
|
|
349
|
+
const onVisibility = () => {
|
|
350
|
+
if (!document.hidden) redraw();
|
|
351
|
+
};
|
|
352
|
+
const resolutionQuery = window.matchMedia(`(resolution: ${window.devicePixelRatio || 1}dppx)`);
|
|
353
|
+
const onResolutionChange = () => {
|
|
354
|
+
resize();
|
|
355
|
+
draw();
|
|
356
|
+
};
|
|
357
|
+
if (resolutionQuery.addEventListener !== void 0) resolutionQuery.addEventListener("change", onResolutionChange);
|
|
358
|
+
else resolutionQuery.addListener(onResolutionChange);
|
|
359
|
+
resizeObserver.observe(canvas);
|
|
360
|
+
themeObserver.observe(document.body, {
|
|
361
|
+
attributes: true,
|
|
362
|
+
attributeFilter: ["data-ds-dark-theme"]
|
|
363
|
+
});
|
|
364
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
365
|
+
redrawRef.current = redraw;
|
|
366
|
+
resize();
|
|
367
|
+
const r = radiationRef.current;
|
|
368
|
+
if (r.target !== void 0) r.progress = r.target;
|
|
369
|
+
draw();
|
|
370
|
+
if (!reducedMotion.matches && !document.hidden) frame = window.requestAnimationFrame(loop);
|
|
371
|
+
return () => {
|
|
372
|
+
window.cancelAnimationFrame(frame);
|
|
373
|
+
resizeObserver.disconnect();
|
|
374
|
+
themeObserver.disconnect();
|
|
375
|
+
if (resolutionQuery.removeEventListener !== void 0) resolutionQuery.removeEventListener("change", onResolutionChange);
|
|
376
|
+
else resolutionQuery.removeListener(onResolutionChange);
|
|
377
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
378
|
+
redrawRef.current = null;
|
|
379
|
+
};
|
|
380
|
+
}, [canvasRef, radiationRef]);
|
|
381
|
+
return react.useCallback(() => redrawRef.current?.(), []);
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/client/effortDrag.ts
|
|
385
|
+
/**
|
|
386
|
+
* dsh-model-selector — EffortSlider 的指针拖动状态机。
|
|
387
|
+
*
|
|
388
|
+
* 从 ModelSelect.tsx 原样搬出(原 EffortSlider 的 8 个 ref 指针生命周期):
|
|
389
|
+
* 拖动只认发起时记录的活动 pointerId,window 捕获阶段的 move/up/cancel 兜底
|
|
390
|
+
* 指针拖出输入条/菜单外的场景;取消(pointercancel / 触控失控)走回滚回调,
|
|
391
|
+
* 不提交。几何换算(clientX → 档位原始值)与忙态判定都经由回调外接,
|
|
392
|
+
* hook 本身不读组件状态,拖动中途组件重渲染(preview setState)不会中断拖动。
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* 拖动中由 pointerId 校验的几何换算:raw = (clientX - left) / width 映射到档位区间。
|
|
396
|
+
* 输入条退化(宽度 ≤ 0 / 档位数 < 2)时返回 fallback(当前值,不跳档)。
|
|
397
|
+
*/
|
|
398
|
+
function rawFromPointer(input, clientX, levelCount, fallback) {
|
|
399
|
+
const bounds = input.getBoundingClientRect();
|
|
400
|
+
return dmsPointerRaw(clientX, bounds.left, bounds.width, levelCount, fallback);
|
|
401
|
+
}
|
|
402
|
+
function useEffortDrag({ levelCount, canStart, onPreview, onCommit, onRollback, inputRef, draggingRef }) {
|
|
403
|
+
const [dragging, setDragging] = react.useState(false);
|
|
404
|
+
const pointerActiveRef = react.useRef(false);
|
|
405
|
+
const activePointerIdRef = react.useRef(null);
|
|
406
|
+
const lastRawRef = react.useRef(0);
|
|
407
|
+
const callbacksRef = react.useRef({
|
|
408
|
+
levelCount,
|
|
409
|
+
canStart,
|
|
410
|
+
onPreview,
|
|
411
|
+
onCommit,
|
|
412
|
+
onRollback
|
|
413
|
+
});
|
|
414
|
+
callbacksRef.current = {
|
|
415
|
+
levelCount,
|
|
416
|
+
canStart,
|
|
417
|
+
onPreview,
|
|
418
|
+
onCommit,
|
|
419
|
+
onRollback
|
|
420
|
+
};
|
|
421
|
+
const showPointerPreview = react.useCallback((input, clientX) => {
|
|
422
|
+
const raw = rawFromPointer(input, clientX, callbacksRef.current.levelCount(), lastRawRef.current);
|
|
423
|
+
lastRawRef.current = raw;
|
|
424
|
+
callbacksRef.current.onPreview(raw);
|
|
425
|
+
}, []);
|
|
426
|
+
const beginDragging = react.useCallback((input, pointerId, clientX) => {
|
|
427
|
+
pointerActiveRef.current = true;
|
|
428
|
+
activePointerIdRef.current = pointerId;
|
|
429
|
+
draggingRef.current = true;
|
|
430
|
+
setDragging(true);
|
|
431
|
+
showPointerPreview(input, clientX);
|
|
432
|
+
try {
|
|
433
|
+
if (!input.hasPointerCapture(pointerId)) input.setPointerCapture(pointerId);
|
|
434
|
+
} catch (cause) {
|
|
435
|
+
console.debug("[dms-effort] setPointerCapture failed", cause);
|
|
436
|
+
}
|
|
437
|
+
}, [showPointerPreview]);
|
|
438
|
+
const moveDragging = react.useCallback((input, pointerId, clientX) => {
|
|
439
|
+
if (!pointerActiveRef.current || activePointerIdRef.current !== pointerId) return;
|
|
440
|
+
showPointerPreview(input, clientX);
|
|
441
|
+
}, [showPointerPreview]);
|
|
442
|
+
const stopDragging = react.useCallback((input, pointerId, clientX) => {
|
|
443
|
+
if (!dmsIsActiveDrag(pointerActiveRef.current, activePointerIdRef.current, pointerId)) return;
|
|
444
|
+
const raw = clientX === void 0 ? lastRawRef.current : rawFromPointer(input, clientX, callbacksRef.current.levelCount(), lastRawRef.current);
|
|
445
|
+
lastRawRef.current = raw;
|
|
446
|
+
pointerActiveRef.current = false;
|
|
447
|
+
activePointerIdRef.current = null;
|
|
448
|
+
draggingRef.current = false;
|
|
449
|
+
setDragging(false);
|
|
450
|
+
if (pointerId !== void 0 && input.hasPointerCapture(pointerId)) input.releasePointerCapture(pointerId);
|
|
451
|
+
callbacksRef.current.onCommit(raw);
|
|
452
|
+
}, []);
|
|
453
|
+
const cancelDragging = (pointerId) => {
|
|
454
|
+
if (!dmsIsActiveDrag(pointerActiveRef.current, activePointerIdRef.current, pointerId)) return;
|
|
455
|
+
pointerActiveRef.current = false;
|
|
456
|
+
activePointerIdRef.current = null;
|
|
457
|
+
draggingRef.current = false;
|
|
458
|
+
setDragging(false);
|
|
459
|
+
callbacksRef.current.onRollback();
|
|
460
|
+
};
|
|
461
|
+
const globalMoveRef = react.useRef(() => {});
|
|
462
|
+
const globalEndRef = react.useRef(() => {});
|
|
463
|
+
const globalCancelRef = react.useRef(() => {});
|
|
464
|
+
globalMoveRef.current = (event) => {
|
|
465
|
+
const input = inputRef.current;
|
|
466
|
+
if (input !== null) moveDragging(input, event.pointerId, event.clientX);
|
|
467
|
+
};
|
|
468
|
+
globalEndRef.current = (event) => {
|
|
469
|
+
const input = inputRef.current;
|
|
470
|
+
if (input !== null) stopDragging(input, event.pointerId, event.clientX);
|
|
471
|
+
};
|
|
472
|
+
globalCancelRef.current = (event) => cancelDragging(event.pointerId);
|
|
473
|
+
react.useEffect(() => {
|
|
474
|
+
if (!dragging) return;
|
|
475
|
+
const move = (event) => globalMoveRef.current(event);
|
|
476
|
+
const end = (event) => globalEndRef.current(event);
|
|
477
|
+
const cancel = (event) => globalCancelRef.current(event);
|
|
478
|
+
window.addEventListener("pointermove", move, true);
|
|
479
|
+
window.addEventListener("pointerup", end, true);
|
|
480
|
+
window.addEventListener("pointercancel", cancel, true);
|
|
481
|
+
return () => {
|
|
482
|
+
window.removeEventListener("pointermove", move, true);
|
|
483
|
+
window.removeEventListener("pointerup", end, true);
|
|
484
|
+
window.removeEventListener("pointercancel", cancel, true);
|
|
485
|
+
};
|
|
486
|
+
}, [dragging]);
|
|
487
|
+
return {
|
|
488
|
+
dragging,
|
|
489
|
+
draggingRef,
|
|
490
|
+
handlers: {
|
|
491
|
+
onPointerDown: (event) => {
|
|
492
|
+
if (!callbacksRef.current.canStart()) return;
|
|
493
|
+
event.preventDefault();
|
|
494
|
+
event.currentTarget.focus();
|
|
495
|
+
beginDragging(event.currentTarget, event.pointerId, event.clientX);
|
|
496
|
+
},
|
|
497
|
+
onPointerMove: (event) => moveDragging(event.currentTarget, event.pointerId, event.clientX),
|
|
498
|
+
onPointerUp: (event) => stopDragging(event.currentTarget, event.pointerId, event.clientX),
|
|
499
|
+
onPointerCancel: (event) => {
|
|
500
|
+
if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId);
|
|
501
|
+
cancelDragging(event.pointerId);
|
|
502
|
+
},
|
|
503
|
+
onBlur: () => {
|
|
504
|
+
const input = inputRef.current;
|
|
505
|
+
if (input !== null) stopDragging(input);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 向上弹当且仅当 trigger 上方可用空间不小于下方可用空间:
|
|
512
|
+
* 取上下两侧中空间更大的一侧,避免矮窗口把面板挤到视口下沿外面。
|
|
513
|
+
*/
|
|
514
|
+
function dmsMenuAbove(triggerTop, triggerBottom, viewportHeight) {
|
|
515
|
+
return triggerTop >= viewportHeight - triggerBottom;
|
|
516
|
+
}
|
|
517
|
+
/** 向下弹时按 trigger 下方空间钳位菜单高度(top-anchored,不能复用向上弹的 fit)。 */
|
|
518
|
+
function dmsBelowMaxHeight(triggerBottom, viewportHeight, cap) {
|
|
519
|
+
return Math.max(0, Math.min(cap, viewportHeight - triggerBottom - 12));
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 菜单水平钳位:默认右锚定(right:0,与 seat 右缘对齐);seat 右缘左侧放不下
|
|
523
|
+
* 整幅菜单(seat 靠输入区左下 + 窄窗口)、或 seat 右缘本身已越出视口右缘
|
|
524
|
+
* (页面横向溢出/缩放时 rect.right 可大于 innerWidth,右锚定会把整幅菜单
|
|
525
|
+
* 拖出屏外)时,返回钳到视口内的 left 值,调用方改用 left 锚定。
|
|
526
|
+
* 返回 undefined 表示保持默认右锚定。
|
|
527
|
+
* menuWidth 传菜单实际渲染宽度(offsetWidth),与 CSS `min(280px, 100vw-32px)` 解耦。
|
|
528
|
+
*/
|
|
529
|
+
function dmsMenuLeft(rootRight, menuWidth, viewportWidth, margin) {
|
|
530
|
+
if (rootRight - menuWidth >= margin && rootRight <= viewportWidth - margin) return void 0;
|
|
531
|
+
return Math.min(Math.max(margin, rootRight - menuWidth), Math.max(margin, viewportWidth - margin - menuWidth));
|
|
532
|
+
}
|
|
533
|
+
//#endregion
|
|
534
|
+
//#region src/client/roving.ts
|
|
535
|
+
/**
|
|
536
|
+
* Tab 默认落点:搜索态取「选中行若在命中集内,否则首个命中」;分组态取
|
|
537
|
+
* 「选中行若可见,否则首个可见模型行」;全部折叠(无可见模型行)时回退到
|
|
538
|
+
* 首个组头。返回 null 表示当前没有任何可聚焦行(空态由菜单外的 role=status
|
|
539
|
+
* 播报,列表容器不渲染)。
|
|
540
|
+
*/
|
|
541
|
+
function dmsDefaultRowKey(set) {
|
|
542
|
+
if (set.hitKeys !== null) {
|
|
543
|
+
if (set.hitKeys.length === 0) return null;
|
|
544
|
+
return set.selectedKey !== null && set.hitKeys.includes(set.selectedKey) ? set.selectedKey : set.hitKeys[0];
|
|
545
|
+
}
|
|
546
|
+
if (set.modelKeys.length > 0) return set.selectedKey !== null && set.modelKeys.includes(set.selectedKey) ? set.selectedKey : set.modelKeys[0];
|
|
547
|
+
return set.headerKeys[0] ?? null;
|
|
548
|
+
}
|
|
549
|
+
//#endregion
|
|
550
|
+
//#region src/client/ModelSelect.tsx
|
|
551
|
+
/**
|
|
552
|
+
* ModelSelect: the enhanced composer model seat (`conversation.input.model`).
|
|
553
|
+
*
|
|
554
|
+
* A single-pane menu (simpler than the shipped two-level root menu): opening
|
|
555
|
+
* the seat lands directly on the model list — search box + collapsible
|
|
556
|
+
* provider groups — with the current model's effort slider pinned inline at
|
|
557
|
+
* the bottom of the menu, so switching models and tuning reasoning effort
|
|
558
|
+
* both happen in one surface without an intermediate page.
|
|
559
|
+
*
|
|
560
|
+
* Data and submission ride the SAME per-session directory the /model popup
|
|
561
|
+
* shares (via `modelDirectories`), so a switch made here is what the popup
|
|
562
|
+
* shows next and vice versa. Icons, Toast and the menu fit/dismiss hooks are
|
|
563
|
+
* reused from @deepseek-ai/dsh-client-ui-primitives at runtime; colors
|
|
564
|
+
* come from `--dsw-*` tokens in the injected stylesheet.
|
|
565
|
+
*/
|
|
566
|
+
const IconChevronDown = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {});
|
|
567
|
+
const IconCheck = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline14, {});
|
|
568
|
+
const IconClear = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFill14, {});
|
|
569
|
+
/**
|
|
570
|
+
* How long a successfully loaded directory snapshot is trusted before the
|
|
571
|
+
* menu re-fetches it over RPC. The snapshot lives in the per-session store,
|
|
572
|
+
* so reopening the menu within this window costs zero RPC and zero re-render.
|
|
573
|
+
* 调法:调大 → 打开更省(RPC/重渲染都免),但目录侧改动要等窗口过期才可见;
|
|
574
|
+
* 调小 → 目录更新更实时。目录内容由官方 modelDirectories 推送,本值只影响
|
|
575
|
+
* 打开瞬间是否信任快照。
|
|
576
|
+
*/
|
|
577
|
+
const DIRECTORY_STALE_MS = 3e4;
|
|
578
|
+
/** 搜索命中渲染上限:宽泛关键词(如单字母)命中数百条时避免 DOM 爆炸。
|
|
579
|
+
* 调法:调大 → 宽泛搜索看到更多命中(渲染与搜索成本线性上升);调小 → 更省。 */
|
|
580
|
+
const MAX_VISIBLE_HITS = 100;
|
|
581
|
+
/** effort select RPC 的超时护栏:官方 select 无超时契约,RPC 永久挂起时
|
|
582
|
+
* 必须释放滑杆的 committing 锁并回滚,否则滑杆被锁死到菜单关闭。
|
|
583
|
+
* 调法:后端/网络慢时调大避免误回滚(滑杆锁死时间随之变长);调小更早释放交互。 */
|
|
584
|
+
const EFFORT_COMMIT_TIMEOUT_MS = 12e3;
|
|
585
|
+
const EffortSlider = react.memo(function EffortSlider({ state, select, t }) {
|
|
586
|
+
const levels = dmsSliderLevels(state);
|
|
587
|
+
const [effort, setEffort] = react.useState(() => levels[dmsEffectiveEffortIndex(levels, state)]?.id ?? "");
|
|
588
|
+
const [preview, setPreview] = react.useState(() => dmsEffectiveEffortIndex(levels, state));
|
|
589
|
+
const [committing, setCommitting] = react.useState(false);
|
|
590
|
+
const [localError, setLocalError] = react.useState(null);
|
|
591
|
+
const canvasRef = react.useRef(null);
|
|
592
|
+
const inputRef = react.useRef(null);
|
|
593
|
+
const committedRef = react.useRef("");
|
|
594
|
+
const committingRef = react.useRef(false);
|
|
595
|
+
const previewRef = react.useRef(0);
|
|
596
|
+
const draggingRef = react.useRef(false);
|
|
597
|
+
const commitTimerRef = react.useRef(null);
|
|
598
|
+
const mountedRef = react.useRef(false);
|
|
599
|
+
const commitEpochRef = react.useRef(0);
|
|
600
|
+
const radiationRef = react.useRef({
|
|
601
|
+
progress: levels.length >= 2 ? dmsEffectiveEffortIndex(levels, state) / (levels.length - 1) : .5,
|
|
602
|
+
dragging: false
|
|
603
|
+
});
|
|
604
|
+
const redrawRef = react.useRef(null);
|
|
605
|
+
const available = state.current !== null && levels.length >= 2;
|
|
606
|
+
const busy = dmsEffortBusy(committing, state.status);
|
|
607
|
+
const error = localError;
|
|
608
|
+
react.useEffect(() => {
|
|
609
|
+
if (!available || committingRef.current || draggingRef.current) return;
|
|
610
|
+
const index = dmsEffectiveEffortIndex(levels, state);
|
|
611
|
+
const next = levels[index]?.id ?? "";
|
|
612
|
+
committedRef.current = next;
|
|
613
|
+
previewRef.current = index;
|
|
614
|
+
setEffort(next);
|
|
615
|
+
setPreview(index);
|
|
616
|
+
setLocalError(null);
|
|
617
|
+
}, [
|
|
618
|
+
available,
|
|
619
|
+
levels.length,
|
|
620
|
+
state.current?.provider,
|
|
621
|
+
state.current?.model,
|
|
622
|
+
state.current?.reasoningEffort
|
|
623
|
+
]);
|
|
624
|
+
react.useEffect(() => {
|
|
625
|
+
previewRef.current = preview;
|
|
626
|
+
radiationRef.current.target = levels.length >= 2 ? preview / (levels.length - 1) : .5;
|
|
627
|
+
redrawRef.current?.();
|
|
628
|
+
}, [preview, levels.length]);
|
|
629
|
+
const redraw = useEffortCanvas(canvasRef, radiationRef);
|
|
630
|
+
react.useEffect(() => {
|
|
631
|
+
redrawRef.current = redraw;
|
|
632
|
+
return () => {
|
|
633
|
+
redrawRef.current = null;
|
|
634
|
+
};
|
|
635
|
+
}, [redraw]);
|
|
636
|
+
react.useEffect(() => {
|
|
637
|
+
mountedRef.current = true;
|
|
638
|
+
return () => {
|
|
639
|
+
mountedRef.current = false;
|
|
640
|
+
if (commitTimerRef.current !== null) {
|
|
641
|
+
window.clearTimeout(commitTimerRef.current);
|
|
642
|
+
commitTimerRef.current = null;
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}, []);
|
|
646
|
+
const showPointerPreview = react.useCallback((raw) => {
|
|
647
|
+
previewRef.current = raw;
|
|
648
|
+
setPreview(raw);
|
|
649
|
+
setEffort(levels[dmsClampIndex(raw, levels.length)]?.id ?? "");
|
|
650
|
+
}, [levels]);
|
|
651
|
+
const rollback = react.useCallback(() => {
|
|
652
|
+
const previous = committedRef.current;
|
|
653
|
+
previewRef.current = Math.max(0, dmsEffortIndex(levels, previous));
|
|
654
|
+
setEffort(previous);
|
|
655
|
+
setPreview(Math.max(0, dmsEffortIndex(levels, previous)));
|
|
656
|
+
}, [levels]);
|
|
657
|
+
const commit = react.useCallback(async (raw) => {
|
|
658
|
+
if (dmsEffortBusy(committingRef.current, state.status)) return;
|
|
659
|
+
const index = dmsClampIndex(raw, levels.length);
|
|
660
|
+
const next = levels[index]?.id;
|
|
661
|
+
if (next === void 0 || next === committedRef.current) return;
|
|
662
|
+
committingRef.current = true;
|
|
663
|
+
const previous = committedRef.current;
|
|
664
|
+
commitEpochRef.current += 1;
|
|
665
|
+
const epochAtCommit = commitEpochRef.current;
|
|
666
|
+
setCommitting(true);
|
|
667
|
+
setLocalError(null);
|
|
668
|
+
previewRef.current = index;
|
|
669
|
+
setPreview(index);
|
|
670
|
+
setEffort(next);
|
|
671
|
+
try {
|
|
672
|
+
const current = state.current;
|
|
673
|
+
if (current === null) throw new Error(t("empty.efforts"));
|
|
674
|
+
if (!await new Promise((resolve, reject) => {
|
|
675
|
+
let settled = false;
|
|
676
|
+
const timer = window.setTimeout(() => {
|
|
677
|
+
if (settled) return;
|
|
678
|
+
settled = true;
|
|
679
|
+
commitTimerRef.current = null;
|
|
680
|
+
reject(new Error(t("effort.timeout")));
|
|
681
|
+
}, EFFORT_COMMIT_TIMEOUT_MS);
|
|
682
|
+
commitTimerRef.current = timer;
|
|
683
|
+
select({
|
|
684
|
+
provider: current.provider,
|
|
685
|
+
model: current.model,
|
|
686
|
+
reasoningEffort: next
|
|
687
|
+
}).then((accepted) => {
|
|
688
|
+
if (settled) {
|
|
689
|
+
if (accepted && mountedRef.current && dmsShouldAdoptLateSuccess(committedRef.current, previous, draggingRef.current, committingRef.current, commitEpochRef.current, epochAtCommit)) {
|
|
690
|
+
committedRef.current = next;
|
|
691
|
+
previewRef.current = index;
|
|
692
|
+
setEffort(next);
|
|
693
|
+
setPreview(index);
|
|
694
|
+
setLocalError(null);
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
settled = true;
|
|
699
|
+
window.clearTimeout(timer);
|
|
700
|
+
commitTimerRef.current = null;
|
|
701
|
+
resolve(accepted);
|
|
702
|
+
}, (cause) => {
|
|
703
|
+
if (settled) return;
|
|
704
|
+
settled = true;
|
|
705
|
+
window.clearTimeout(timer);
|
|
706
|
+
commitTimerRef.current = null;
|
|
707
|
+
reject(cause);
|
|
708
|
+
});
|
|
709
|
+
})) throw new Error(t("effort.failed"));
|
|
710
|
+
if (mountedRef.current) {
|
|
711
|
+
committedRef.current = next;
|
|
712
|
+
previewRef.current = index;
|
|
713
|
+
setEffort(next);
|
|
714
|
+
setPreview(index);
|
|
715
|
+
}
|
|
716
|
+
} catch (cause) {
|
|
717
|
+
const restore = Math.max(0, dmsEffortIndex(levels, previous));
|
|
718
|
+
committedRef.current = previous;
|
|
719
|
+
previewRef.current = restore;
|
|
720
|
+
if (mountedRef.current) {
|
|
721
|
+
setEffort(previous);
|
|
722
|
+
setPreview(restore);
|
|
723
|
+
setLocalError(cause instanceof Error ? cause.message : String(cause));
|
|
724
|
+
}
|
|
725
|
+
} finally {
|
|
726
|
+
committingRef.current = false;
|
|
727
|
+
if (mountedRef.current) setCommitting(false);
|
|
728
|
+
}
|
|
729
|
+
}, [
|
|
730
|
+
levels,
|
|
731
|
+
select,
|
|
732
|
+
state,
|
|
733
|
+
t
|
|
734
|
+
]);
|
|
735
|
+
const drag = useEffortDrag({
|
|
736
|
+
levelCount: () => levels.length,
|
|
737
|
+
canStart: () => !busy,
|
|
738
|
+
onPreview: showPointerPreview,
|
|
739
|
+
onCommit: (raw) => void commit(raw),
|
|
740
|
+
onRollback: rollback,
|
|
741
|
+
inputRef,
|
|
742
|
+
draggingRef
|
|
743
|
+
});
|
|
744
|
+
const { dragging } = drag;
|
|
745
|
+
react.useEffect(() => {
|
|
746
|
+
radiationRef.current.dragging = dragging;
|
|
747
|
+
redrawRef.current?.();
|
|
748
|
+
}, [dragging]);
|
|
749
|
+
const onKeyDown = (event) => {
|
|
750
|
+
const current = dmsClampIndex(Number(event.currentTarget.value), levels.length);
|
|
751
|
+
let target;
|
|
752
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowDown" || event.key === "PageDown") target = Math.max(0, current - 1);
|
|
753
|
+
else if (event.key === "ArrowRight" || event.key === "ArrowUp" || event.key === "PageUp") target = Math.min(levels.length - 1, current + 1);
|
|
754
|
+
else if (event.key === "Home") target = 0;
|
|
755
|
+
else if (event.key === "End") target = levels.length - 1;
|
|
756
|
+
if (target === void 0) return;
|
|
757
|
+
event.preventDefault();
|
|
758
|
+
if (busy) return;
|
|
759
|
+
commit(target);
|
|
760
|
+
};
|
|
761
|
+
if (!available) return null;
|
|
762
|
+
const count = levels.length;
|
|
763
|
+
const effortName = levels[dmsEffortIndex(levels, effort)]?.name ?? effort;
|
|
764
|
+
const effortDesc = levels[dmsEffortIndex(levels, effort)]?.description;
|
|
765
|
+
const style = { "--dms-progress": `${preview / (count - 1) * 100}%` };
|
|
766
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
767
|
+
className: `dms-effort${dragging ? " is-dragging" : ""}${busy ? " is-busy" : ""}${error === null ? "" : " is-error"}`,
|
|
768
|
+
children: [
|
|
769
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
770
|
+
className: "dms-effort-slider",
|
|
771
|
+
style,
|
|
772
|
+
children: [
|
|
773
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
774
|
+
className: "dms-effort-track",
|
|
775
|
+
"aria-hidden": "true"
|
|
776
|
+
}),
|
|
777
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
778
|
+
className: "dms-effort-fx",
|
|
779
|
+
"aria-hidden": "true",
|
|
780
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
|
|
781
|
+
ref: canvasRef,
|
|
782
|
+
className: "dms-effort-canvas"
|
|
783
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "dms-effort-flare" })]
|
|
784
|
+
}),
|
|
785
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
786
|
+
ref: inputRef,
|
|
787
|
+
className: "dms-effort-input",
|
|
788
|
+
type: "range",
|
|
789
|
+
min: "0",
|
|
790
|
+
max: count - 1,
|
|
791
|
+
step: "0.01",
|
|
792
|
+
value: preview,
|
|
793
|
+
"aria-disabled": busy,
|
|
794
|
+
"aria-label": t("menu.effort"),
|
|
795
|
+
"aria-valuetext": effortName,
|
|
796
|
+
onChange: (event) => {
|
|
797
|
+
const raw = Number(event.currentTarget.value);
|
|
798
|
+
showPointerPreview(raw);
|
|
799
|
+
},
|
|
800
|
+
onPointerDown: drag.handlers.onPointerDown,
|
|
801
|
+
onPointerMove: drag.handlers.onPointerMove,
|
|
802
|
+
onPointerUp: drag.handlers.onPointerUp,
|
|
803
|
+
onPointerCancel: drag.handlers.onPointerCancel,
|
|
804
|
+
onBlur: drag.handlers.onBlur,
|
|
805
|
+
onKeyDown
|
|
806
|
+
}),
|
|
807
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
808
|
+
className: "dms-effort-knob",
|
|
809
|
+
"aria-hidden": "true"
|
|
810
|
+
})
|
|
811
|
+
]
|
|
812
|
+
}),
|
|
813
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
814
|
+
className: "dms-effort-value",
|
|
815
|
+
children: effortName
|
|
816
|
+
}),
|
|
817
|
+
effortDesc === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
818
|
+
className: "dms-effort-desc",
|
|
819
|
+
children: effortDesc
|
|
820
|
+
}),
|
|
821
|
+
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
822
|
+
className: "dms-sr",
|
|
823
|
+
role: "status",
|
|
824
|
+
children: error
|
|
825
|
+
}),
|
|
826
|
+
error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
827
|
+
className: "dms-effort-error",
|
|
828
|
+
children: error
|
|
829
|
+
})
|
|
830
|
+
]
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
/**
|
|
834
|
+
* 菜单里的一行模型。独立成 memo 组件:目录可能数百行,搜索输入每击键都会
|
|
835
|
+
* 重建菜单内容,行 props(group/model 引用、selected/busy/rowKey、稳定的
|
|
836
|
+
* onChoose)稳定时 React 直接跳过 reconcile,只重渲染真正变化的那行。
|
|
837
|
+
*
|
|
838
|
+
* 自定义比较器(默认浅比较的补充):nameHit 是每次击键新建的对象(引用必变),
|
|
839
|
+
* 但 100 条命中里绝大多数命中区间不变——按值比较 start/end,值相同即跳过
|
|
840
|
+
* 重渲染,搜索才不卡顿。
|
|
841
|
+
*/
|
|
842
|
+
const ModelOption = react.memo(function ModelOption({ group, model, showProvider, selected, busy, rowKey, active, onRowFocus, nameHit, t, onChoose }) {
|
|
843
|
+
const hit = nameHit === void 0 || nameHit === null || nameHit.start === nameHit.end ? null : [
|
|
844
|
+
model.name.slice(0, nameHit.start),
|
|
845
|
+
model.name.slice(nameHit.start, nameHit.end),
|
|
846
|
+
model.name.slice(nameHit.end)
|
|
847
|
+
];
|
|
848
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
849
|
+
type: "button",
|
|
850
|
+
role: "menuitemradio",
|
|
851
|
+
"aria-checked": selected,
|
|
852
|
+
"data-row-key": rowKey,
|
|
853
|
+
tabIndex: active ? 0 : -1,
|
|
854
|
+
className: `dms-model-option${selected ? " dms-model-optionSelected" : ""}`,
|
|
855
|
+
title: model.description === void 0 ? model.name : `${model.name} — ${model.description}`,
|
|
856
|
+
"aria-disabled": busy,
|
|
857
|
+
onFocus: () => onRowFocus(rowKey),
|
|
858
|
+
onClick: () => onChoose({
|
|
859
|
+
provider: group.id,
|
|
860
|
+
model: model.id
|
|
861
|
+
}),
|
|
862
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
863
|
+
className: "dms-model-option-copy",
|
|
864
|
+
children: [
|
|
865
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
866
|
+
className: "dms-nameRow",
|
|
867
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
868
|
+
className: "dms-model-option-name",
|
|
869
|
+
children: hit === null ? model.name : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
870
|
+
hit[0],
|
|
871
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
872
|
+
className: "dms-hit",
|
|
873
|
+
children: hit[1]
|
|
874
|
+
}),
|
|
875
|
+
hit[2]
|
|
876
|
+
] })
|
|
877
|
+
}), model.reasoning !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
878
|
+
className: "dms-badge",
|
|
879
|
+
title: t("badge.reasoningHint"),
|
|
880
|
+
children: t("badge.reasoning")
|
|
881
|
+
})]
|
|
882
|
+
}),
|
|
883
|
+
model.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
884
|
+
className: "dms-model-option-desc",
|
|
885
|
+
children: model.description
|
|
886
|
+
}),
|
|
887
|
+
showProvider && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
888
|
+
className: "dms-model-option-provider",
|
|
889
|
+
children: group.name
|
|
890
|
+
})
|
|
891
|
+
]
|
|
892
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
893
|
+
className: "dms-model-check",
|
|
894
|
+
"aria-hidden": "true",
|
|
895
|
+
children: selected ? IconCheck : null
|
|
896
|
+
})]
|
|
897
|
+
});
|
|
898
|
+
}, (prev, next) => {
|
|
899
|
+
if (prev.group !== next.group) return false;
|
|
900
|
+
if (prev.model !== next.model) return false;
|
|
901
|
+
if (prev.showProvider !== next.showProvider) return false;
|
|
902
|
+
if (prev.selected !== next.selected) return false;
|
|
903
|
+
if (prev.busy !== next.busy) return false;
|
|
904
|
+
if (prev.rowKey !== next.rowKey) return false;
|
|
905
|
+
if (prev.active !== next.active) return false;
|
|
906
|
+
if (prev.onRowFocus !== next.onRowFocus) return false;
|
|
907
|
+
if (prev.t !== next.t) return false;
|
|
908
|
+
if (prev.onChoose !== next.onChoose) return false;
|
|
909
|
+
if (prev.nameHit === next.nameHit) return true;
|
|
910
|
+
if (prev.nameHit === null || prev.nameHit === void 0) return false;
|
|
911
|
+
if (next.nameHit === null || next.nameHit === void 0) return false;
|
|
912
|
+
return prev.nameHit.start === next.nameHit.start && prev.nameHit.end === next.nameHit.end;
|
|
913
|
+
});
|
|
914
|
+
function ModelSelect({ locked, available, directory, load, select, t }) {
|
|
915
|
+
const state = react.useSyncExternalStore(directory.subscribe, directory.getSnapshot);
|
|
916
|
+
const [open, setOpen] = react.useState(false);
|
|
917
|
+
const [menuAbove, setMenuAbove] = react.useState(true);
|
|
918
|
+
const [query, setQuery] = react.useState("");
|
|
919
|
+
const [collapsed, setCollapsed] = react.useState(() => /* @__PURE__ */ new Set());
|
|
920
|
+
const [notice, setNotice] = react.useState(null);
|
|
921
|
+
const [activeRowKey, setActiveRowKey] = react.useState(null);
|
|
922
|
+
const onRowFocus = react.useCallback((key) => {
|
|
923
|
+
setActiveRowKey(key);
|
|
924
|
+
}, []);
|
|
925
|
+
const [toast, setToast] = react.useState(null);
|
|
926
|
+
const toastSeqRef = react.useRef(0);
|
|
927
|
+
const showToast = react.useCallback((text, failed = true) => {
|
|
928
|
+
toastSeqRef.current += 1;
|
|
929
|
+
setToast({
|
|
930
|
+
seq: toastSeqRef.current,
|
|
931
|
+
text,
|
|
932
|
+
failed
|
|
933
|
+
});
|
|
934
|
+
}, []);
|
|
935
|
+
const lastActionRef = react.useRef("load");
|
|
936
|
+
const rootRef = react.useRef(null);
|
|
937
|
+
const triggerRef = react.useRef(null);
|
|
938
|
+
const searchRef = react.useRef(null);
|
|
939
|
+
const lastLoadRef = react.useRef(0);
|
|
940
|
+
const id = react.useId();
|
|
941
|
+
const choices = react.useMemo(() => state.groups.flatMap((group) => group.models.map((model) => ({
|
|
942
|
+
group,
|
|
943
|
+
model,
|
|
944
|
+
haystack: `${model.name}\n${model.description ?? ""}\n${group.name}\n${model.id}\n${group.id}`.toLowerCase(),
|
|
945
|
+
selection: {
|
|
946
|
+
provider: group.id,
|
|
947
|
+
model: model.id,
|
|
948
|
+
...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
|
|
949
|
+
}
|
|
950
|
+
}))), [state.groups]);
|
|
951
|
+
const currentChoice = react.useMemo(() => {
|
|
952
|
+
const current = state.current;
|
|
953
|
+
if (current === null) return void 0;
|
|
954
|
+
return choices.find((c) => c.selection.provider === current.provider && c.selection.model === current.model);
|
|
955
|
+
}, [choices, state.current]);
|
|
956
|
+
const reasoning = currentChoice?.model.reasoning;
|
|
957
|
+
const effectiveEffort = state.current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
958
|
+
const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
|
|
959
|
+
const busy = state.status === "selecting";
|
|
960
|
+
const normalized = query.trim().toLowerCase();
|
|
961
|
+
const hits = react.useMemo(() => {
|
|
962
|
+
if (normalized === "") return null;
|
|
963
|
+
const items = [];
|
|
964
|
+
let total = 0;
|
|
965
|
+
for (const choice of choices) {
|
|
966
|
+
if (!choice.haystack.includes(normalized)) continue;
|
|
967
|
+
total += 1;
|
|
968
|
+
if (items.length >= MAX_VISIBLE_HITS) continue;
|
|
969
|
+
let nameHit = null;
|
|
970
|
+
const at = choice.model.name.toLowerCase().indexOf(normalized);
|
|
971
|
+
if (at >= 0) nameHit = {
|
|
972
|
+
start: at,
|
|
973
|
+
end: at + normalized.length
|
|
974
|
+
};
|
|
975
|
+
items.push({
|
|
976
|
+
group: choice.group,
|
|
977
|
+
model: choice.model,
|
|
978
|
+
nameHit
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
items,
|
|
983
|
+
total
|
|
984
|
+
};
|
|
985
|
+
}, [choices, normalized]);
|
|
986
|
+
const defaultRowKey = react.useMemo(() => dmsDefaultRowKey({
|
|
987
|
+
hitKeys: hits === null ? null : hits.items.map((h) => `${h.group.id}\u0000${h.model.id}`),
|
|
988
|
+
modelKeys: state.groups.flatMap((g) => collapsed.has(g.id) ? [] : g.models.map((m) => `${g.id}\u0000${m.id}`)),
|
|
989
|
+
headerKeys: state.groups.map((g) => `header:${g.id}`),
|
|
990
|
+
selectedKey: state.current === null ? null : `${state.current.provider}\u0000${state.current.model}`
|
|
991
|
+
}), [
|
|
992
|
+
hits,
|
|
993
|
+
state.groups,
|
|
994
|
+
collapsed,
|
|
995
|
+
state.current
|
|
996
|
+
]);
|
|
997
|
+
react.useEffect(() => {
|
|
998
|
+
if (!open) {
|
|
999
|
+
setActiveRowKey(null);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
setActiveRowKey(defaultRowKey);
|
|
1003
|
+
}, [open, defaultRowKey]);
|
|
1004
|
+
const reload = react.useCallback(() => {
|
|
1005
|
+
lastActionRef.current = "load";
|
|
1006
|
+
lastLoadRef.current = Date.now();
|
|
1007
|
+
load();
|
|
1008
|
+
}, [load]);
|
|
1009
|
+
react.useEffect(() => {
|
|
1010
|
+
if (available) {
|
|
1011
|
+
if (Date.now() - lastLoadRef.current < DIRECTORY_STALE_MS && state.status === "ready" && state.groups.length > 0) return;
|
|
1012
|
+
lastActionRef.current = "load";
|
|
1013
|
+
lastLoadRef.current = Date.now();
|
|
1014
|
+
load();
|
|
1015
|
+
}
|
|
1016
|
+
}, [available, load]);
|
|
1017
|
+
const menuRef = react.useRef(null);
|
|
1018
|
+
const inactiveMenuRef = react.useRef(null);
|
|
1019
|
+
const menuMaxHeight = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredMaxHeight)(menuAbove ? menuRef : inactiveMenuRef, 420, open);
|
|
1020
|
+
const [belowMaxHeight, setBelowMaxHeight] = react.useState(420);
|
|
1021
|
+
const [menuLeft, setMenuLeft] = react.useState(void 0);
|
|
1022
|
+
react.useLayoutEffect(() => {
|
|
1023
|
+
if (!open) return;
|
|
1024
|
+
const measure = () => {
|
|
1025
|
+
const trigger = triggerRef.current;
|
|
1026
|
+
if (trigger === null) return;
|
|
1027
|
+
const rect = trigger.getBoundingClientRect();
|
|
1028
|
+
setMenuAbove(dmsMenuAbove(rect.top, rect.bottom, window.innerHeight));
|
|
1029
|
+
setBelowMaxHeight(dmsBelowMaxHeight(rect.bottom, window.innerHeight, 420));
|
|
1030
|
+
const menuWidth = menuRef.current?.offsetWidth ?? 0;
|
|
1031
|
+
setMenuLeft(dmsMenuLeft(rect.right, menuWidth, window.innerWidth, 12));
|
|
1032
|
+
};
|
|
1033
|
+
measure();
|
|
1034
|
+
let raf = 0;
|
|
1035
|
+
const onViewportChange = () => {
|
|
1036
|
+
if (raf !== 0) return;
|
|
1037
|
+
raf = requestAnimationFrame(() => {
|
|
1038
|
+
raf = 0;
|
|
1039
|
+
measure();
|
|
1040
|
+
});
|
|
1041
|
+
};
|
|
1042
|
+
window.addEventListener("resize", onViewportChange, { passive: true });
|
|
1043
|
+
window.addEventListener("scroll", onViewportChange, {
|
|
1044
|
+
capture: true,
|
|
1045
|
+
passive: true
|
|
1046
|
+
});
|
|
1047
|
+
return () => {
|
|
1048
|
+
if (raf !== 0) cancelAnimationFrame(raf);
|
|
1049
|
+
window.removeEventListener("resize", onViewportChange);
|
|
1050
|
+
window.removeEventListener("scroll", onViewportChange, { capture: true });
|
|
1051
|
+
};
|
|
1052
|
+
}, [open]);
|
|
1053
|
+
react.useEffect(() => {
|
|
1054
|
+
if (!open) return;
|
|
1055
|
+
searchRef.current?.focus();
|
|
1056
|
+
}, [open]);
|
|
1057
|
+
const scrollSelectedIntoView = () => {
|
|
1058
|
+
const list = menuRef.current?.querySelector(".dms-groups");
|
|
1059
|
+
if (list === null || list === void 0) return;
|
|
1060
|
+
const maxScroll = list.scrollHeight - list.clientHeight;
|
|
1061
|
+
if (list.scrollTop > maxScroll) list.scrollTop = Math.max(0, maxScroll);
|
|
1062
|
+
const selected = state.current;
|
|
1063
|
+
const row = list.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]");
|
|
1064
|
+
if (row === null) {
|
|
1065
|
+
if (selected === null) return;
|
|
1066
|
+
const section = list.querySelector(`section[data-group-id="${globalThis.CSS.escape(selected.provider)}"]`);
|
|
1067
|
+
if (section === null) return;
|
|
1068
|
+
const sectionRect = section.getBoundingClientRect();
|
|
1069
|
+
const listRect = list.getBoundingClientRect();
|
|
1070
|
+
if (sectionRect.top < listRect.top) list.scrollTop += sectionRect.top - listRect.top;
|
|
1071
|
+
else if (sectionRect.bottom > listRect.bottom) list.scrollTop += sectionRect.bottom - listRect.bottom;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const rowRect = row.getBoundingClientRect();
|
|
1075
|
+
const listRect = list.getBoundingClientRect();
|
|
1076
|
+
const header = row.closest("section")?.querySelector(".dms-groupHeader");
|
|
1077
|
+
const headerHeight = header === null || header === void 0 ? 0 : header.getBoundingClientRect().height;
|
|
1078
|
+
if (rowRect.top < listRect.top + headerHeight) list.scrollTop += rowRect.top - (listRect.top + headerHeight);
|
|
1079
|
+
else if (rowRect.bottom > listRect.bottom) list.scrollTop += rowRect.bottom - listRect.bottom;
|
|
1080
|
+
};
|
|
1081
|
+
const expandedProviderRef = react.useRef(null);
|
|
1082
|
+
react.useLayoutEffect(() => {
|
|
1083
|
+
if (!open) {
|
|
1084
|
+
expandedProviderRef.current = null;
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
if (hits !== null) return;
|
|
1088
|
+
const provider = state.current?.provider ?? null;
|
|
1089
|
+
if (provider !== null && provider !== expandedProviderRef.current && collapsed.has(provider)) {
|
|
1090
|
+
expandedProviderRef.current = provider;
|
|
1091
|
+
setCollapsed((prev) => {
|
|
1092
|
+
if (!prev.has(provider)) return prev;
|
|
1093
|
+
const next = new Set(prev);
|
|
1094
|
+
next.delete(provider);
|
|
1095
|
+
return next;
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
scrollSelectedIntoView();
|
|
1099
|
+
}, [
|
|
1100
|
+
open,
|
|
1101
|
+
state.groups,
|
|
1102
|
+
state.current,
|
|
1103
|
+
collapsed,
|
|
1104
|
+
hits
|
|
1105
|
+
]);
|
|
1106
|
+
const show = react.useCallback(() => {
|
|
1107
|
+
setOpen(true);
|
|
1108
|
+
if (state.status === "error" || state.groups.length === 0 || Date.now() - lastLoadRef.current > DIRECTORY_STALE_MS) reload();
|
|
1109
|
+
}, [
|
|
1110
|
+
state.status,
|
|
1111
|
+
state.groups.length,
|
|
1112
|
+
reload
|
|
1113
|
+
]);
|
|
1114
|
+
const close = react.useCallback((restoreFocus = false) => {
|
|
1115
|
+
setOpen(false);
|
|
1116
|
+
setNotice(null);
|
|
1117
|
+
setQuery("");
|
|
1118
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
1119
|
+
triggerRef.current?.focus();
|
|
1120
|
+
});
|
|
1121
|
+
}, []);
|
|
1122
|
+
const dismissOnOutsidePointer = react.useCallback(() => {
|
|
1123
|
+
close(true);
|
|
1124
|
+
}, [close]);
|
|
1125
|
+
(0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, open, dismissOnOutsidePointer);
|
|
1126
|
+
react.useEffect(() => {
|
|
1127
|
+
if (locked && open) close();
|
|
1128
|
+
}, [
|
|
1129
|
+
locked,
|
|
1130
|
+
open,
|
|
1131
|
+
close
|
|
1132
|
+
]);
|
|
1133
|
+
const choose = react.useCallback((selection) => {
|
|
1134
|
+
if (busy) return;
|
|
1135
|
+
if (state.current?.provider === selection.provider && state.current.model === selection.model) {
|
|
1136
|
+
setNotice(t("notice.already"));
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const target = choices.find((c) => c.selection.provider === selection.provider && c.selection.model === selection.model);
|
|
1140
|
+
const max = target?.model.reasoning === void 0 ? void 0 : maxEffortOf(target.model.reasoning);
|
|
1141
|
+
const effort = max === "off" ? void 0 : max;
|
|
1142
|
+
const full = {
|
|
1143
|
+
provider: selection.provider,
|
|
1144
|
+
model: selection.model,
|
|
1145
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
1146
|
+
};
|
|
1147
|
+
lastActionRef.current = "select";
|
|
1148
|
+
const autoRaised = effort !== void 0 && effort !== target?.model.reasoning?.defaultEffort;
|
|
1149
|
+
const autoName = target?.model.reasoning?.efforts.find((level) => level.id === effort)?.name ?? effort ?? "";
|
|
1150
|
+
(async () => {
|
|
1151
|
+
try {
|
|
1152
|
+
if (!await new Promise((resolve, reject) => {
|
|
1153
|
+
let settled = false;
|
|
1154
|
+
const timer = window.setTimeout(() => {
|
|
1155
|
+
if (settled) return;
|
|
1156
|
+
settled = true;
|
|
1157
|
+
reject(new Error(t("notice.selectFailed")));
|
|
1158
|
+
}, EFFORT_COMMIT_TIMEOUT_MS);
|
|
1159
|
+
select(full).then((ok) => {
|
|
1160
|
+
if (settled) return;
|
|
1161
|
+
settled = true;
|
|
1162
|
+
window.clearTimeout(timer);
|
|
1163
|
+
resolve(ok);
|
|
1164
|
+
}, (cause) => {
|
|
1165
|
+
if (settled) return;
|
|
1166
|
+
settled = true;
|
|
1167
|
+
window.clearTimeout(timer);
|
|
1168
|
+
reject(cause);
|
|
1169
|
+
});
|
|
1170
|
+
})) {
|
|
1171
|
+
const message = directory.getSnapshot().error;
|
|
1172
|
+
showToast(message !== null ? t("error.action", { message }) : t("notice.selectFailed"));
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (rootRef.current !== null) {
|
|
1176
|
+
close(true);
|
|
1177
|
+
if (autoRaised) showToast(t("toast.effortAuto", { effort: autoName }), false);
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
showToast(t("notice.selectFailed"));
|
|
1181
|
+
}
|
|
1182
|
+
})();
|
|
1183
|
+
}, [
|
|
1184
|
+
busy,
|
|
1185
|
+
state.current,
|
|
1186
|
+
choices,
|
|
1187
|
+
select,
|
|
1188
|
+
t,
|
|
1189
|
+
directory,
|
|
1190
|
+
close,
|
|
1191
|
+
showToast
|
|
1192
|
+
]);
|
|
1193
|
+
const toggleCollapse = react.useCallback((groupId) => {
|
|
1194
|
+
setCollapsed((prev) => {
|
|
1195
|
+
const next = new Set(prev);
|
|
1196
|
+
if (next.has(groupId)) next.delete(groupId);
|
|
1197
|
+
else next.add(groupId);
|
|
1198
|
+
return next;
|
|
1199
|
+
});
|
|
1200
|
+
}, []);
|
|
1201
|
+
if (!available) return null;
|
|
1202
|
+
const moveFocus = (offset) => {
|
|
1203
|
+
const menu = menuRef.current;
|
|
1204
|
+
if (menu === null) return;
|
|
1205
|
+
const items = [...menu.querySelectorAll("[data-row-key]")];
|
|
1206
|
+
if (items.length === 0) return;
|
|
1207
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
1208
|
+
items[((active < 0 ? offset > 0 ? -1 : 0 : active) + offset + items.length) % items.length]?.focus();
|
|
1209
|
+
};
|
|
1210
|
+
const focusEdge = (last) => {
|
|
1211
|
+
const items = menuRef.current?.querySelectorAll("[data-row-key]");
|
|
1212
|
+
if (items === void 0 || items.length === 0) return;
|
|
1213
|
+
(last ? items[items.length - 1] : items[0])?.focus();
|
|
1214
|
+
};
|
|
1215
|
+
const onRootKeyDown = (event) => {
|
|
1216
|
+
if (event.nativeEvent.isComposing) return;
|
|
1217
|
+
const target = event.target;
|
|
1218
|
+
if (event.key === "Escape" && open) {
|
|
1219
|
+
if (query !== "" && rootRef.current?.contains(document.activeElement) === true) {
|
|
1220
|
+
const fromSearch = target instanceof HTMLInputElement && target === searchRef.current;
|
|
1221
|
+
setNotice(null);
|
|
1222
|
+
setQuery("");
|
|
1223
|
+
if (!fromSearch) searchRef.current?.focus();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
event.preventDefault();
|
|
1227
|
+
close(true);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
if (!open) return;
|
|
1231
|
+
const fromSearch = target instanceof HTMLInputElement && target === searchRef.current;
|
|
1232
|
+
if ((event.key === "ArrowDown" || event.key === "ArrowUp") && (!(target instanceof HTMLInputElement) || fromSearch)) {
|
|
1233
|
+
event.preventDefault();
|
|
1234
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
if ((event.key === "Home" || event.key === "End") && !(target instanceof HTMLInputElement)) {
|
|
1238
|
+
event.preventDefault();
|
|
1239
|
+
focusEdge(event.key === "End");
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (event.key === "Enter" && fromSearch) {
|
|
1243
|
+
if (hits !== null && hits.items.length > 0) {
|
|
1244
|
+
event.preventDefault();
|
|
1245
|
+
const first = hits.items[0];
|
|
1246
|
+
choose({
|
|
1247
|
+
provider: first.group.id,
|
|
1248
|
+
model: first.model.id
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
const onBlur = (event) => {
|
|
1254
|
+
if (busy) return;
|
|
1255
|
+
const related = event.relatedTarget;
|
|
1256
|
+
if (related instanceof Node) {
|
|
1257
|
+
if (rootRef.current?.contains(related)) return;
|
|
1258
|
+
close();
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
close();
|
|
1262
|
+
};
|
|
1263
|
+
const waiting = state.current === null && state.status === "loading";
|
|
1264
|
+
const modelLabel = currentChoice?.model.name ?? (waiting ? t("trigger.loading") : state.current === null ? t("trigger.fallback") : `${state.current.provider}/${state.current.model}`);
|
|
1265
|
+
const providerLabel = currentChoice?.group.name;
|
|
1266
|
+
const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
|
|
1267
|
+
const triggerTitle = providerLabel === void 0 ? triggerLabel : `${providerLabel} · ${triggerLabel}`;
|
|
1268
|
+
const triggerAria = waiting ? t("trigger.loading") : currentChoice === void 0 ? state.current === null ? t("trigger.selectAria") : t("trigger.aria", { model: `${state.current.provider}/${state.current.model}` }) : effortLabel === void 0 ? t("trigger.aria", { model: providerLabel === void 0 ? modelLabel : `${providerLabel} ${modelLabel}` }) : t("trigger.ariaEffort", {
|
|
1269
|
+
model: providerLabel === void 0 ? modelLabel : `${providerLabel} ${modelLabel}`,
|
|
1270
|
+
effort: effortLabel
|
|
1271
|
+
});
|
|
1272
|
+
const renderErrorStrip = () => {
|
|
1273
|
+
if (state.error !== null && lastActionRef.current === "load") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1274
|
+
className: "dms-error",
|
|
1275
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("error.action", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1276
|
+
type: "button",
|
|
1277
|
+
className: "dms-retry",
|
|
1278
|
+
onClick: reload,
|
|
1279
|
+
children: t("action.reload")
|
|
1280
|
+
})]
|
|
1281
|
+
});
|
|
1282
|
+
return null;
|
|
1283
|
+
};
|
|
1284
|
+
const renderFailures = (failures) => failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1285
|
+
className: "dms-warning",
|
|
1286
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("warning.groupLoad", {
|
|
1287
|
+
name: failure.name,
|
|
1288
|
+
message: failure.message
|
|
1289
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1290
|
+
type: "button",
|
|
1291
|
+
className: "dms-retry",
|
|
1292
|
+
onClick: reload,
|
|
1293
|
+
children: t("action.reload")
|
|
1294
|
+
})]
|
|
1295
|
+
}, failure.id));
|
|
1296
|
+
const noHits = hits !== null && hits.items.length === 0;
|
|
1297
|
+
const noModels = hits === null && state.status === "ready" && choices.length === 0;
|
|
1298
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1299
|
+
ref: rootRef,
|
|
1300
|
+
className: "dms-root",
|
|
1301
|
+
onKeyDown: onRootKeyDown,
|
|
1302
|
+
onBlur,
|
|
1303
|
+
children: [
|
|
1304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1305
|
+
ref: triggerRef,
|
|
1306
|
+
type: "button",
|
|
1307
|
+
className: "dms-trigger",
|
|
1308
|
+
"aria-label": triggerAria,
|
|
1309
|
+
"aria-haspopup": "menu",
|
|
1310
|
+
"aria-expanded": open,
|
|
1311
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
1312
|
+
title: triggerTitle,
|
|
1313
|
+
disabled: locked,
|
|
1314
|
+
onClick: () => open ? close() : show(),
|
|
1315
|
+
children: [
|
|
1316
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1317
|
+
className: "dms-triggerLabel",
|
|
1318
|
+
children: modelLabel
|
|
1319
|
+
}),
|
|
1320
|
+
providerLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1321
|
+
className: "dms-triggerProvider",
|
|
1322
|
+
children: providerLabel
|
|
1323
|
+
}),
|
|
1324
|
+
effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1325
|
+
className: "dms-triggerEffort",
|
|
1326
|
+
children: effortLabel
|
|
1327
|
+
}),
|
|
1328
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1329
|
+
className: `dms-chevron${open ? " dms-chevronOpen" : ""}`,
|
|
1330
|
+
"aria-hidden": "true",
|
|
1331
|
+
children: IconChevronDown
|
|
1332
|
+
})
|
|
1333
|
+
]
|
|
1334
|
+
}),
|
|
1335
|
+
open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1336
|
+
id: `${id}-menu`,
|
|
1337
|
+
ref: menuRef,
|
|
1338
|
+
style: {
|
|
1339
|
+
maxHeight: menuAbove ? menuMaxHeight : belowMaxHeight,
|
|
1340
|
+
...menuLeft === void 0 ? null : {
|
|
1341
|
+
left: menuLeft,
|
|
1342
|
+
right: "auto"
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
className: "dms-menu dms-menuModel" + (menuAbove ? "" : " dms-menuBelow"),
|
|
1346
|
+
"aria-busy": state.status === "loading" || busy,
|
|
1347
|
+
children: [
|
|
1348
|
+
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1349
|
+
className: "dms-status",
|
|
1350
|
+
children: t("status.loading")
|
|
1351
|
+
}),
|
|
1352
|
+
renderErrorStrip(),
|
|
1353
|
+
state.failures.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1354
|
+
className: "dms-failures",
|
|
1355
|
+
children: renderFailures(state.failures)
|
|
1356
|
+
}),
|
|
1357
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1358
|
+
className: "dms-search",
|
|
1359
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1360
|
+
ref: searchRef,
|
|
1361
|
+
type: "text",
|
|
1362
|
+
className: "dms-searchInput",
|
|
1363
|
+
value: query,
|
|
1364
|
+
placeholder: t("search.placeholder"),
|
|
1365
|
+
"aria-label": t("search.placeholder"),
|
|
1366
|
+
"aria-controls": `${id}-groups`,
|
|
1367
|
+
autoComplete: "off",
|
|
1368
|
+
enterKeyHint: "search",
|
|
1369
|
+
spellCheck: false,
|
|
1370
|
+
onChange: (event) => {
|
|
1371
|
+
setNotice(null);
|
|
1372
|
+
setQuery(event.target.value);
|
|
1373
|
+
}
|
|
1374
|
+
}), query !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1375
|
+
type: "button",
|
|
1376
|
+
className: "dms-searchClear",
|
|
1377
|
+
"aria-label": t("search.clearAria"),
|
|
1378
|
+
onClick: () => {
|
|
1379
|
+
setNotice(null);
|
|
1380
|
+
setQuery("");
|
|
1381
|
+
searchRef.current?.focus();
|
|
1382
|
+
},
|
|
1383
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1384
|
+
"aria-hidden": "true",
|
|
1385
|
+
className: "dms-icon-slot",
|
|
1386
|
+
children: IconClear
|
|
1387
|
+
})
|
|
1388
|
+
})]
|
|
1389
|
+
}),
|
|
1390
|
+
hits !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1391
|
+
className: "dms-sr",
|
|
1392
|
+
role: "status",
|
|
1393
|
+
children: t("search.status", { count: String(hits.total) })
|
|
1394
|
+
}),
|
|
1395
|
+
noHits && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1396
|
+
className: "dms-empty dms-groupsFill",
|
|
1397
|
+
role: "status",
|
|
1398
|
+
children: t("search.noMatch", { query: query.trim() })
|
|
1399
|
+
}),
|
|
1400
|
+
noModels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1401
|
+
className: "dms-empty dms-groupsFill",
|
|
1402
|
+
role: "status",
|
|
1403
|
+
children: t("empty.models")
|
|
1404
|
+
}),
|
|
1405
|
+
!noHits && !noModels && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1406
|
+
className: "dms-groups",
|
|
1407
|
+
id: `${id}-groups`,
|
|
1408
|
+
role: "menu",
|
|
1409
|
+
"aria-label": t("menu.aria"),
|
|
1410
|
+
children: hits !== null ? hits.items.map((hit) => {
|
|
1411
|
+
const rowKey = `${hit.group.id}\u0000${hit.model.id}`;
|
|
1412
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOption, {
|
|
1413
|
+
group: hit.group,
|
|
1414
|
+
model: hit.model,
|
|
1415
|
+
showProvider: true,
|
|
1416
|
+
selected: state.current?.provider === hit.group.id && state.current.model === hit.model.id,
|
|
1417
|
+
busy,
|
|
1418
|
+
rowKey,
|
|
1419
|
+
active: rowKey === activeRowKey,
|
|
1420
|
+
onRowFocus,
|
|
1421
|
+
nameHit: hit.nameHit,
|
|
1422
|
+
t,
|
|
1423
|
+
onChoose: choose
|
|
1424
|
+
}, rowKey);
|
|
1425
|
+
}) : state.groups.map((group) => {
|
|
1426
|
+
const headingId = `${id}-${group.id}`;
|
|
1427
|
+
const isCollapsed = collapsed.has(group.id);
|
|
1428
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1429
|
+
"data-group-id": group.id,
|
|
1430
|
+
role: "group",
|
|
1431
|
+
"aria-labelledby": headingId,
|
|
1432
|
+
className: "dms-group",
|
|
1433
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1434
|
+
type: "button",
|
|
1435
|
+
role: "menuitem",
|
|
1436
|
+
"data-row-key": `header:${group.id}`,
|
|
1437
|
+
tabIndex: -1,
|
|
1438
|
+
className: "dms-groupHeader",
|
|
1439
|
+
"aria-expanded": !isCollapsed,
|
|
1440
|
+
"aria-label": t("group.toggleAria", {
|
|
1441
|
+
name: group.name,
|
|
1442
|
+
count: String(group.models.length)
|
|
1443
|
+
}),
|
|
1444
|
+
onClick: () => toggleCollapse(group.id),
|
|
1445
|
+
children: [
|
|
1446
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1447
|
+
className: `dms-groupChevron${isCollapsed ? " dms-groupChevronClosed" : ""}`,
|
|
1448
|
+
"aria-hidden": "true",
|
|
1449
|
+
children: IconChevronDown
|
|
1450
|
+
}),
|
|
1451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1452
|
+
id: headingId,
|
|
1453
|
+
className: "dms-groupName",
|
|
1454
|
+
children: group.name
|
|
1455
|
+
}),
|
|
1456
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1457
|
+
className: "dms-groupCount",
|
|
1458
|
+
children: group.models.length
|
|
1459
|
+
})
|
|
1460
|
+
]
|
|
1461
|
+
}), !isCollapsed && group.models.map((model) => {
|
|
1462
|
+
const rowKey = `${group.id}\u0000${model.id}`;
|
|
1463
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelOption, {
|
|
1464
|
+
group,
|
|
1465
|
+
model,
|
|
1466
|
+
showProvider: false,
|
|
1467
|
+
selected: state.current?.provider === group.id && state.current.model === model.id,
|
|
1468
|
+
busy,
|
|
1469
|
+
rowKey,
|
|
1470
|
+
active: rowKey === activeRowKey,
|
|
1471
|
+
onRowFocus,
|
|
1472
|
+
t,
|
|
1473
|
+
onChoose: choose
|
|
1474
|
+
}, rowKey);
|
|
1475
|
+
})]
|
|
1476
|
+
}, group.id);
|
|
1477
|
+
})
|
|
1478
|
+
}),
|
|
1479
|
+
hits !== null && hits.total > MAX_VISIBLE_HITS && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1480
|
+
className: "dms-more",
|
|
1481
|
+
role: "note",
|
|
1482
|
+
children: t("search.more", {
|
|
1483
|
+
shown: String(MAX_VISIBLE_HITS),
|
|
1484
|
+
total: String(hits.total)
|
|
1485
|
+
})
|
|
1486
|
+
}),
|
|
1487
|
+
state.current !== null && dmsSliderLevels(state).length >= 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1488
|
+
className: "dms-effortFooter",
|
|
1489
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1490
|
+
className: "dms-effortFooterLabel",
|
|
1491
|
+
children: t("menu.effort")
|
|
1492
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
1493
|
+
state,
|
|
1494
|
+
select,
|
|
1495
|
+
t
|
|
1496
|
+
})]
|
|
1497
|
+
}),
|
|
1498
|
+
notice !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1499
|
+
className: "dms-notice",
|
|
1500
|
+
role: "status",
|
|
1501
|
+
children: notice
|
|
1502
|
+
})
|
|
1503
|
+
]
|
|
1504
|
+
}) : null,
|
|
1505
|
+
toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
1506
|
+
text: toast.text,
|
|
1507
|
+
icon: toast.failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}) : void 0,
|
|
1508
|
+
anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
|
|
1509
|
+
onDone: () => {
|
|
1510
|
+
setToast(null);
|
|
1511
|
+
}
|
|
1512
|
+
}, toast.seq)
|
|
1513
|
+
]
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
//#endregion
|
|
1517
|
+
//#region src/client/styles.ts
|
|
1518
|
+
/**
|
|
1519
|
+
* Stylesheet for the enhanced model seat.
|
|
1520
|
+
*
|
|
1521
|
+
* Injected as one `<style data-plugin="@bittersmilezzz/dsh-model-selector">` tag by the client
|
|
1522
|
+
* apply and removed again on unload. Class names are prefixed `dms-` so they
|
|
1523
|
+
* cannot collide with CSS-module hashes from other plugins. Colors come only
|
|
1524
|
+
* from `--dsw-*` theme tokens, matching the shipped Menu material.
|
|
1525
|
+
*/
|
|
1526
|
+
const CSS = `
|
|
1527
|
+
.dms-root {
|
|
1528
|
+
position: relative;
|
|
1529
|
+
min-width: 0;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.dms-trigger {
|
|
1533
|
+
display: flex;
|
|
1534
|
+
align-items: center;
|
|
1535
|
+
gap: 4px;
|
|
1536
|
+
min-width: 0;
|
|
1537
|
+
max-width: min(420px, calc(100vw - 48px));
|
|
1538
|
+
height: 28px;
|
|
1539
|
+
padding: 0 4px 0 8px;
|
|
1540
|
+
border: none;
|
|
1541
|
+
border-radius: 24px;
|
|
1542
|
+
outline: none;
|
|
1543
|
+
background: transparent;
|
|
1544
|
+
color: var(--dsw-alias-label-secondary);
|
|
1545
|
+
font-size: 13px;
|
|
1546
|
+
line-height: 20px;
|
|
1547
|
+
font-weight: 500;
|
|
1548
|
+
cursor: pointer;
|
|
1549
|
+
}
|
|
1550
|
+
.dms-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
1551
|
+
.dms-trigger:focus-visible { box-shadow: 0 0 0 2px var(--dsw-alias-border-l3); }
|
|
1552
|
+
.dms-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
1553
|
+
|
|
1554
|
+
.dms-triggerLabel {
|
|
1555
|
+
min-width: 0;
|
|
1556
|
+
overflow: hidden;
|
|
1557
|
+
text-overflow: ellipsis;
|
|
1558
|
+
white-space: nowrap;
|
|
1559
|
+
line-height: 20px;
|
|
1560
|
+
}
|
|
1561
|
+
.dms-triggerEffort {
|
|
1562
|
+
flex: 0 0 auto;
|
|
1563
|
+
line-height: 20px;
|
|
1564
|
+
color: var(--dsw-alias-label-caption);
|
|
1565
|
+
}
|
|
1566
|
+
.dms-triggerProvider {
|
|
1567
|
+
flex: 0 0 auto;
|
|
1568
|
+
max-width: 88px;
|
|
1569
|
+
overflow: hidden;
|
|
1570
|
+
text-overflow: ellipsis;
|
|
1571
|
+
white-space: nowrap;
|
|
1572
|
+
color: var(--dsw-alias-label-caption);
|
|
1573
|
+
font-size: 11px;
|
|
1574
|
+
line-height: 20px;
|
|
1575
|
+
font-weight: 400;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
.dms-chevron,
|
|
1579
|
+
.dms-groupChevron {
|
|
1580
|
+
display: inline-flex;
|
|
1581
|
+
align-items: center;
|
|
1582
|
+
justify-content: center;
|
|
1583
|
+
flex: 0 0 auto;
|
|
1584
|
+
line-height: 0;
|
|
1585
|
+
}
|
|
1586
|
+
.dms-chevron {
|
|
1587
|
+
color: var(--dsw-alias-label-caption);
|
|
1588
|
+
transition: transform 120ms ease;
|
|
1589
|
+
}
|
|
1590
|
+
.dms-chevronOpen { transform: rotate(180deg); }
|
|
1591
|
+
.dms-groupChevron {
|
|
1592
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1593
|
+
transition: transform 120ms ease;
|
|
1594
|
+
}
|
|
1595
|
+
.dms-groupChevronClosed { transform: rotate(-90deg); }
|
|
1596
|
+
.dms-model-check svg { display: block; }
|
|
1597
|
+
|
|
1598
|
+
.dms-menu {
|
|
1599
|
+
position: absolute;
|
|
1600
|
+
right: 0;
|
|
1601
|
+
bottom: calc(100% + 8px);
|
|
1602
|
+
z-index: 20;
|
|
1603
|
+
display: flex;
|
|
1604
|
+
flex-direction: column;
|
|
1605
|
+
width: min(280px, calc(100vw - 32px));
|
|
1606
|
+
overflow: hidden;
|
|
1607
|
+
padding: 4px 0 4px 4px;
|
|
1608
|
+
box-sizing: border-box;
|
|
1609
|
+
border: 1px solid var(--dsw-alias-border-inverted);
|
|
1610
|
+
border-radius: 12px;
|
|
1611
|
+
background: var(--dsw-specific-menu);
|
|
1612
|
+
box-shadow: var(--dsw-shadow-lv3);
|
|
1613
|
+
color: var(--dsw-alias-label-primary);
|
|
1614
|
+
animation: dms-menu-in 150ms cubic-bezier(.22,1,.36,1);
|
|
1615
|
+
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
1616
|
+
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
1617
|
+
}
|
|
1618
|
+
.dms-menuModel { border-right: none; }
|
|
1619
|
+
.dms-menuBelow {
|
|
1620
|
+
bottom: auto;
|
|
1621
|
+
top: calc(100% + 8px);
|
|
1622
|
+
animation-name: dms-menu-in-below;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
.dms-status,
|
|
1626
|
+
.dms-empty {
|
|
1627
|
+
flex: 0 0 auto;
|
|
1628
|
+
padding: 10px;
|
|
1629
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1630
|
+
font-size: 13px;
|
|
1631
|
+
line-height: 20px;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
.dms-more {
|
|
1635
|
+
flex: 0 0 auto;
|
|
1636
|
+
padding: 8px 12px;
|
|
1637
|
+
border-top: 1px solid var(--dsw-alias-border-l1, transparent);
|
|
1638
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1639
|
+
font-size: 12px;
|
|
1640
|
+
line-height: 18px;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/* Provider-load failure strip: capped and scrollable so a long list of failed
|
|
1644
|
+
groups can never squeeze the model list out of the menu (E4). */
|
|
1645
|
+
.dms-failures {
|
|
1646
|
+
flex: 0 0 auto;
|
|
1647
|
+
max-height: 96px;
|
|
1648
|
+
min-height: 0;
|
|
1649
|
+
overflow-y: auto;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
.dms-error,
|
|
1653
|
+
.dms-warning {
|
|
1654
|
+
flex: 0 0 auto;
|
|
1655
|
+
display: flex;
|
|
1656
|
+
align-items: flex-start;
|
|
1657
|
+
justify-content: space-between;
|
|
1658
|
+
gap: 8px;
|
|
1659
|
+
margin-bottom: 4px;
|
|
1660
|
+
padding: 7px 8px;
|
|
1661
|
+
border-radius: 8px;
|
|
1662
|
+
background: var(--dsw-alias-interactive-bg-hover-danger);
|
|
1663
|
+
color: var(--dsw-alias-state-error-primary);
|
|
1664
|
+
font-size: 12px;
|
|
1665
|
+
line-height: 18px;
|
|
1666
|
+
}
|
|
1667
|
+
.dms-warning {
|
|
1668
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
1669
|
+
color: var(--dsw-alias-state-warn-label);
|
|
1670
|
+
}
|
|
1671
|
+
.dms-retry {
|
|
1672
|
+
flex: 0 0 auto;
|
|
1673
|
+
padding: 0;
|
|
1674
|
+
border: none;
|
|
1675
|
+
border-radius: 4px;
|
|
1676
|
+
background: transparent;
|
|
1677
|
+
color: inherit;
|
|
1678
|
+
font: inherit;
|
|
1679
|
+
font-weight: 600;
|
|
1680
|
+
cursor: pointer;
|
|
1681
|
+
}
|
|
1682
|
+
.dms-retry:focus-visible { box-shadow: 0 0 0 2px var(--dsw-alias-border-l3); }
|
|
1683
|
+
|
|
1684
|
+
/* Search box pinned above the scrollable list. */
|
|
1685
|
+
.dms-search {
|
|
1686
|
+
position: relative;
|
|
1687
|
+
margin-bottom: 4px;
|
|
1688
|
+
margin-right: 4px;
|
|
1689
|
+
flex: 0 0 auto;
|
|
1690
|
+
}
|
|
1691
|
+
.dms-searchInput {
|
|
1692
|
+
box-sizing: border-box;
|
|
1693
|
+
width: 100%;
|
|
1694
|
+
height: 30px;
|
|
1695
|
+
padding: 0 30px 0 10px;
|
|
1696
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
1697
|
+
border-radius: 8px;
|
|
1698
|
+
outline: none;
|
|
1699
|
+
background: var(--dsw-alias-bg-input);
|
|
1700
|
+
color: var(--dsw-alias-label-primary);
|
|
1701
|
+
font-size: 13px;
|
|
1702
|
+
line-height: 18px;
|
|
1703
|
+
}
|
|
1704
|
+
.dms-searchInput::placeholder { color: var(--dsw-alias-label-tertiary); }
|
|
1705
|
+
.dms-searchInput:focus-visible { box-shadow: 0 0 0 2px var(--dsw-alias-border-l3); }
|
|
1706
|
+
.dms-searchClear {
|
|
1707
|
+
position: absolute;
|
|
1708
|
+
top: 0;
|
|
1709
|
+
right: 0;
|
|
1710
|
+
display: grid;
|
|
1711
|
+
place-items: center;
|
|
1712
|
+
width: 30px;
|
|
1713
|
+
height: 30px;
|
|
1714
|
+
padding: 0;
|
|
1715
|
+
border: none;
|
|
1716
|
+
background: transparent;
|
|
1717
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1718
|
+
cursor: pointer;
|
|
1719
|
+
}
|
|
1720
|
+
.dms-searchClear:hover { color: var(--dsw-alias-label-primary); }
|
|
1721
|
+
/* 键盘焦点此前在这三个可聚焦控件上完全隐身(上轮只补了模型行)。 */
|
|
1722
|
+
.dms-searchClear:focus-visible {
|
|
1723
|
+
border-radius: 8px;
|
|
1724
|
+
box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);
|
|
1725
|
+
}
|
|
1726
|
+
/* 图标组件只收 size/className,装饰性 aria-hidden 只能包一层 span;contents 让它不占布局。 */
|
|
1727
|
+
.dms-icon-slot { display: contents; }
|
|
1728
|
+
|
|
1729
|
+
.dms-groups { flex: 1 1 auto; min-height: 0; overflow-y: auto; contain: content; }
|
|
1730
|
+
/* 空态占位:空态播报节点在 role=menu 容器外,用与 .dms-groups 同款弹性布局
|
|
1731
|
+
补回列表区本来的位置(不挤压其余栏,消息也不会贴到菜单顶部)。 */
|
|
1732
|
+
.dms-groupsFill { flex: 1 1 auto; min-height: 0; overflow-y: auto; contain: content; }
|
|
1733
|
+
.dms-group + .dms-group { margin-top: 4px; }
|
|
1734
|
+
|
|
1735
|
+
/* Collapsible provider header: a full-width toggle button. */
|
|
1736
|
+
.dms-groupHeader {
|
|
1737
|
+
position: sticky;
|
|
1738
|
+
top: 0;
|
|
1739
|
+
z-index: 1;
|
|
1740
|
+
display: flex;
|
|
1741
|
+
align-items: center;
|
|
1742
|
+
gap: 4px;
|
|
1743
|
+
width: 100%;
|
|
1744
|
+
padding: 5px 8px 3px;
|
|
1745
|
+
border: none;
|
|
1746
|
+
outline: none;
|
|
1747
|
+
background: var(--dsw-specific-menu);
|
|
1748
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1749
|
+
font-size: 12px;
|
|
1750
|
+
line-height: 18px;
|
|
1751
|
+
font-weight: 500;
|
|
1752
|
+
text-align: left;
|
|
1753
|
+
cursor: pointer;
|
|
1754
|
+
}
|
|
1755
|
+
.dms-groupHeader:hover { color: var(--dsw-alias-label-primary); }
|
|
1756
|
+
.dms-groupHeader:focus-visible { box-shadow: inset 0 0 0 2px var(--dsw-alias-border-l3); }
|
|
1757
|
+
.dms-groupName {
|
|
1758
|
+
flex: 1 1 auto;
|
|
1759
|
+
min-width: 0;
|
|
1760
|
+
overflow: hidden;
|
|
1761
|
+
text-overflow: ellipsis;
|
|
1762
|
+
white-space: nowrap;
|
|
1763
|
+
}
|
|
1764
|
+
.dms-groupCount { flex: 0 0 auto; color: var(--dsw-alias-label-dimmed); font-weight: 400; }
|
|
1765
|
+
|
|
1766
|
+
.dms-nameRow {
|
|
1767
|
+
display: flex;
|
|
1768
|
+
align-items: center;
|
|
1769
|
+
gap: 6px;
|
|
1770
|
+
min-width: 0;
|
|
1771
|
+
}
|
|
1772
|
+
.dms-badge {
|
|
1773
|
+
flex: 0 0 auto;
|
|
1774
|
+
padding: 0 6px;
|
|
1775
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
1776
|
+
border-radius: 999px;
|
|
1777
|
+
color: var(--dsw-alias-label-caption);
|
|
1778
|
+
font-size: 10px;
|
|
1779
|
+
line-height: 16px;
|
|
1780
|
+
font-weight: 500;
|
|
1781
|
+
}
|
|
1782
|
+
.dms-notice {
|
|
1783
|
+
flex: 0 0 auto;
|
|
1784
|
+
margin: 4px 0 0;
|
|
1785
|
+
padding: 6px 10px;
|
|
1786
|
+
border-radius: 8px;
|
|
1787
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
1788
|
+
color: var(--dsw-alias-label-secondary);
|
|
1789
|
+
font-size: 12px;
|
|
1790
|
+
line-height: 18px;
|
|
1791
|
+
}
|
|
1792
|
+
.dms-effort {
|
|
1793
|
+
display: flex;
|
|
1794
|
+
align-items: center;
|
|
1795
|
+
flex-wrap: wrap;
|
|
1796
|
+
width: 100%;
|
|
1797
|
+
min-width: 0;
|
|
1798
|
+
min-height: 32px;
|
|
1799
|
+
color: var(--dsw-alias-label-secondary);
|
|
1800
|
+
user-select: none;
|
|
1801
|
+
box-sizing: border-box;
|
|
1802
|
+
}
|
|
1803
|
+
/* 档位读数:拖动时实时跟随 preview,替代此前只有读屏能看到的 aria-valuetext。 */
|
|
1804
|
+
.dms-effort-value {
|
|
1805
|
+
flex: 0 0 auto;
|
|
1806
|
+
margin-left: 8px;
|
|
1807
|
+
min-width: 3.2em;
|
|
1808
|
+
color: var(--dsw-alias-label-primary);
|
|
1809
|
+
font-size: 11px;
|
|
1810
|
+
line-height: 16px;
|
|
1811
|
+
text-align: right;
|
|
1812
|
+
}
|
|
1813
|
+
/* 官方契约的 efforts[].description,整行展示在滑杆下方。 */
|
|
1814
|
+
.dms-effort-desc {
|
|
1815
|
+
flex: 1 0 100%;
|
|
1816
|
+
margin-top: 2px;
|
|
1817
|
+
color: var(--dsw-alias-label-tertiary);
|
|
1818
|
+
font-size: 10px;
|
|
1819
|
+
line-height: 14px;
|
|
1820
|
+
}
|
|
1821
|
+
.dms-effort-slider {
|
|
1822
|
+
--dms-progress: 50%;
|
|
1823
|
+
position: relative;
|
|
1824
|
+
width: 100%;
|
|
1825
|
+
height: 30px;
|
|
1826
|
+
flex: 1 1 0;
|
|
1827
|
+
min-width: 0;
|
|
1828
|
+
border-radius: 999px;
|
|
1829
|
+
isolation: isolate;
|
|
1830
|
+
transition: filter 180ms ease;
|
|
1831
|
+
}
|
|
1832
|
+
.dms-effort-track {
|
|
1833
|
+
position: absolute;
|
|
1834
|
+
inset: 0;
|
|
1835
|
+
overflow: hidden;
|
|
1836
|
+
border-radius: inherit;
|
|
1837
|
+
background: linear-gradient(100deg, #03040a 0%, #071126 22%, #101d4c 45%, #302262 70%, #5d35a0 100%);
|
|
1838
|
+
box-shadow:
|
|
1839
|
+
inset 0 1px 0 rgba(189, 199, 255, .15),
|
|
1840
|
+
inset 0 -1px 0 rgba(0, 0, 0, .55),
|
|
1841
|
+
0 3px 10px rgba(12, 17, 55, .34);
|
|
1842
|
+
}
|
|
1843
|
+
.dms-effort-track::after {
|
|
1844
|
+
content: "";
|
|
1845
|
+
position: absolute;
|
|
1846
|
+
inset: 0;
|
|
1847
|
+
background:
|
|
1848
|
+
radial-gradient(circle at 18% 45%, rgba(82, 130, 255, .12), transparent 24%),
|
|
1849
|
+
linear-gradient(90deg, rgba(0, 0, 0, .28), transparent 42%, rgba(168, 113, 255, .12));
|
|
1850
|
+
pointer-events: none;
|
|
1851
|
+
}
|
|
1852
|
+
.dms-effort-fx {
|
|
1853
|
+
position: absolute;
|
|
1854
|
+
z-index: 1;
|
|
1855
|
+
inset: 0;
|
|
1856
|
+
overflow: hidden;
|
|
1857
|
+
border-radius: inherit;
|
|
1858
|
+
pointer-events: none;
|
|
1859
|
+
}
|
|
1860
|
+
.dms-effort-canvas {
|
|
1861
|
+
position: absolute;
|
|
1862
|
+
z-index: 2;
|
|
1863
|
+
inset: 0;
|
|
1864
|
+
width: 100%;
|
|
1865
|
+
height: 100%;
|
|
1866
|
+
opacity: 1;
|
|
1867
|
+
image-rendering: pixelated;
|
|
1868
|
+
mix-blend-mode: screen;
|
|
1869
|
+
transition: filter 140ms ease;
|
|
1870
|
+
}
|
|
1871
|
+
.dms-effort-flare {
|
|
1872
|
+
position: absolute;
|
|
1873
|
+
z-index: 3;
|
|
1874
|
+
transition: left 70ms linear, filter 140ms ease;
|
|
1875
|
+
top: 50%;
|
|
1876
|
+
left: var(--dms-progress);
|
|
1877
|
+
width: 78px;
|
|
1878
|
+
height: 46px;
|
|
1879
|
+
border-radius: 50%;
|
|
1880
|
+
background: radial-gradient(ellipse at 100% 50%, rgba(255,255,255,.96) 0 4%, rgba(188,189,255,.8) 11%, rgba(106,87,255,.5) 28%, rgba(105,31,255,.2) 49%, transparent 74%);
|
|
1881
|
+
filter: blur(2px) saturate(1.25);
|
|
1882
|
+
mix-blend-mode: screen;
|
|
1883
|
+
transform: translate(-100%, -50%);
|
|
1884
|
+
pointer-events: none;
|
|
1885
|
+
}
|
|
1886
|
+
.dms-effort-flare::before,
|
|
1887
|
+
.dms-effort-flare::after {
|
|
1888
|
+
content: "";
|
|
1889
|
+
position: absolute;
|
|
1890
|
+
inset: 50% auto auto 100%;
|
|
1891
|
+
border-radius: 999px;
|
|
1892
|
+
transform: translate(-50%, -50%);
|
|
1893
|
+
}
|
|
1894
|
+
.dms-effort-flare::before {
|
|
1895
|
+
width: 52px;
|
|
1896
|
+
height: 1px;
|
|
1897
|
+
background: linear-gradient(90deg, transparent, rgba(100,160,255,.42), #f1ecff, rgba(193,82,255,.65), transparent);
|
|
1898
|
+
box-shadow: 0 0 7px #9b7cff, 0 0 13px rgba(72,132,255,.64);
|
|
1899
|
+
}
|
|
1900
|
+
.dms-effort-flare::after {
|
|
1901
|
+
width: 1px;
|
|
1902
|
+
height: 20px;
|
|
1903
|
+
background: linear-gradient(180deg, transparent, rgba(196,190,255,.84), transparent);
|
|
1904
|
+
box-shadow: 0 0 7px #9c7cff;
|
|
1905
|
+
}
|
|
1906
|
+
.dms-effort-knob {
|
|
1907
|
+
position: absolute;
|
|
1908
|
+
z-index: 4;
|
|
1909
|
+
top: 50%;
|
|
1910
|
+
left: clamp(14px, var(--dms-progress), calc(100% - 14px));
|
|
1911
|
+
width: 28px;
|
|
1912
|
+
height: 28px;
|
|
1913
|
+
border: 1px solid rgba(255,255,255,.94);
|
|
1914
|
+
border-radius: 50%;
|
|
1915
|
+
background: #fff;
|
|
1916
|
+
box-shadow:
|
|
1917
|
+
0 0 0 2px rgba(92,105,255,.12),
|
|
1918
|
+
0 0 14px rgba(121,82,255,.48),
|
|
1919
|
+
0 2px 7px rgba(0,0,0,.3);
|
|
1920
|
+
transform: translate(-50%, -50%);
|
|
1921
|
+
transition: left 240ms cubic-bezier(.22,1,.36,1), transform 160ms ease, box-shadow 180ms ease;
|
|
1922
|
+
pointer-events: none;
|
|
1923
|
+
}
|
|
1924
|
+
.dms-effort-input {
|
|
1925
|
+
position: absolute;
|
|
1926
|
+
z-index: 5;
|
|
1927
|
+
inset: -5px 0;
|
|
1928
|
+
width: 100%;
|
|
1929
|
+
height: calc(100% + 10px);
|
|
1930
|
+
margin: 0;
|
|
1931
|
+
opacity: 0;
|
|
1932
|
+
cursor: grab;
|
|
1933
|
+
touch-action: none;
|
|
1934
|
+
}
|
|
1935
|
+
.dms-effort-input:active { cursor: grabbing; }
|
|
1936
|
+
.dms-effort-input:focus-visible + .dms-effort-knob {
|
|
1937
|
+
outline: 2px solid var(--dsw-static-blue-400);
|
|
1938
|
+
outline-offset: 2px;
|
|
1939
|
+
}
|
|
1940
|
+
.dms-effort.is-dragging .dms-effort-canvas {
|
|
1941
|
+
filter: saturate(1.45) brightness(1.28) contrast(1.06);
|
|
1942
|
+
}
|
|
1943
|
+
.dms-effort.is-dragging .dms-effort-flare {
|
|
1944
|
+
filter: blur(1.5px) saturate(1.6) brightness(1.42);
|
|
1945
|
+
transition: none;
|
|
1946
|
+
}
|
|
1947
|
+
.dms-effort.is-dragging .dms-effort-knob {
|
|
1948
|
+
transform: translate(-50%, -50%) scale(1.07);
|
|
1949
|
+
transition: none;
|
|
1950
|
+
box-shadow:
|
|
1951
|
+
0 0 0 3px rgba(113,115,255,.25),
|
|
1952
|
+
0 0 20px rgba(74,145,255,.86),
|
|
1953
|
+
0 0 31px rgba(171,53,255,.66),
|
|
1954
|
+
0 3px 8px rgba(0,0,0,.32);
|
|
1955
|
+
}
|
|
1956
|
+
.dms-effort.is-error .dms-effort-slider {
|
|
1957
|
+
outline: 1px solid var(--dsw-alias-state-error-secondary);
|
|
1958
|
+
outline-offset: 2px;
|
|
1959
|
+
}
|
|
1960
|
+
.dms-effort.is-busy { opacity: .72; }
|
|
1961
|
+
.dms-effort-error { flex: 1 0 100%; margin-top: 8px; padding: 6px 10px; border-radius: 8px; color: var(--dsw-alias-state-error-primary, #c83e4d); background: var(--dsw-alias-state-error-tertiary, rgba(220,55,70,.08)); font-size: 11px; line-height: 1.5; }
|
|
1962
|
+
/* 视觉隐藏工具类(SR 播报/错误原文:不占布局但可被读屏读到)。 */
|
|
1963
|
+
.dms-sr {
|
|
1964
|
+
position: absolute;
|
|
1965
|
+
width: 1px;
|
|
1966
|
+
height: 1px;
|
|
1967
|
+
padding: 0;
|
|
1968
|
+
margin: -1px;
|
|
1969
|
+
overflow: hidden;
|
|
1970
|
+
clip: rect(0, 0, 0, 0);
|
|
1971
|
+
white-space: nowrap;
|
|
1972
|
+
border: 0;
|
|
1973
|
+
}
|
|
1974
|
+
.dms-model-option {
|
|
1975
|
+
display: grid;
|
|
1976
|
+
grid-template-columns: minmax(0, 1fr) 20px;
|
|
1977
|
+
align-items: center;
|
|
1978
|
+
gap: 8px;
|
|
1979
|
+
width: 100%;
|
|
1980
|
+
min-height: 38px;
|
|
1981
|
+
padding: 7px 9px;
|
|
1982
|
+
border: 0;
|
|
1983
|
+
border-radius: 9px;
|
|
1984
|
+
color: inherit;
|
|
1985
|
+
background: transparent;
|
|
1986
|
+
font: inherit;
|
|
1987
|
+
text-align: left;
|
|
1988
|
+
cursor: pointer;
|
|
1989
|
+
}
|
|
1990
|
+
.dms-model-option:hover { background: var(--dsw-alias-fill-tertiary, rgba(120,125,140,.09)); }
|
|
1991
|
+
.dms-model-option-copy { min-width: 0; }
|
|
1992
|
+
.dms-model-option-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
|
1993
|
+
/* 搜索命中片段:名称内标下划线(命中描述/供应商时不标,见 nameHit)。 */
|
|
1994
|
+
.dms-hit {
|
|
1995
|
+
text-decoration: underline;
|
|
1996
|
+
text-decoration-color: var(--dsw-static-blue-400, #4d70ff);
|
|
1997
|
+
text-decoration-thickness: 2px;
|
|
1998
|
+
text-underline-offset: 3px;
|
|
1999
|
+
}
|
|
2000
|
+
.dms-model-option-desc { display: block; margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-tertiary, #9296a0); font-size: 10px; }
|
|
2001
|
+
.dms-model-check { color: var(--dsw-static-deepseek-500, #4d70ff); font-size: 15px; text-align: center; }
|
|
2002
|
+
/* 方向键导航把焦点落在选项上,此前没有任何焦点指示。 */
|
|
2003
|
+
.dms-model-option:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
|
|
2004
|
+
/* 当前选中行:仅靠勾号不足以在密集列表里定位。 */
|
|
2005
|
+
.dms-model-optionSelected { background: var(--dsw-alias-interactive-bg-hover); }
|
|
2006
|
+
.dms-model-optionSelected .dms-model-option-name { font-weight: 600; }
|
|
2007
|
+
/* select 进行中:官方以 disabled 变灰表达,这里用 aria-busy 驱动同样的视觉、
|
|
2008
|
+
不夺键盘焦点(aria-disabled 保连续性,见 ModelOption)。 */
|
|
2009
|
+
.dms-menu[aria-busy="true"] .dms-model-option { color: var(--dsw-alias-label-dimmed); }
|
|
2010
|
+
/* 搜索结果里的供应商标是区分跨供应商同名模型的唯一线索,之前无任何样式。 */
|
|
2011
|
+
.dms-model-option-provider {
|
|
2012
|
+
display: block;
|
|
2013
|
+
margin-top: 2px;
|
|
2014
|
+
overflow: hidden;
|
|
2015
|
+
color: var(--dsw-alias-label-dimmed, #9296a0);
|
|
2016
|
+
font-size: 10px;
|
|
2017
|
+
line-height: 14px;
|
|
2018
|
+
text-overflow: ellipsis;
|
|
2019
|
+
white-space: nowrap;
|
|
2020
|
+
}
|
|
2021
|
+
@keyframes dms-menu-in {
|
|
2022
|
+
from { opacity: 0; transform: translateY(5px) scale(.98); }
|
|
2023
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
2024
|
+
}
|
|
2025
|
+
@keyframes dms-menu-in-below {
|
|
2026
|
+
from { opacity: 0; transform: translateY(-5px) scale(.98); }
|
|
2027
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
2028
|
+
}
|
|
2029
|
+
body:not([data-ds-dark-theme]) .dms-effort-slider {
|
|
2030
|
+
filter: none;
|
|
2031
|
+
}
|
|
2032
|
+
body:not([data-ds-dark-theme]) .dms-effort-track {
|
|
2033
|
+
background: var(--dsw-static-blue-75, #e5f0ff);
|
|
2034
|
+
box-shadow:
|
|
2035
|
+
inset 0 1px 0 rgba(255,255,255,.9),
|
|
2036
|
+
inset 0 0 0 1px rgba(80,133,194,.14),
|
|
2037
|
+
0 3px 10px rgba(48,101,165,.13);
|
|
2038
|
+
}
|
|
2039
|
+
body:not([data-ds-dark-theme]) .dms-effort-track::before {
|
|
2040
|
+
content: "";
|
|
2041
|
+
position: absolute;
|
|
2042
|
+
z-index: 0;
|
|
2043
|
+
inset: 0 auto 0 0;
|
|
2044
|
+
width: var(--dms-progress);
|
|
2045
|
+
border-radius: inherit;
|
|
2046
|
+
background: linear-gradient(90deg, #fff 0%, #e2f0ff 20%, #a8d0fb 57%, #438fdf 100%);
|
|
2047
|
+
transition: width 240ms cubic-bezier(.22,1,.36,1);
|
|
2048
|
+
}
|
|
2049
|
+
body:not([data-ds-dark-theme]) .dms-effort.is-dragging .dms-effort-track::before {
|
|
2050
|
+
transition: none;
|
|
2051
|
+
}
|
|
2052
|
+
body:not([data-ds-dark-theme]) .dms-effort-track::after {
|
|
2053
|
+
z-index: 1;
|
|
2054
|
+
background: linear-gradient(90deg, rgba(255,255,255,.48), transparent 34%, rgba(23,101,201,.07));
|
|
2055
|
+
}
|
|
2056
|
+
body:not([data-ds-dark-theme]) .dms-effort-canvas {
|
|
2057
|
+
opacity: .78;
|
|
2058
|
+
mix-blend-mode: multiply;
|
|
2059
|
+
}
|
|
2060
|
+
body:not([data-ds-dark-theme]) .dms-effort-flare {
|
|
2061
|
+
background: radial-gradient(ellipse at 100% 50%, rgba(255,255,255,.98) 0 5%, rgba(204,231,255,.88) 13%, rgba(91,162,241,.48) 31%, rgba(37,111,207,.16) 53%, transparent 75%);
|
|
2062
|
+
filter: blur(2px) saturate(1.12);
|
|
2063
|
+
}
|
|
2064
|
+
body:not([data-ds-dark-theme]) .dms-effort-flare::before {
|
|
2065
|
+
background: linear-gradient(90deg, transparent, rgba(116,177,244,.34), #fff, rgba(66,139,225,.58), transparent);
|
|
2066
|
+
box-shadow: 0 0 7px rgba(58,133,222,.5), 0 0 13px rgba(104,176,255,.38);
|
|
2067
|
+
}
|
|
2068
|
+
body:not([data-ds-dark-theme]) .dms-effort-flare::after {
|
|
2069
|
+
background: linear-gradient(180deg, transparent, rgba(255,255,255,.94), transparent);
|
|
2070
|
+
box-shadow: 0 0 7px rgba(64,137,224,.44);
|
|
2071
|
+
}
|
|
2072
|
+
body:not([data-ds-dark-theme]) .dms-effort-knob {
|
|
2073
|
+
border-color: rgba(126,160,197,.32);
|
|
2074
|
+
box-shadow:
|
|
2075
|
+
0 0 0 2px rgba(58,124,207,.09),
|
|
2076
|
+
0 0 13px rgba(48,118,207,.3),
|
|
2077
|
+
0 3px 8px rgba(39,77,119,.18);
|
|
2078
|
+
}
|
|
2079
|
+
body:not([data-ds-dark-theme]) .dms-effort.is-dragging .dms-effort-knob {
|
|
2080
|
+
box-shadow:
|
|
2081
|
+
0 0 0 3px rgba(36,105,192,.15),
|
|
2082
|
+
0 0 20px rgba(25,100,201,.45),
|
|
2083
|
+
0 3px 8px rgba(39,77,119,.18);
|
|
2084
|
+
}
|
|
2085
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2086
|
+
.dms-chevron,
|
|
2087
|
+
.dms-groupChevron,
|
|
2088
|
+
.dms-effort-slider,
|
|
2089
|
+
.dms-effort-canvas,
|
|
2090
|
+
.dms-effort-knob,
|
|
2091
|
+
.dms-effort-flare,
|
|
2092
|
+
body:not([data-ds-dark-theme]) .dms-effort-track::before { transition: none; }
|
|
2093
|
+
.dms-menu { animation: none; }
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/* Inline effort footer pinned below the scrollable model list (single-pane
|
|
2097
|
+
menu: switching models and adjusting effort both live in one surface). */
|
|
2098
|
+
.dms-effortFooter {
|
|
2099
|
+
flex: 0 0 auto;
|
|
2100
|
+
display: flex;
|
|
2101
|
+
align-items: center;
|
|
2102
|
+
gap: 8px;
|
|
2103
|
+
margin: 4px 4px 0 0;
|
|
2104
|
+
padding: 6px 10px;
|
|
2105
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
2106
|
+
}
|
|
2107
|
+
.dms-effortFooterLabel {
|
|
2108
|
+
flex: 0 0 auto;
|
|
2109
|
+
color: var(--dsw-alias-label-tertiary);
|
|
2110
|
+
font-size: 11px;
|
|
2111
|
+
line-height: 18px;
|
|
2112
|
+
white-space: nowrap;
|
|
2113
|
+
}
|
|
2114
|
+
.dms-effortFooter .dms-effort { flex: 1 1 auto; min-width: 0; }
|
|
2115
|
+
|
|
2116
|
+
`;
|
|
2117
|
+
//#endregion
|
|
2118
|
+
//#region src/client/index.ts
|
|
2119
|
+
/** Dictionary namespace owned by this plugin. */
|
|
2120
|
+
const NS = "modelSelector";
|
|
2121
|
+
/** Required services: the registry, session lookup, locale, the slot seat, and the model directory's Remote faces. */
|
|
2122
|
+
const inject = [
|
|
2123
|
+
"slots",
|
|
2124
|
+
"sessions",
|
|
2125
|
+
"locale",
|
|
2126
|
+
"modelDirectories",
|
|
2127
|
+
"remote",
|
|
2128
|
+
"remote.session"
|
|
2129
|
+
];
|
|
2130
|
+
/**
|
|
2131
|
+
* Client plugin body: register the dictionaries and stylesheet, then take the
|
|
2132
|
+
* model seat over the shared directory once `modelDirectories` appears.
|
|
2133
|
+
* @param ctx - client root context.
|
|
2134
|
+
*/
|
|
2135
|
+
function apply(ctx) {
|
|
2136
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
2137
|
+
zh,
|
|
2138
|
+
en
|
|
2139
|
+
}), "dsh-model-selector: dictionaries");
|
|
2140
|
+
ctx.effect(() => {
|
|
2141
|
+
const tag = document.createElement("style");
|
|
2142
|
+
tag.dataset.plugin = "@bittersmilezzz/dsh-model-selector";
|
|
2143
|
+
tag.textContent = CSS;
|
|
2144
|
+
document.head.appendChild(tag);
|
|
2145
|
+
return () => {
|
|
2146
|
+
tag.remove();
|
|
2147
|
+
};
|
|
2148
|
+
}, "dsh-model-selector: styles");
|
|
2149
|
+
ctx.inject(["modelDirectories"], (scope) => {
|
|
2150
|
+
const models = scope.modelDirectories;
|
|
2151
|
+
const sessions = scope.sessions;
|
|
2152
|
+
scope.slots.inject("conversation.input.model", () => scope.slots.register({
|
|
2153
|
+
name: "conversation.input.model",
|
|
2154
|
+
locale: NS,
|
|
2155
|
+
priority: -1,
|
|
2156
|
+
inject: (sessionId) => {
|
|
2157
|
+
const sid = sessionId;
|
|
2158
|
+
const directory = models.directoryFor(sid);
|
|
2159
|
+
const available = sessions.subagentAddress(sid) === void 0;
|
|
2160
|
+
return {
|
|
2161
|
+
available,
|
|
2162
|
+
directory: directory.store,
|
|
2163
|
+
load: () => {
|
|
2164
|
+
if (available) directory.load().catch((error) => {
|
|
2165
|
+
console.warn("[dsh-model-selector] directory.load failed:", error);
|
|
2166
|
+
});
|
|
2167
|
+
},
|
|
2168
|
+
select: (selection) => available ? directory.select(selection).then(() => true, (error) => {
|
|
2169
|
+
console.warn("[dsh-model-selector] select failed:", error);
|
|
2170
|
+
return false;
|
|
2171
|
+
}) : Promise.resolve(false)
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
}, ModelSelect));
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
//#endregion
|
|
2178
|
+
exports.apply = apply;
|
|
2179
|
+
exports.inject = inject;
|
|
2180
|
+
return module.exports;
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
//# sourceMappingURL=client.js.map
|