@lynn123411/dsh-model-submenu 0.3.10 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DETAILS.md +8 -6
- package/README.md +1 -1
- package/lib/client.js +97 -153
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/EffortSlider.tsx +49 -27
- package/src/client/ModelOptions.module.css +29 -7
- package/src/client/ModelPicker.tsx +4 -86
- package/src/client/ModelRow.tsx +2 -12
- package/src/client/locales.ts +0 -6
- package/src/client/slider.ts +60 -13
package/DETAILS.md
CHANGED
|
@@ -72,13 +72,15 @@ dsh plugin --profile web add @lynn123411/dsh-model-submenu
|
|
|
72
72
|
|
|
73
73
|
| 状态 | 轨道 | 旋钮 |
|
|
74
74
|
| --- | --- | --- |
|
|
75
|
-
| Off |
|
|
76
|
-
| 中间档 |
|
|
77
|
-
| 最高档 |
|
|
75
|
+
| Off | 空——填充只有旋钮那么大,完全藏在旋钮底下 | 实心白 |
|
|
76
|
+
| 中间档 | 该档的纯色,右端圆弧落在旋钮底下 | 实心白 |
|
|
77
|
+
| 最高档 | 整条蓝→紫渐变,铺到轨道尽头 | 实心白 |
|
|
78
78
|
| provider default | 空 | 空心环,停在模型声明的默认档(没声明就停最左) |
|
|
79
|
-
| 拖动中 |
|
|
79
|
+
| 拖动中 | 跟着指针连续移动 | 实心白,松手吸附到最近一档并提交 |
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
轨道高 16px、旋钮直径 20px:旋钮比轨道大一圈,两端因此露出轨道自己的圆角,旋钮不会被误读成轨道的端帽。填充是「轨道形状裁到旋钮」——右端圆弧取轨道的半径、圆心落在旋钮底下,所以色块是钻进旋钮而不是在它旁边直切一刀;最高档的填充宽度正好铺满整条轨道。
|
|
82
|
+
|
|
83
|
+
拖动时旋钮跟的是指针的连续位置(`fractionAt`),不是一档一格:档位取离指针最近的那个刻度(`detentAt`),所以旋钮在两档之间也能停在任意位置,只有跨过中点才换档。松手时旋钮用 160ms 的缓出滑到那个刻度上再提交;拖动期间这条过渡关掉(`data-dragging`),否则旋钮会落后于指针。`prefers-reduced-motion` 下过渡一律关掉。provider default 不是档位:`effortChoices` 只返回模型声明的真实等级,默认态由 `state.current.reasoningEffort === undefined` 判定,点标签提交 `reasoningEffort: undefined` 切回去。颜色由 `level-color.ts` 的 `levelColor(index, count)` 给出:`0` 是中性灰,其余档位把「Off 以上」均匀铺在蓝→紫的斜坡上,最高档固定取紫端并用整条渐变填充。
|
|
82
84
|
|
|
83
85
|
## 开发副本
|
|
84
86
|
|
|
@@ -102,7 +104,7 @@ cd plugins/dsh-model-submenu && pnpm check
|
|
|
102
104
|
`check` = `typecheck` + `build`(tsdown)+ `smoke` + `test` + `assert:lib-untracked`。
|
|
103
105
|
|
|
104
106
|
- `scripts/smoke-client.mjs` 按 Web shell 的方式装载构建产物 `lib/client.js`(`window.__ModuleLoader__.load` 桩件),喂假 cordis 服务,断言导出身份、`inject` 表逐字相等、三处贡献各自的 list `id`(`model-menu-bridge`、`model-picker`、`model-effort-tint`)、前两处共用同一个 store、着色那条读同一个目录、产物里走的是共享的 `Modal`、产物里带着认宿主菜单面板(`data-menu-material`)、两格(`:scope > button[role=`)、档位文字(`span[class*='cellValue']`)与触发器档位文字(`span[class*="triggerEffort"]`)的四段选择器、滑块以 `role="slider"` 暴露给辅助技术、字典键集中英对齐且带着滑块那三条文案、元信息取数确实问过设置镜像与供应商目录,以及「设置没答案时答未知而不猜」。末尾 7 条是 store 自身的契约:起始关闭、open 发布目标、重复 open 不通知、第二次 open 替换目标、close 清空、退订后静默。
|
|
105
|
-
- `scripts/test-favorites.mjs`:收藏表的键名、读写、坏值降级、跨供应商靠前排序。`scripts/test-facts.mjs`:设置值到上下文窗口与图片输入的映射、两类适配器的字段差异、缺字段整段略去。`scripts/test-commit.mjs`:点模型立即提交、已选中的模型不重复提交、记住的档位不在可选档里时不强塞。`scripts/test-rows.mjs`:目录拍平、标签页派生、搜索的两把钥匙。`scripts/test-slider.mjs
|
|
107
|
+
- `scripts/test-favorites.mjs`:收藏表的键名、读写、坏值降级、跨供应商靠前排序。`scripts/test-facts.mjs`:设置值到上下文窗口与图片输入的映射、两类适配器的字段差异、缺字段整段略去。`scripts/test-commit.mjs`:点模型立即提交、已选中的模型不重复提交、记住的档位不在可选档里时不强塞。`scripts/test-rows.mjs`:目录拍平、标签页派生、搜索的两把钥匙。`scripts/test-slider.mjs`:滑块几何与配色——档位到轨道比例、拖动位置是连续比例而不是逐档跳、比例换回最近一档且跨过中点才变、指针到最近档、两端夹取、单档与零宽轨道的退路、填充圆弧始终藏在旋钮底下且末档铺满轨道、Off 的中性灰、蓝到紫的两端、只有最高档用渐变。`scripts/test-render.mjs`:用真 react 把菜单桥、触发器着色与弹窗画进 jsdom,40 条用例锁住「点宿主第一格 → 关菜单 → store → 弹窗」这条链、宿主那两格的下钻确实没执行、点标签切回默认、点滑块以外的位置把焦点交给滑块、刻度数与旋钮位置、数值与箭头隐藏、Off 灰/中间纯色/最高档渐变、触发器档位文字跟着变色且默认态交还宿主颜色、默认态空轨道与空心旋钮、拖动松手才提交一次、按下与松手同任务也不丢提交、末档与进行中不提交、被拒走宿主提示、单档与无档位不挂滑块、卸载后样式写回,以及弹窗侧的收藏置顶、星标独立、提交与拒绝、思考等级记忆、加载与失败、会话不可选时整块不渲染。
|
|
106
108
|
- `lib/` 是构建镜像,`assert:lib-untracked` 判它没被 git 跟踪。
|
|
107
109
|
|
|
108
110
|
要肉眼确认页面上的形态,用仓库的 `node tools/verify-client.mjs dsh-model-submenu --needle '<你新写的类名或文案>'`。
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
7
|
- **第一行就是入口**:菜单里的「模型」点下去直接打开弹窗,不再只列当前供应商;顶部搜索框同时搜模型名与供应商名,每个有模型的供应商一个标签页。
|
|
8
|
-
-
|
|
8
|
+
- **第二行是滑块**:推理等级不再下钻到档位列表,拖动时旋钮跟着指针走、松手吸附到最近一档并切换,键盘左右键逐档调整;轨道上的刻度点数就是该模型可选的档位数,旋钮比轨道大一圈。
|
|
9
9
|
- **档位有颜色**:Off 是灰色,往上从蓝走到紫,只有最高档的填充是渐变;输入框触发器上的档位文字跟着同一套颜色走。
|
|
10
10
|
- **默认档位是一个状态**:点最左边的「推理等级」四个字切回 provider default——轨道空着、旋钮是空心环,不会被误认成最低档;再拖一下滑块就回到明确档位。
|
|
11
11
|
- **收藏靠前**:每行右侧一个星标,点它只切换收藏、不切换模型;收藏表存在浏览器本地,整台浏览器共用一张,收藏的模型排在列表前面。
|
package/lib/client.js
CHANGED
|
@@ -198,6 +198,8 @@ window.__ModuleLoader__.load({
|
|
|
198
198
|
hidden: true
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
|
+
/** The knob's centre keeps its own radius away from either end of the track. */
|
|
202
|
+
const KNOB_RADIUS = 10;
|
|
201
203
|
/**
|
|
202
204
|
* @param index - detent index.
|
|
203
205
|
* @param count - number of detents.
|
|
@@ -208,17 +210,26 @@ window.__ModuleLoader__.load({
|
|
|
208
210
|
return Math.min(Math.max(index, 0), count - 1) / (count - 1);
|
|
209
211
|
}
|
|
210
212
|
/**
|
|
213
|
+
* Where a pointer sits along the knob's travel. The drag reads this rather than a
|
|
214
|
+
* detent, so the knob follows the pointer instead of jumping a level at a time.
|
|
211
215
|
* @param clientX - pointer position in viewport coordinates.
|
|
212
216
|
* @param left - the track's left edge in viewport coordinates.
|
|
213
217
|
* @param width - the track's width in pixels.
|
|
214
|
-
* @
|
|
215
|
-
* @returns the detent nearest the pointer, clamped to the two ends.
|
|
218
|
+
* @returns the position as a fraction of the travel, clamped to 0 and 1.
|
|
216
219
|
*/
|
|
217
|
-
function
|
|
218
|
-
if (count <= 1) return 0;
|
|
220
|
+
function fractionAt(clientX, left, width) {
|
|
219
221
|
const usable = width - 20;
|
|
220
222
|
if (usable <= 0) return 0;
|
|
221
|
-
const fraction = (clientX - left -
|
|
223
|
+
const fraction = (clientX - left - KNOB_RADIUS) / usable;
|
|
224
|
+
return Math.min(Math.max(fraction, 0), 1);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* @param fraction - a knob position between 0 and 1.
|
|
228
|
+
* @param count - number of detents.
|
|
229
|
+
* @returns the detent nearest that position.
|
|
230
|
+
*/
|
|
231
|
+
function detentAt(fraction, count) {
|
|
232
|
+
if (count <= 1) return 0;
|
|
222
233
|
return Math.round(Math.min(Math.max(fraction, 0), 1) * (count - 1));
|
|
223
234
|
}
|
|
224
235
|
/**
|
|
@@ -226,11 +237,24 @@ window.__ModuleLoader__.load({
|
|
|
226
237
|
* @returns the CSS length that puts the knob centre there, inset at both ends.
|
|
227
238
|
*/
|
|
228
239
|
function knobOffset(fraction) {
|
|
229
|
-
return `calc(
|
|
240
|
+
return `calc(${KNOB_RADIUS}px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 20px))`;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Width of the colored fill for a knob position.
|
|
244
|
+
*
|
|
245
|
+
* The fill is the track's own shape cut at the knob: its right cap carries the
|
|
246
|
+
* track's radius and ends under the knob, so the color runs into the knob
|
|
247
|
+
* instead of stopping at a straight edge, and the last detent reaches the track's
|
|
248
|
+
* end rather than the knob's centre.
|
|
249
|
+
* @param fraction - a knob position between 0 and 1.
|
|
250
|
+
* @returns the CSS width of the fill.
|
|
251
|
+
*/
|
|
252
|
+
function fillWidth(fraction) {
|
|
253
|
+
return `calc(16px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 16px))`;
|
|
230
254
|
}
|
|
231
255
|
//#endregion
|
|
232
256
|
//#region \0dsh-css:/home/runner/work/my-dsh/my-dsh/plugins/dsh-model-submenu/src/client/ModelOptions.module.css.mjs
|
|
233
|
-
const css = ".TcCbqa_pickerHost{display:contents}.TcCbqa_sliderCell{flex:auto;align-self:stretch;align-items:center;min-width:96px;display:flex}.TcCbqa_slider{cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none;outline:none;flex:1;align-items:center;min-width:0;height:100%;display:flex}.TcCbqa_slider[data-busy]{cursor:default}.TcCbqa_sliderTrack{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;width:100%;height:
|
|
257
|
+
const css = ".TcCbqa_pickerHost{display:contents}.TcCbqa_sliderCell{flex:auto;align-self:stretch;align-items:center;min-width:96px;display:flex}.TcCbqa_slider{cursor:pointer;touch-action:none;-webkit-user-select:none;user-select:none;outline:none;flex:1;align-items:center;min-width:0;height:100%;display:flex}.TcCbqa_slider[data-busy]{cursor:default}.TcCbqa_sliderTrack{corner-shape:round;background:var(--dsw-alias-border-l3);border-radius:999px;width:100%;height:16px;position:relative}.TcCbqa_sliderFill{corner-shape:round;pointer-events:none;border-radius:999px;transition:width .16s cubic-bezier(.2,0,0,1);position:absolute;top:0;bottom:0;left:0}.TcCbqa_sliderTick{corner-shape:round;background:var(--dsw-alias-label-tertiary);pointer-events:none;border-radius:50%;width:4px;height:4px;margin:-2px 0 0 -2px;position:absolute;top:50%}.TcCbqa_sliderTick[data-on]{background:#00000047}.TcCbqa_sliderKnob{box-sizing:border-box;corner-shape:round;background:var(--dsw-alias-label-primary-foreground);width:20px;height:20px;box-shadow:var(--dsw-elevation-soft);pointer-events:none;border-radius:50%;margin:-10px 0 0 -10px;transition:left .16s cubic-bezier(.2,0,0,1);position:absolute;top:50%}.TcCbqa_slider[data-dragging] .TcCbqa_sliderKnob,.TcCbqa_slider[data-dragging] .TcCbqa_sliderFill{transition:none}@media (prefers-reduced-motion:reduce){.TcCbqa_sliderKnob,.TcCbqa_sliderFill{transition:none}}.TcCbqa_slider[data-default] .TcCbqa_sliderKnob{background:var(--dsw-alias-bg-layer-1);border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none}.TcCbqa_slider[data-busy] .TcCbqa_sliderKnob{opacity:.6}.TcCbqa_slider:focus-visible .TcCbqa_sliderTrack{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:3px}.TcCbqa_rowBadge{flex:none;margin-inline-start:6px}.TcCbqa_row{align-items:center;gap:2px;display:flex}.TcCbqa_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;border-radius:var(--dsw-radius-md);padding-inline:6px;background:0 0;border:none;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.TcCbqa_rowPick:hover:not(:disabled),.TcCbqa_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.TcCbqa_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.TcCbqa_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.TcCbqa_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.TcCbqa_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.TcCbqa_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;border-radius:var(--dsw-radius-sm);opacity:0;background:0 0;border:none;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.TcCbqa_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_row:hover .TcCbqa_starButton,.TcCbqa_row:focus-within .TcCbqa_starButton{opacity:1}.TcCbqa_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.TcCbqa_starButton{opacity:1}}.TcCbqa_star{flex:none}.TcCbqa_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.TcCbqa_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.TcCbqa_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.TcCbqa_pickerContent,.TcCbqa_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.TcCbqa_pickerSearch{padding:0 20px 12px}.TcCbqa_pickerSearchInput{width:100%}.TcCbqa_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);overscroll-behavior-x:contain;scrollbar-width:none;flex-wrap:nowrap;gap:2px;padding:0 16px;display:flex;overflow-x:auto}.TcCbqa_pickerTabs::-webkit-scrollbar{display:none}.TcCbqa_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;border-radius:var(--dsw-radius-sm);white-space:nowrap;background:0 0;border:none;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.TcCbqa_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.TcCbqa_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.TcCbqa_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.TcCbqa_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.TcCbqa_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.TcCbqa_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}";
|
|
234
258
|
const tagId = "@lynn123411/dsh-model-submenu/ModelOptions.module.css";
|
|
235
259
|
if (typeof document !== "undefined") {
|
|
236
260
|
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
@@ -244,7 +268,6 @@ window.__ModuleLoader__.load({
|
|
|
244
268
|
}
|
|
245
269
|
var ModelOptions_module_css_default = {
|
|
246
270
|
"check": "TcCbqa_check",
|
|
247
|
-
"footerDot": "TcCbqa_footerDot",
|
|
248
271
|
"footerHint": "TcCbqa_footerHint",
|
|
249
272
|
"notice": "TcCbqa_notice",
|
|
250
273
|
"pickerContent": "TcCbqa_pickerContent",
|
|
@@ -279,9 +302,10 @@ window.__ModuleLoader__.load({
|
|
|
279
302
|
*
|
|
280
303
|
* The Host's second menu cell is a button that drills into a list of levels.
|
|
281
304
|
* This slider sits inside that same cell, between the Host's label and value, so
|
|
282
|
-
* the levels are reachable without leaving the menu.
|
|
283
|
-
*
|
|
284
|
-
*
|
|
305
|
+
* the levels are reachable without leaving the menu. The levels are discrete but
|
|
306
|
+
* the drag is not: the knob follows the pointer continuously, the level it
|
|
307
|
+
* reports is the detent nearest that position, and letting go eases the knob onto
|
|
308
|
+
* that detent and commits once. Keyboard arrows step and commit immediately.
|
|
285
309
|
*
|
|
286
310
|
* The fill takes the level's color, so the track and the two captions read as
|
|
287
311
|
* one signal: "off" is neutral, the levels above it walk a blue-to-violet ramp,
|
|
@@ -299,22 +323,34 @@ window.__ModuleLoader__.load({
|
|
|
299
323
|
const trackRef = (0, react.useRef)(null);
|
|
300
324
|
const dragRef = (0, react.useRef)(null);
|
|
301
325
|
const [drag, setDrag] = (0, react.useState)(null);
|
|
302
|
-
const
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
326
|
+
const [hold, setHold] = (0, react.useState)(null);
|
|
327
|
+
const dragging = drag !== null;
|
|
328
|
+
const resting = defaultActive ? defaultIndex : current;
|
|
329
|
+
const active = dragging ? detentAt(drag, steps.length) : resting;
|
|
330
|
+
const knobFraction = drag ?? hold ?? offsetOf(resting, steps.length);
|
|
331
|
+
const shown = dragging || hold !== null ? detentAt(knobFraction, steps.length) : resting;
|
|
332
|
+
const beginDrag = (fraction) => {
|
|
333
|
+
dragRef.current = fraction;
|
|
334
|
+
setDrag(fraction);
|
|
306
335
|
};
|
|
307
|
-
|
|
336
|
+
(0, react.useEffect)(() => {
|
|
337
|
+
if (!busy) setHold(null);
|
|
338
|
+
}, [busy]);
|
|
339
|
+
(0, react.useEffect)(() => {
|
|
340
|
+
setHold(null);
|
|
341
|
+
}, [current, defaultActive]);
|
|
342
|
+
const fractionAtPointer = (clientX) => {
|
|
308
343
|
const track = trackRef.current;
|
|
309
|
-
if (track === null) return
|
|
344
|
+
if (track === null) return dragRef.current ?? knobFraction;
|
|
310
345
|
const rect = track.getBoundingClientRect();
|
|
311
|
-
return
|
|
346
|
+
return fractionAt(clientX, rect.left, rect.width);
|
|
312
347
|
};
|
|
313
348
|
const onPointerDown = (event) => {
|
|
314
349
|
if (busy) return;
|
|
315
350
|
event.preventDefault();
|
|
316
351
|
event.stopPropagation();
|
|
317
|
-
|
|
352
|
+
setHold(null);
|
|
353
|
+
beginDrag(fractionAtPointer(event.clientX));
|
|
318
354
|
try {
|
|
319
355
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
320
356
|
} catch {}
|
|
@@ -322,15 +358,17 @@ window.__ModuleLoader__.load({
|
|
|
322
358
|
};
|
|
323
359
|
const onPointerMove = (event) => {
|
|
324
360
|
if (dragRef.current === null || busy) return;
|
|
325
|
-
|
|
326
|
-
if (next !== dragRef.current) beginDrag(next);
|
|
361
|
+
beginDrag(fractionAtPointer(event.clientX));
|
|
327
362
|
};
|
|
328
363
|
const settle = (event, commit) => {
|
|
329
364
|
if (dragRef.current === null) return;
|
|
330
|
-
const next =
|
|
365
|
+
const next = detentAt(fractionAtPointer(event.clientX), steps.length);
|
|
331
366
|
beginDrag(null);
|
|
332
367
|
if (!commit) return;
|
|
333
|
-
if (next !== current || defaultActive)
|
|
368
|
+
if (next !== current || defaultActive) {
|
|
369
|
+
setHold(offsetOf(next, steps.length));
|
|
370
|
+
onCommit(next);
|
|
371
|
+
}
|
|
334
372
|
};
|
|
335
373
|
const onKeyDown = (event) => {
|
|
336
374
|
if (busy) return;
|
|
@@ -341,11 +379,10 @@ window.__ModuleLoader__.load({
|
|
|
341
379
|
event.stopPropagation();
|
|
342
380
|
if (next !== current || defaultActive) onCommit(next);
|
|
343
381
|
};
|
|
344
|
-
const knobFraction = offsetOf(active, steps.length);
|
|
345
382
|
const offset = knobOffset(knobFraction);
|
|
346
|
-
const idle = defaultActive &&
|
|
383
|
+
const idle = defaultActive && !dragging && hold === null;
|
|
347
384
|
const filled = idle ? 0 : knobFraction;
|
|
348
|
-
const fill = isTopLevel(
|
|
385
|
+
const fill = isTopLevel(shown, steps.length) ? LEVEL_GRADIENT : levelColor(shown, steps.length);
|
|
349
386
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
350
387
|
ref: trackRef,
|
|
351
388
|
className: ModelOptions_module_css_default.slider,
|
|
@@ -355,11 +392,11 @@ window.__ModuleLoader__.load({
|
|
|
355
392
|
"aria-valuemin": 0,
|
|
356
393
|
"aria-valuemax": Math.max(0, steps.length - 1),
|
|
357
394
|
"aria-valuenow": active,
|
|
358
|
-
"aria-valuetext": defaultActive &&
|
|
395
|
+
"aria-valuetext": defaultActive && !dragging ? defaultLabel : steps[active]?.label,
|
|
359
396
|
"aria-disabled": busy || void 0,
|
|
360
397
|
"data-busy": busy ? "" : void 0,
|
|
361
|
-
"data-dragging":
|
|
362
|
-
"data-default":
|
|
398
|
+
"data-dragging": dragging ? "" : void 0,
|
|
399
|
+
"data-default": idle ? "" : void 0,
|
|
363
400
|
onPointerDown,
|
|
364
401
|
onPointerMove,
|
|
365
402
|
onPointerUp: (event) => {
|
|
@@ -379,13 +416,13 @@ window.__ModuleLoader__.load({
|
|
|
379
416
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
380
417
|
className: ModelOptions_module_css_default.sliderFill,
|
|
381
418
|
style: {
|
|
382
|
-
width:
|
|
419
|
+
width: fillWidth(filled),
|
|
383
420
|
background: fill
|
|
384
421
|
}
|
|
385
422
|
}),
|
|
386
423
|
steps.map((step, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
387
424
|
className: ModelOptions_module_css_default.sliderTick,
|
|
388
|
-
"data-on": index <=
|
|
425
|
+
"data-on": index <= shown && !idle ? "" : void 0,
|
|
389
426
|
style: { left: knobOffset(offsetOf(index, steps.length)) }
|
|
390
427
|
}, step.key)),
|
|
391
428
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -605,7 +642,7 @@ window.__ModuleLoader__.load({
|
|
|
605
642
|
* @param props - the row's content and behaviour.
|
|
606
643
|
* @returns the row element.
|
|
607
644
|
*/
|
|
608
|
-
function ModelRow({ name, badge, selected, busy, favorited, meta, starLabel,
|
|
645
|
+
function ModelRow({ name, badge, selected, busy, favorited, meta, starLabel, onPick, onToggleFavorite, children }) {
|
|
609
646
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
610
647
|
className: ModelOptions_module_css_default.row,
|
|
611
648
|
"data-selected": selected ? "" : void 0,
|
|
@@ -613,14 +650,11 @@ window.__ModuleLoader__.load({
|
|
|
613
650
|
children: [
|
|
614
651
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
615
652
|
type: "button",
|
|
616
|
-
|
|
617
|
-
"aria-checked": selected,
|
|
618
|
-
tabIndex: rowTabIndex,
|
|
653
|
+
"aria-current": selected ? "true" : void 0,
|
|
619
654
|
className: ModelOptions_module_css_default.rowPick,
|
|
620
655
|
title: name,
|
|
621
656
|
disabled: busy,
|
|
622
657
|
onClick: onPick,
|
|
623
|
-
onFocus: onFocusRow,
|
|
624
658
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
625
659
|
className: ModelOptions_module_css_default.rowCopy,
|
|
626
660
|
children: [
|
|
@@ -641,7 +675,6 @@ window.__ModuleLoader__.load({
|
|
|
641
675
|
className: ModelOptions_module_css_default.starButton,
|
|
642
676
|
"aria-pressed": favorited,
|
|
643
677
|
"aria-label": starLabel,
|
|
644
|
-
tabIndex: starTabStop ? 0 : -1,
|
|
645
678
|
disabled: busy,
|
|
646
679
|
onClick: onToggleFavorite,
|
|
647
680
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
|
|
@@ -940,8 +973,6 @@ window.__ModuleLoader__.load({
|
|
|
940
973
|
const listRef = (0, react.useRef)(null);
|
|
941
974
|
const [query, setQuery] = (0, react.useState)("");
|
|
942
975
|
const [tab, setTab] = (0, react.useState)("*");
|
|
943
|
-
const [highlight, setHighlight] = (0, react.useState)(0);
|
|
944
|
-
const tabRefs = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
945
976
|
const tabs = (0, react.useMemo)(() => pickerTabs(state.groups, t("picker.all")), [state.groups, t]);
|
|
946
977
|
const reason = (0, react.useMemo)(() => reasoningOf(state.groups), [state.groups]);
|
|
947
978
|
const rows = (0, react.useMemo)(() => orderWithFavorites(rowsForTab(flattenGroups(state.groups), tab), (row) => isFavorite(favorites, row.provider, row.model)), [
|
|
@@ -958,7 +989,6 @@ window.__ModuleLoader__.load({
|
|
|
958
989
|
if (!open) return;
|
|
959
990
|
setQuery("");
|
|
960
991
|
setTab("*");
|
|
961
|
-
setHighlight(0);
|
|
962
992
|
if (listRef.current !== null) listRef.current.scrollTop = 0;
|
|
963
993
|
const frame = requestAnimationFrame(() => {
|
|
964
994
|
if (!(0, _deepseek_ai_dsh_client_ui_primitives.isMobileDevice)()) searchRef.current?.focus();
|
|
@@ -967,29 +997,7 @@ window.__ModuleLoader__.load({
|
|
|
967
997
|
cancelAnimationFrame(frame);
|
|
968
998
|
};
|
|
969
999
|
}, [open]);
|
|
970
|
-
(0, react.useEffect)(() => {
|
|
971
|
-
setHighlight((current) => visible.length === 0 ? 0 : Math.min(current, visible.length - 1));
|
|
972
|
-
}, [visible.length]);
|
|
973
|
-
(0, react.useEffect)(() => {
|
|
974
|
-
(listRef.current?.querySelector(`[data-row-index="${highlight}"]`))?.scrollIntoView({ block: "nearest" });
|
|
975
|
-
}, [highlight]);
|
|
976
1000
|
if (!open) return null;
|
|
977
|
-
const activeRow = visible[highlight];
|
|
978
|
-
const activeId = activeRow === void 0 ? void 0 : `${id}-row-${highlight}`;
|
|
979
|
-
const onSearchKeyDown = (event) => {
|
|
980
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
981
|
-
event.preventDefault();
|
|
982
|
-
if (visible.length === 0) return;
|
|
983
|
-
setHighlight((current) => {
|
|
984
|
-
return (current + (event.key === "ArrowDown" ? 1 : -1) + visible.length) % visible.length;
|
|
985
|
-
});
|
|
986
|
-
return;
|
|
987
|
-
}
|
|
988
|
-
if (event.key === "Enter" && activeRow !== void 0 && !busy) {
|
|
989
|
-
event.preventDefault();
|
|
990
|
-
pick(activeRow);
|
|
991
|
-
}
|
|
992
|
-
};
|
|
993
1001
|
const pick = (row) => {
|
|
994
1002
|
const selection = planModelCommit(live, row.provider, row.model, reason, rememberedOf?.(row.provider, row.model));
|
|
995
1003
|
if (selection === null) {
|
|
@@ -998,26 +1006,6 @@ window.__ModuleLoader__.load({
|
|
|
998
1006
|
}
|
|
999
1007
|
onPick(selection);
|
|
1000
1008
|
};
|
|
1001
|
-
/** Move to another tab and hand it the keyboard, in one step. */
|
|
1002
|
-
const goTab = (next) => {
|
|
1003
|
-
if (next === void 0 || next.id === tab) return;
|
|
1004
|
-
setTab(next.id);
|
|
1005
|
-
setHighlight(0);
|
|
1006
|
-
tabRefs.current.get(next.id)?.focus();
|
|
1007
|
-
};
|
|
1008
|
-
const onTabKeyDown = (event) => {
|
|
1009
|
-
const index = tabs.findIndex((candidate) => candidate.id === tab);
|
|
1010
|
-
if (event.key === "ArrowRight" || event.key === "ArrowLeft") {
|
|
1011
|
-
event.preventDefault();
|
|
1012
|
-
const step = event.key === "ArrowRight" ? 1 : -1;
|
|
1013
|
-
goTab(tabs[((index === -1 ? 0 : index) + step + tabs.length) % tabs.length]);
|
|
1014
|
-
return;
|
|
1015
|
-
}
|
|
1016
|
-
if (event.key === "Home" || event.key === "End") {
|
|
1017
|
-
event.preventDefault();
|
|
1018
|
-
goTab(tabs[event.key === "Home" ? 0 : tabs.length - 1]);
|
|
1019
|
-
}
|
|
1020
|
-
};
|
|
1021
1009
|
const failures = state.failures.filter((failure) => tab === "*" || failure.id === tab);
|
|
1022
1010
|
const failed = state.error !== null && state.status === "error";
|
|
1023
1011
|
const loading = state.status === "loading";
|
|
@@ -1038,7 +1026,6 @@ window.__ModuleLoader__.load({
|
|
|
1038
1026
|
role: "searchbox",
|
|
1039
1027
|
"aria-label": t("picker.search"),
|
|
1040
1028
|
"aria-controls": `${id}-list`,
|
|
1041
|
-
"aria-activedescendant": activeId,
|
|
1042
1029
|
placeholder: t("picker.search"),
|
|
1043
1030
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 14 }),
|
|
1044
1031
|
value: query,
|
|
@@ -1046,7 +1033,6 @@ window.__ModuleLoader__.load({
|
|
|
1046
1033
|
onChange: (event) => {
|
|
1047
1034
|
setQuery(event.target.value);
|
|
1048
1035
|
},
|
|
1049
|
-
onKeyDown: onSearchKeyDown,
|
|
1050
1036
|
"data-modal-autofocus": ""
|
|
1051
1037
|
})
|
|
1052
1038
|
}),
|
|
@@ -1055,10 +1041,6 @@ window.__ModuleLoader__.load({
|
|
|
1055
1041
|
role: "tablist",
|
|
1056
1042
|
"aria-label": t("picker.title"),
|
|
1057
1043
|
children: tabs.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1058
|
-
ref: (node) => {
|
|
1059
|
-
if (node === null) tabRefs.current.delete(entry.id);
|
|
1060
|
-
else tabRefs.current.set(entry.id, node);
|
|
1061
|
-
},
|
|
1062
1044
|
type: "button",
|
|
1063
1045
|
role: "tab",
|
|
1064
1046
|
id: `${id}-tab-${entry.id}`,
|
|
@@ -1068,14 +1050,6 @@ window.__ModuleLoader__.load({
|
|
|
1068
1050
|
className: ModelOptions_module_css_default.pickerTab,
|
|
1069
1051
|
onClick: () => {
|
|
1070
1052
|
setTab(entry.id);
|
|
1071
|
-
setHighlight(0);
|
|
1072
|
-
},
|
|
1073
|
-
onKeyDown: onTabKeyDown,
|
|
1074
|
-
onFocus: () => {
|
|
1075
|
-
if (entry.id !== tab) {
|
|
1076
|
-
setTab(entry.id);
|
|
1077
|
-
setHighlight(0);
|
|
1078
|
-
}
|
|
1079
1053
|
},
|
|
1080
1054
|
children: entry.label
|
|
1081
1055
|
}, entry.id))
|
|
@@ -1084,8 +1058,6 @@ window.__ModuleLoader__.load({
|
|
|
1084
1058
|
ref: listRef,
|
|
1085
1059
|
id: `${id}-list`,
|
|
1086
1060
|
className: ModelOptions_module_css_default.pickerList,
|
|
1087
|
-
role: "menu",
|
|
1088
|
-
"aria-label": t("picker.title"),
|
|
1089
1061
|
children: [
|
|
1090
1062
|
loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1091
1063
|
className: ModelOptions_module_css_default.notice,
|
|
@@ -1116,63 +1088,39 @@ window.__ModuleLoader__.load({
|
|
|
1116
1088
|
className: ModelOptions_module_css_default.notice,
|
|
1117
1089
|
children: state.groups.length === 0 ? t("status.empty") : t("picker.searchEmpty")
|
|
1118
1090
|
}),
|
|
1119
|
-
visible.map((row
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
},
|
|
1146
|
-
onFocusRow: () => {
|
|
1147
|
-
setHighlight(index);
|
|
1148
|
-
},
|
|
1149
|
-
onToggleFavorite: () => {
|
|
1150
|
-
onToggleFavorite(row.provider, row.model);
|
|
1151
|
-
}
|
|
1152
|
-
})
|
|
1153
|
-
}, `${row.provider}/${row.model}`))
|
|
1091
|
+
visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
|
|
1092
|
+
name: row.entry.name,
|
|
1093
|
+
badge: row.entry.badge === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
1094
|
+
tone: row.entry.badge.tone,
|
|
1095
|
+
className: ModelOptions_module_css_default.rowBadge,
|
|
1096
|
+
children: resolveText(row.entry.badge.text)
|
|
1097
|
+
}),
|
|
1098
|
+
selected: live?.provider === row.provider && live.model === row.model,
|
|
1099
|
+
busy,
|
|
1100
|
+
favorited: isFavorite(favorites, row.provider, row.model),
|
|
1101
|
+
meta: metaLine({
|
|
1102
|
+
row,
|
|
1103
|
+
facts: facts(row.provider, row.model),
|
|
1104
|
+
providerName: tab === "*" ? row.providerName : void 0,
|
|
1105
|
+
thinks: row.entry.reasoning !== void 0,
|
|
1106
|
+
effortLabel: t("meta.effort"),
|
|
1107
|
+
imageLabel: t("meta.images")
|
|
1108
|
+
}),
|
|
1109
|
+
starLabel: isFavorite(favorites, row.provider, row.model) ? t("picker.unfavorite", { model: row.entry.name }) : t("picker.favorite", { model: row.entry.name }),
|
|
1110
|
+
onPick: () => {
|
|
1111
|
+
pick(row);
|
|
1112
|
+
},
|
|
1113
|
+
onToggleFavorite: () => {
|
|
1114
|
+
onToggleFavorite(row.provider, row.model);
|
|
1115
|
+
}
|
|
1116
|
+
}) }, `${row.provider}/${row.model}`))
|
|
1154
1117
|
]
|
|
1155
1118
|
}),
|
|
1156
1119
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1157
1120
|
className: ModelOptions_module_css_default.pickerFooter,
|
|
1158
1121
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1159
1122
|
className: ModelOptions_module_css_default.footerHint,
|
|
1160
|
-
children: [
|
|
1161
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["↑", "↓"] }),
|
|
1162
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintNavigate") }),
|
|
1163
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1164
|
-
className: ModelOptions_module_css_default.footerDot,
|
|
1165
|
-
children: "·"
|
|
1166
|
-
}),
|
|
1167
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Enter"] }),
|
|
1168
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintSelect") }),
|
|
1169
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1170
|
-
className: ModelOptions_module_css_default.footerDot,
|
|
1171
|
-
children: "·"
|
|
1172
|
-
}),
|
|
1173
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Esc"] }),
|
|
1174
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintClose") })
|
|
1175
|
-
]
|
|
1123
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Esc"] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintClose") })]
|
|
1176
1124
|
})
|
|
1177
1125
|
})
|
|
1178
1126
|
]
|
|
@@ -1437,8 +1385,6 @@ window.__ModuleLoader__.load({
|
|
|
1437
1385
|
"picker.all": "全部",
|
|
1438
1386
|
"picker.favorite": "收藏 {model}",
|
|
1439
1387
|
"picker.unfavorite": "取消收藏 {model}",
|
|
1440
|
-
"picker.hintNavigate": "导航",
|
|
1441
|
-
"picker.hintSelect": "选择",
|
|
1442
1388
|
"picker.hintClose": "关闭",
|
|
1443
1389
|
"meta.effort": "思考",
|
|
1444
1390
|
"meta.images": "图片输入",
|
|
@@ -1462,8 +1408,6 @@ window.__ModuleLoader__.load({
|
|
|
1462
1408
|
"picker.all": "All",
|
|
1463
1409
|
"picker.favorite": "Favorite {model}",
|
|
1464
1410
|
"picker.unfavorite": "Remove {model} from favorites",
|
|
1465
|
-
"picker.hintNavigate": "navigate",
|
|
1466
|
-
"picker.hintSelect": "select",
|
|
1467
1411
|
"picker.hintClose": "close",
|
|
1468
1412
|
"meta.effort": "Effort",
|
|
1469
1413
|
"meta.images": "Images",
|