@lynn123411/dsh-model-submenu 0.3.5 → 0.3.10
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 +60 -4
- package/README.md +12 -11
- package/lib/client.js +754 -631
- package/lib/client.js.map +1 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +18 -17
- package/src/client/EffortSlider.tsx +159 -0
- package/src/client/EffortTint.tsx +61 -0
- package/src/client/ModelMenuBridge.tsx +203 -0
- package/src/client/ModelOptions.module.css +372 -0
- package/src/client/ModelPicker.tsx +10 -4
- package/src/client/ModelPickerHost.tsx +146 -0
- package/src/client/ModelRow.tsx +55 -96
- package/src/client/index.ts +65 -28
- package/src/client/level-color.ts +51 -0
- package/src/client/locales.ts +12 -33
- package/src/client/picker-state.ts +55 -0
- package/src/client/selection.ts +43 -17
- package/src/client/slider.ts +51 -0
- package/src/client/ModelSubmenu.module.css +0 -509
- package/src/client/ModelSubmenu.tsx +0 -429
package/lib/client.js
CHANGED
|
@@ -5,9 +5,570 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
9
|
let react_dom = require("react-dom");
|
|
9
10
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
-
|
|
11
|
+
//#region src/client/level-color.ts
|
|
12
|
+
/**
|
|
13
|
+
* The accent one reasoning level carries.
|
|
14
|
+
*
|
|
15
|
+
* "Off" carries no accent at all — the fill and the caption stay neutral, so
|
|
16
|
+
* switching reasoning off reads as switching it off rather than as the first
|
|
17
|
+
* shade of a ramp. Every level above it walks a blue-to-violet ramp, and the
|
|
18
|
+
* highest level is the only one whose fill is a gradient.
|
|
19
|
+
*/
|
|
20
|
+
/** Off: the dimmed label tone, the same neutral the Host uses for quiet text. */
|
|
21
|
+
const NEUTRAL = "var(--dsw-alias-label-dimmed)";
|
|
22
|
+
/** Lowest lit level: the blue the composer already uses for accents. */
|
|
23
|
+
const LOW = [
|
|
24
|
+
77,
|
|
25
|
+
147,
|
|
26
|
+
248
|
|
27
|
+
];
|
|
28
|
+
/** Highest level: violet. */
|
|
29
|
+
const HIGH = [
|
|
30
|
+
147,
|
|
31
|
+
51,
|
|
32
|
+
234
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* @param t - a point on the ramp, 0 at the lowest lit level and 1 at the top.
|
|
36
|
+
* @returns the ramp color there.
|
|
37
|
+
*/
|
|
38
|
+
function ramp(t) {
|
|
39
|
+
const mix = (from, to) => Math.round(from + (to - from) * t);
|
|
40
|
+
return `rgb(${mix(LOW[0], HIGH[0])}, ${mix(LOW[1], HIGH[1])}, ${mix(LOW[2], HIGH[2])})`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* @param index - the level's detent, 0 being "off".
|
|
44
|
+
* @param count - number of detents.
|
|
45
|
+
* @returns the level's color as a CSS color string.
|
|
46
|
+
*/
|
|
47
|
+
function levelColor(index, count) {
|
|
48
|
+
if (index <= 0) return NEUTRAL;
|
|
49
|
+
if (isTopLevel(index, count)) return ramp(1);
|
|
50
|
+
const span = Math.max(1, count - 2);
|
|
51
|
+
return ramp(Math.min(Math.max((index - 1) / span, 0), 1));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @param index - the level's detent.
|
|
55
|
+
* @param count - number of detents.
|
|
56
|
+
* @returns whether this level is the only one painted with a gradient.
|
|
57
|
+
*/
|
|
58
|
+
function isTopLevel(index, count) {
|
|
59
|
+
return count > 1 && index === count - 1;
|
|
60
|
+
}
|
|
61
|
+
/** The top level's fill: the whole ramp, ending on the color its text takes. */
|
|
62
|
+
const LEVEL_GRADIENT = `linear-gradient(90deg, ${ramp(0)}, ${ramp(1)})`;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/client/selection.ts
|
|
65
|
+
/**
|
|
66
|
+
* Build the slider's detents for one model.
|
|
67
|
+
*
|
|
68
|
+
* The provider default is deliberately absent: it is a state, not a level, and
|
|
69
|
+
* giving it a detent would read as the weakest reasoning instead of "let the
|
|
70
|
+
* provider decide". The label above the track switches to it.
|
|
71
|
+
* @param reasoning - the catalog's reasoning metadata, undefined when the model has none.
|
|
72
|
+
* @returns one detent per advertised level, in catalog order.
|
|
73
|
+
*/
|
|
74
|
+
function effortChoices(reasoning) {
|
|
75
|
+
if (reasoning === void 0) return [];
|
|
76
|
+
return reasoning.efforts.map((level) => ({
|
|
77
|
+
key: level.id,
|
|
78
|
+
effort: level.id,
|
|
79
|
+
label: level.name
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The selection a level click submits.
|
|
84
|
+
* @param live - the selection in force right now.
|
|
85
|
+
* @param effort - the clicked level, undefined meaning the provider default.
|
|
86
|
+
* @returns the selection to submit, or null when the level already is the live one.
|
|
87
|
+
*/
|
|
88
|
+
function planEffortCommit(live, effort) {
|
|
89
|
+
if (sameEffort(live.reasoningEffort, effort)) return null;
|
|
90
|
+
return {
|
|
91
|
+
provider: live.provider,
|
|
92
|
+
model: live.model,
|
|
93
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The selection a model click submits.
|
|
98
|
+
* @param live - the selection in force right now, null before a first selection resolves.
|
|
99
|
+
* @param provider - the clicked model's provider.
|
|
100
|
+
* @param model - the clicked model id.
|
|
101
|
+
* @param reasoningOf - the catalog's reasoning metadata for one provider and model.
|
|
102
|
+
* @param remembered - the level this browser last used for the target model, if any.
|
|
103
|
+
* @returns the selection to submit, or null when the click changes nothing.
|
|
104
|
+
*/
|
|
105
|
+
function planModelCommit(live, provider, model, reasoningOf, remembered) {
|
|
106
|
+
if (live !== null && sameModel(live, provider, model)) return null;
|
|
107
|
+
const offered = reasoningOf(provider, model);
|
|
108
|
+
return {
|
|
109
|
+
provider,
|
|
110
|
+
model,
|
|
111
|
+
...offered !== void 0 && remembered !== void 0 && offered.efforts.some((level) => level.id === remembered) ? { reasoningEffort: remembered } : {}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The reasoning metadata the catalog advertises for the model in force.
|
|
116
|
+
* @param state - the session's directory snapshot.
|
|
117
|
+
* @returns the metadata, or undefined when no model is live or it advertises none.
|
|
118
|
+
*/
|
|
119
|
+
function liveReasoning(state) {
|
|
120
|
+
const current = state.current;
|
|
121
|
+
if (current === null) return void 0;
|
|
122
|
+
return state.groups.find((group) => group.id === current.provider)?.models.find((model) => model.id === current.model)?.reasoning;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The selection in force, or null while the Host has not resolved one.
|
|
126
|
+
* @param state - the session's directory snapshot.
|
|
127
|
+
* @returns the live selection, with the effort only when one is set.
|
|
128
|
+
*/
|
|
129
|
+
function selectionOf(state) {
|
|
130
|
+
if (state.current === null) return null;
|
|
131
|
+
return state.current.reasoningEffort === void 0 ? {
|
|
132
|
+
provider: state.current.provider,
|
|
133
|
+
model: state.current.model
|
|
134
|
+
} : {
|
|
135
|
+
provider: state.current.provider,
|
|
136
|
+
model: state.current.model,
|
|
137
|
+
reasoningEffort: state.current.reasoningEffort
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* @param live - one selection.
|
|
142
|
+
* @param provider - the provider to compare.
|
|
143
|
+
* @param model - the model id to compare.
|
|
144
|
+
* @returns whether both name the same model.
|
|
145
|
+
*/
|
|
146
|
+
function sameModel(live, provider, model) {
|
|
147
|
+
return live.provider === provider && live.model === model;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* @param liveEffort - the level in force, undefined meaning the provider default.
|
|
151
|
+
* @param effort - the level in question.
|
|
152
|
+
* @returns whether they name the same level.
|
|
153
|
+
*/
|
|
154
|
+
function sameEffort(liveEffort, effort) {
|
|
155
|
+
return liveEffort === effort;
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/client/EffortTint.tsx
|
|
159
|
+
/**
|
|
160
|
+
* Tint the composer's effort caption with the level's color.
|
|
161
|
+
*
|
|
162
|
+
* The slider's fill and the menu's value already carry the level's color; the
|
|
163
|
+
* caption on the collapsed trigger is the third place the level shows, and it
|
|
164
|
+
* lives outside the menu. This entry stays mounted for the whole session and
|
|
165
|
+
* writes that one color, so all three read as one signal.
|
|
166
|
+
*
|
|
167
|
+
* The caption is recognized as the trigger's `span[class*='triggerEffort']`
|
|
168
|
+
* child — the same class-substring contract the menu row uses. When the model
|
|
169
|
+
* declares no levels the span does not exist and nothing is written.
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* Render nothing and keep the trigger caption colored.
|
|
173
|
+
* @param props - the session's catalog face.
|
|
174
|
+
* @returns an inert marker.
|
|
175
|
+
*/
|
|
176
|
+
function EffortTint({ available, directory }) {
|
|
177
|
+
const markerRef = (0, react.useRef)(null);
|
|
178
|
+
const [caption, setCaption] = (0, react.useState)(null);
|
|
179
|
+
const state = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot());
|
|
180
|
+
const levels = effortChoices(liveReasoning(state));
|
|
181
|
+
const effort = state.current?.reasoningEffort;
|
|
182
|
+
const chosen = effort === void 0 ? -1 : levels.findIndex((level) => level.effort === effort);
|
|
183
|
+
const color = available && chosen >= 0 && levels.length >= 2 ? levelColor(chosen, levels.length) : null;
|
|
184
|
+
(0, react.useLayoutEffect)(() => {
|
|
185
|
+
const next = (markerRef.current?.closest("[data-composer-card]"))?.querySelector("[data-slot=\"conversation.input.model\"] button[aria-haspopup=\"menu\"] span[class*=\"triggerEffort\"]") ?? null;
|
|
186
|
+
if (next !== caption) setCaption(next);
|
|
187
|
+
});
|
|
188
|
+
(0, react.useLayoutEffect)(() => {
|
|
189
|
+
if (caption === null || color === null) return;
|
|
190
|
+
const saved = caption.style.color;
|
|
191
|
+
caption.style.color = color;
|
|
192
|
+
return () => {
|
|
193
|
+
caption.style.color = saved;
|
|
194
|
+
};
|
|
195
|
+
}, [caption, color]);
|
|
196
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
197
|
+
ref: markerRef,
|
|
198
|
+
hidden: true
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* @param index - detent index.
|
|
203
|
+
* @param count - number of detents.
|
|
204
|
+
* @returns the knob centre as a fraction of the track, 0 at the first detent and 1 at the last.
|
|
205
|
+
*/
|
|
206
|
+
function offsetOf(index, count) {
|
|
207
|
+
if (count <= 1) return 0;
|
|
208
|
+
return Math.min(Math.max(index, 0), count - 1) / (count - 1);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* @param clientX - pointer position in viewport coordinates.
|
|
212
|
+
* @param left - the track's left edge in viewport coordinates.
|
|
213
|
+
* @param width - the track's width in pixels.
|
|
214
|
+
* @param count - number of detents.
|
|
215
|
+
* @returns the detent nearest the pointer, clamped to the two ends.
|
|
216
|
+
*/
|
|
217
|
+
function stepAt(clientX, left, width, count) {
|
|
218
|
+
if (count <= 1) return 0;
|
|
219
|
+
const usable = width - 20;
|
|
220
|
+
if (usable <= 0) return 0;
|
|
221
|
+
const fraction = (clientX - left - 10) / usable;
|
|
222
|
+
return Math.round(Math.min(Math.max(fraction, 0), 1) * (count - 1));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* @param fraction - a knob position between 0 and 1.
|
|
226
|
+
* @returns the CSS length that puts the knob centre there, inset at both ends.
|
|
227
|
+
*/
|
|
228
|
+
function knobOffset(fraction) {
|
|
229
|
+
return `calc(10px + ${Math.min(Math.max(fraction, 0), 1)} * (100% - 20px))`;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
232
|
+
//#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:20px;position:relative}.TcCbqa_sliderFill{corner-shape:round;pointer-events:none;border-radius:999px;position:absolute;top:0;bottom:0;left:0;overflow:hidden}.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{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;position:absolute;top:50%}.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}.TcCbqa_footerDot{color:var(--dsw-alias-label-dimmed)}";
|
|
234
|
+
const tagId = "@lynn123411/dsh-model-submenu/ModelOptions.module.css";
|
|
235
|
+
if (typeof document !== "undefined") {
|
|
236
|
+
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
237
|
+
if (tag === null) {
|
|
238
|
+
tag = document.createElement("style");
|
|
239
|
+
tag.dataset.plugin = "@lynn123411/dsh-model-submenu";
|
|
240
|
+
tag.dataset.pluginCss = tagId;
|
|
241
|
+
document.head.appendChild(tag);
|
|
242
|
+
}
|
|
243
|
+
tag.textContent = css;
|
|
244
|
+
}
|
|
245
|
+
var ModelOptions_module_css_default = {
|
|
246
|
+
"check": "TcCbqa_check",
|
|
247
|
+
"footerDot": "TcCbqa_footerDot",
|
|
248
|
+
"footerHint": "TcCbqa_footerHint",
|
|
249
|
+
"notice": "TcCbqa_notice",
|
|
250
|
+
"pickerContent": "TcCbqa_pickerContent",
|
|
251
|
+
"pickerDialog": "TcCbqa_pickerDialog",
|
|
252
|
+
"pickerFooter": "TcCbqa_pickerFooter",
|
|
253
|
+
"pickerHost": "TcCbqa_pickerHost",
|
|
254
|
+
"pickerList": "TcCbqa_pickerList",
|
|
255
|
+
"pickerSearch": "TcCbqa_pickerSearch",
|
|
256
|
+
"pickerSearchInput": "TcCbqa_pickerSearchInput",
|
|
257
|
+
"pickerTab": "TcCbqa_pickerTab",
|
|
258
|
+
"pickerTabs": "TcCbqa_pickerTabs",
|
|
259
|
+
"retry": "TcCbqa_retry",
|
|
260
|
+
"row": "TcCbqa_row",
|
|
261
|
+
"rowBadge": "TcCbqa_rowBadge",
|
|
262
|
+
"rowCopy": "TcCbqa_rowCopy",
|
|
263
|
+
"rowMeta": "TcCbqa_rowMeta",
|
|
264
|
+
"rowName": "TcCbqa_rowName",
|
|
265
|
+
"rowPick": "TcCbqa_rowPick",
|
|
266
|
+
"slider": "TcCbqa_slider",
|
|
267
|
+
"sliderCell": "TcCbqa_sliderCell",
|
|
268
|
+
"sliderFill": "TcCbqa_sliderFill",
|
|
269
|
+
"sliderKnob": "TcCbqa_sliderKnob",
|
|
270
|
+
"sliderTick": "TcCbqa_sliderTick",
|
|
271
|
+
"sliderTrack": "TcCbqa_sliderTrack",
|
|
272
|
+
"star": "TcCbqa_star",
|
|
273
|
+
"starButton": "TcCbqa_starButton"
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/client/EffortSlider.tsx
|
|
277
|
+
/**
|
|
278
|
+
* The reasoning-level slider that replaces the Host's effort drill.
|
|
279
|
+
*
|
|
280
|
+
* The Host's second menu cell is a button that drills into a list of levels.
|
|
281
|
+
* This slider sits inside that same cell, between the Host's label and value, so
|
|
282
|
+
* the levels are reachable without leaving the menu. Levels are discrete: the
|
|
283
|
+
* knob snaps to one detent per level, the drag only moves the knob, and the
|
|
284
|
+
* release commits once. Keyboard arrows step and commit immediately.
|
|
285
|
+
*
|
|
286
|
+
* The fill takes the level's color, so the track and the two captions read as
|
|
287
|
+
* one signal: "off" is neutral, the levels above it walk a blue-to-violet ramp,
|
|
288
|
+
* and only the top level fills with the whole gradient.
|
|
289
|
+
*
|
|
290
|
+
* "Provider default" is not a detent: it is a state, drawn as an empty track
|
|
291
|
+
* with a hollow knob, so it never reads as the lowest level.
|
|
292
|
+
*/
|
|
293
|
+
/**
|
|
294
|
+
* Render the slider.
|
|
295
|
+
* @param props - levels, the level in force, and the commit path.
|
|
296
|
+
* @returns one focusable slider over the discrete levels.
|
|
297
|
+
*/
|
|
298
|
+
function EffortSlider({ steps, current, defaultActive, defaultIndex, defaultLabel, busy, label, onCommit }) {
|
|
299
|
+
const trackRef = (0, react.useRef)(null);
|
|
300
|
+
const dragRef = (0, react.useRef)(null);
|
|
301
|
+
const [drag, setDrag] = (0, react.useState)(null);
|
|
302
|
+
const active = drag ?? (defaultActive ? defaultIndex : current);
|
|
303
|
+
const beginDrag = (index) => {
|
|
304
|
+
dragRef.current = index;
|
|
305
|
+
setDrag(index);
|
|
306
|
+
};
|
|
307
|
+
const indexAt = (clientX) => {
|
|
308
|
+
const track = trackRef.current;
|
|
309
|
+
if (track === null) return active;
|
|
310
|
+
const rect = track.getBoundingClientRect();
|
|
311
|
+
return stepAt(clientX, rect.left, rect.width, steps.length);
|
|
312
|
+
};
|
|
313
|
+
const onPointerDown = (event) => {
|
|
314
|
+
if (busy) return;
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
event.stopPropagation();
|
|
317
|
+
beginDrag(indexAt(event.clientX));
|
|
318
|
+
try {
|
|
319
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
320
|
+
} catch {}
|
|
321
|
+
event.currentTarget.focus();
|
|
322
|
+
};
|
|
323
|
+
const onPointerMove = (event) => {
|
|
324
|
+
if (dragRef.current === null || busy) return;
|
|
325
|
+
const next = indexAt(event.clientX);
|
|
326
|
+
if (next !== dragRef.current) beginDrag(next);
|
|
327
|
+
};
|
|
328
|
+
const settle = (event, commit) => {
|
|
329
|
+
if (dragRef.current === null) return;
|
|
330
|
+
const next = indexAt(event.clientX);
|
|
331
|
+
beginDrag(null);
|
|
332
|
+
if (!commit) return;
|
|
333
|
+
if (next !== current || defaultActive) onCommit(next);
|
|
334
|
+
};
|
|
335
|
+
const onKeyDown = (event) => {
|
|
336
|
+
if (busy) return;
|
|
337
|
+
const last = steps.length - 1;
|
|
338
|
+
const next = event.key === "ArrowRight" || event.key === "ArrowUp" ? Math.min(active + 1, last) : event.key === "ArrowLeft" || event.key === "ArrowDown" ? Math.max(active - 1, 0) : event.key === "Home" ? 0 : event.key === "End" ? last : null;
|
|
339
|
+
if (next === null) return;
|
|
340
|
+
event.preventDefault();
|
|
341
|
+
event.stopPropagation();
|
|
342
|
+
if (next !== current || defaultActive) onCommit(next);
|
|
343
|
+
};
|
|
344
|
+
const knobFraction = offsetOf(active, steps.length);
|
|
345
|
+
const offset = knobOffset(knobFraction);
|
|
346
|
+
const idle = defaultActive && drag === null;
|
|
347
|
+
const filled = idle ? 0 : knobFraction;
|
|
348
|
+
const fill = isTopLevel(active, steps.length) ? LEVEL_GRADIENT : levelColor(active, steps.length);
|
|
349
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
350
|
+
ref: trackRef,
|
|
351
|
+
className: ModelOptions_module_css_default.slider,
|
|
352
|
+
role: "slider",
|
|
353
|
+
tabIndex: 0,
|
|
354
|
+
"aria-label": label,
|
|
355
|
+
"aria-valuemin": 0,
|
|
356
|
+
"aria-valuemax": Math.max(0, steps.length - 1),
|
|
357
|
+
"aria-valuenow": active,
|
|
358
|
+
"aria-valuetext": defaultActive && drag === null ? defaultLabel : steps[active]?.label,
|
|
359
|
+
"aria-disabled": busy || void 0,
|
|
360
|
+
"data-busy": busy ? "" : void 0,
|
|
361
|
+
"data-dragging": drag === null ? void 0 : "",
|
|
362
|
+
"data-default": defaultActive && drag === null ? "" : void 0,
|
|
363
|
+
onPointerDown,
|
|
364
|
+
onPointerMove,
|
|
365
|
+
onPointerUp: (event) => {
|
|
366
|
+
settle(event, true);
|
|
367
|
+
},
|
|
368
|
+
onPointerCancel: (event) => {
|
|
369
|
+
settle(event, false);
|
|
370
|
+
},
|
|
371
|
+
onKeyDown,
|
|
372
|
+
onClick: (event) => {
|
|
373
|
+
event.stopPropagation();
|
|
374
|
+
},
|
|
375
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
376
|
+
className: ModelOptions_module_css_default.sliderTrack,
|
|
377
|
+
"aria-hidden": "true",
|
|
378
|
+
children: [
|
|
379
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
380
|
+
className: ModelOptions_module_css_default.sliderFill,
|
|
381
|
+
style: {
|
|
382
|
+
width: knobOffset(filled),
|
|
383
|
+
background: fill
|
|
384
|
+
}
|
|
385
|
+
}),
|
|
386
|
+
steps.map((step, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
387
|
+
className: ModelOptions_module_css_default.sliderTick,
|
|
388
|
+
"data-on": index <= active && !idle ? "" : void 0,
|
|
389
|
+
style: { left: knobOffset(offsetOf(index, steps.length)) }
|
|
390
|
+
}, step.key)),
|
|
391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
392
|
+
className: ModelOptions_module_css_default.sliderKnob,
|
|
393
|
+
style: { left: offset }
|
|
394
|
+
})
|
|
395
|
+
]
|
|
396
|
+
})
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/client/ModelMenuBridge.tsx
|
|
401
|
+
/**
|
|
402
|
+
* Bridge the Host's own model menu: the model cell opens the all-provider
|
|
403
|
+
* picker, and the effort cell carries the reasoning-level slider.
|
|
404
|
+
*
|
|
405
|
+
* The Host draws the root menu's first two cells itself: the model cell drills
|
|
406
|
+
* into the current provider's list, the effort cell into the levels. This plugin
|
|
407
|
+
* wants the first to open its dialog and the second to stop drilling, and the
|
|
408
|
+
* Host exposes no hook for either. This entry is mounted exactly while the root
|
|
409
|
+
* pane is open, so it can take the click on the menu surface before the cells'
|
|
410
|
+
* own handlers run and mount the slider into the effort cell.
|
|
411
|
+
*
|
|
412
|
+
* The cells are recognized as the menu surface's direct `role="menuitem"`
|
|
413
|
+
* children, which is the Host's root-menu shape; the options slot output sits in
|
|
414
|
+
* its own wrapper, so other plugins' rows never join that list. When the second
|
|
415
|
+
* cell is missing the model declares no reasoning levels and the row is left
|
|
416
|
+
* alone; when the first is missing the click falls through to the Host.
|
|
417
|
+
*
|
|
418
|
+
* Clicking the cell's label switches to the provider default, which is a state
|
|
419
|
+
* rather than a detent: the slider shows it as an empty track under a hollow
|
|
420
|
+
* knob, so it never reads as the weakest level.
|
|
421
|
+
*/
|
|
422
|
+
/**
|
|
423
|
+
* Render the marker, redirect the model cell, and mount the effort slider.
|
|
424
|
+
* @param props - menu state, session, catalog face, and localized copy.
|
|
425
|
+
* @returns an inert marker plus the slider while the effort cell is present.
|
|
426
|
+
*/
|
|
427
|
+
function ModelMenuBridge({ sessionId, picker, dismiss, reportFailure, available, directory, select, t }) {
|
|
428
|
+
const markerRef = (0, react.useRef)(null);
|
|
429
|
+
const sliderRef = (0, react.useRef)(null);
|
|
430
|
+
const [surface, setSurface] = (0, react.useState)(null);
|
|
431
|
+
const [effortRow, setEffortRow] = (0, react.useState)(null);
|
|
432
|
+
const [rowLabel, setRowLabel] = (0, react.useState)(null);
|
|
433
|
+
const state = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot());
|
|
434
|
+
const current = state.current;
|
|
435
|
+
const reasoning = liveReasoning(state);
|
|
436
|
+
const levels = (0, react.useMemo)(() => effortChoices(reasoning), [reasoning]);
|
|
437
|
+
const declared = levels.findIndex((level) => level.effort === reasoning?.defaultEffort);
|
|
438
|
+
const chosen = current?.reasoningEffort === void 0 ? -1 : levels.findIndex((level) => level.effort === current.reasoningEffort);
|
|
439
|
+
const defaultActive = current !== null && current.reasoningEffort === void 0;
|
|
440
|
+
const currentIndex = chosen >= 0 ? chosen : declared >= 0 ? declared : 0;
|
|
441
|
+
const defaultIndex = declared >= 0 ? declared : 0;
|
|
442
|
+
const canDefault = !defaultActive;
|
|
443
|
+
const busy = state.pending !== null;
|
|
444
|
+
const showSlider = available && levels.length >= 2 && effortRow !== null;
|
|
445
|
+
(0, react.useLayoutEffect)(() => {
|
|
446
|
+
const foundSurface = markerRef.current?.closest("[data-menu-material]");
|
|
447
|
+
const nextSurface = foundSurface instanceof HTMLElement ? foundSurface : null;
|
|
448
|
+
if (nextSurface !== surface) setSurface(nextSurface);
|
|
449
|
+
const cells = nextSurface === null ? [] : [...nextSurface.querySelectorAll(":scope > button[role=\"menuitem\"]")];
|
|
450
|
+
const nextRow = cells.length >= 2 ? cells[1] ?? null : null;
|
|
451
|
+
if (nextRow !== effortRow) setEffortRow(nextRow);
|
|
452
|
+
const nextLabel = nextRow?.querySelector("span[class*='cellLabel']")?.textContent?.trim() ?? "";
|
|
453
|
+
if (nextLabel !== (rowLabel ?? "")) setRowLabel(nextLabel);
|
|
454
|
+
});
|
|
455
|
+
const submit = (selection) => {
|
|
456
|
+
if (selection === null) return;
|
|
457
|
+
select(selection).then((result) => {
|
|
458
|
+
if (result === void 0 || result.ok) return;
|
|
459
|
+
reportFailure(result.error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${result.error.code}: ${result.error.message}` }));
|
|
460
|
+
});
|
|
461
|
+
};
|
|
462
|
+
const commit = (index) => {
|
|
463
|
+
const live = selectionOf(state);
|
|
464
|
+
const level = levels[index];
|
|
465
|
+
if (live === null || level === void 0) return;
|
|
466
|
+
submit(planEffortCommit(live, level.effort));
|
|
467
|
+
};
|
|
468
|
+
const commitDefault = () => {
|
|
469
|
+
const live = selectionOf(state);
|
|
470
|
+
if (live === null) return;
|
|
471
|
+
submit(planEffortCommit(live, void 0));
|
|
472
|
+
};
|
|
473
|
+
const latest = (0, react.useRef)({
|
|
474
|
+
sessionId,
|
|
475
|
+
picker,
|
|
476
|
+
dismiss,
|
|
477
|
+
showSlider,
|
|
478
|
+
canDefault,
|
|
479
|
+
commitDefault
|
|
480
|
+
});
|
|
481
|
+
latest.current = {
|
|
482
|
+
sessionId,
|
|
483
|
+
picker,
|
|
484
|
+
dismiss,
|
|
485
|
+
showSlider,
|
|
486
|
+
canDefault,
|
|
487
|
+
commitDefault
|
|
488
|
+
};
|
|
489
|
+
(0, react.useEffect)(() => {
|
|
490
|
+
if (surface === null) return;
|
|
491
|
+
const redirect = (event) => {
|
|
492
|
+
const target = event.target;
|
|
493
|
+
if (!(target instanceof Node)) return;
|
|
494
|
+
const cells = [...surface.querySelectorAll(":scope > button[role=\"menuitem\"]")];
|
|
495
|
+
const modelCell = cells[0];
|
|
496
|
+
if (modelCell !== void 0 && modelCell.contains(target)) {
|
|
497
|
+
event.preventDefault();
|
|
498
|
+
event.stopPropagation();
|
|
499
|
+
const request = latest.current;
|
|
500
|
+
request.dismiss();
|
|
501
|
+
request.picker.open(request.sessionId);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const row = cells[1];
|
|
505
|
+
if (row === void 0 || !row.contains(target) || !latest.current.showSlider) return;
|
|
506
|
+
if (sliderRef.current?.contains(target) === true) return;
|
|
507
|
+
event.preventDefault();
|
|
508
|
+
event.stopPropagation();
|
|
509
|
+
const label = row.querySelector("span[class*='cellLabel']");
|
|
510
|
+
if (latest.current.canDefault && label !== null && label.contains(target)) {
|
|
511
|
+
latest.current.commitDefault();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
sliderRef.current?.querySelector("[role=\"slider\"]")?.focus();
|
|
515
|
+
};
|
|
516
|
+
surface.addEventListener("click", redirect, true);
|
|
517
|
+
return () => {
|
|
518
|
+
surface.removeEventListener("click", redirect, true);
|
|
519
|
+
};
|
|
520
|
+
}, [surface]);
|
|
521
|
+
(0, react.useLayoutEffect)(() => {
|
|
522
|
+
if (effortRow === null || !showSlider) return;
|
|
523
|
+
const label = effortRow.querySelector("span[class*='cellLabel']");
|
|
524
|
+
const value = effortRow.querySelector("span[class*='cellValue']");
|
|
525
|
+
const chevron = effortRow.querySelector("svg[class*='cellChevron']");
|
|
526
|
+
const saved = {
|
|
527
|
+
value: value?.style.display ?? "",
|
|
528
|
+
chevron: chevron?.style.display ?? "",
|
|
529
|
+
cursor: label?.style.cursor ?? "",
|
|
530
|
+
title: label?.getAttribute("title") ?? null
|
|
531
|
+
};
|
|
532
|
+
if (value !== null) value.style.display = "none";
|
|
533
|
+
if (chevron !== null) chevron.style.display = "none";
|
|
534
|
+
if (label !== null && canDefault) {
|
|
535
|
+
label.style.cursor = "pointer";
|
|
536
|
+
label.title = t("effort.useDefault");
|
|
537
|
+
}
|
|
538
|
+
return () => {
|
|
539
|
+
if (value !== null) value.style.display = saved.value;
|
|
540
|
+
if (chevron !== null) chevron.style.display = saved.chevron;
|
|
541
|
+
if (label !== null) {
|
|
542
|
+
label.style.cursor = saved.cursor;
|
|
543
|
+
if (saved.title === null) label.removeAttribute("title");
|
|
544
|
+
else label.title = saved.title;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}, [
|
|
548
|
+
effortRow,
|
|
549
|
+
showSlider,
|
|
550
|
+
canDefault,
|
|
551
|
+
t
|
|
552
|
+
]);
|
|
553
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
554
|
+
ref: markerRef,
|
|
555
|
+
hidden: true
|
|
556
|
+
}), showSlider && effortRow !== null && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
557
|
+
ref: sliderRef,
|
|
558
|
+
className: ModelOptions_module_css_default.sliderCell,
|
|
559
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EffortSlider, {
|
|
560
|
+
steps: levels,
|
|
561
|
+
current: currentIndex,
|
|
562
|
+
defaultActive,
|
|
563
|
+
defaultIndex,
|
|
564
|
+
defaultLabel: t("effort.providerDefault"),
|
|
565
|
+
busy,
|
|
566
|
+
label: rowLabel !== null && rowLabel !== "" ? rowLabel : t("effort.label"),
|
|
567
|
+
onCommit: commit
|
|
568
|
+
})
|
|
569
|
+
}), effortRow)] });
|
|
570
|
+
}
|
|
571
|
+
//#endregion
|
|
11
572
|
//#region src/client/icons.tsx
|
|
12
573
|
/**
|
|
13
574
|
* The one glyph this plugin draws itself.
|
|
@@ -38,117 +599,15 @@ window.__ModuleLoader__.load({
|
|
|
38
599
|
});
|
|
39
600
|
}
|
|
40
601
|
//#endregion
|
|
41
|
-
//#region \0dsh-css:/Users/tny/Desktop/work/my-dsh/plugins/dsh-model-submenu/src/client/ModelSubmenu.module.css.mjs
|
|
42
|
-
const css = ".FOsK3a_root{min-width:0;display:flex;position:relative}.FOsK3a_trigger{min-width:0;max-width:min(360px,45cqw);height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:24px;outline:none;align-items:center;gap:4px;padding:0 4px 0 8px;font-size:13px;font-weight:500;line-height:20px;display:flex}.FOsK3a_trigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_trigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.FOsK3a_trigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_triggerLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;display:var(--dsh-composer-model-text-display,block);overflow:hidden}.FOsK3a_triggerEffort{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-state-business-primary);display:var(--dsh-composer-model-text-display,block);flex-shrink:1000;overflow:hidden}.FOsK3a_triggerIcon{display:var(--dsh-composer-model-icon-display,none);flex:none}.FOsK3a_triggerBadge{flex:none;margin-inline-end:4px}.FOsK3a_rowBadge{flex:none;margin-inline-start:6px}.FOsK3a_chevron{color:var(--dsw-alias-label-caption);flex:none;transition:transform .12s}.FOsK3a_chevronOpen{transform:rotate(180deg)}.FOsK3a_menu{z-index:1100;--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);box-sizing:border-box;width:min(252px,100vw - 24px);max-height:var(--dsh-anchored-max-height,min(70vh, calc(100vh - 96px)));box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;flex-direction:column;padding:4px;display:flex;position:fixed;overflow:hidden}.FOsK3a_providerName{text-overflow:ellipsis;white-space:nowrap;min-height:24px;color:var(--dsw-alias-label-tertiary);align-items:center;padding:3px 8px;font-size:12px;font-weight:500;line-height:18px;display:flex;overflow:hidden}.FOsK3a_groups{min-height:0;overflow-y:auto}.FOsK3a_row{align-items:center;gap:2px;display:flex}.FOsK3a_rowPick{box-sizing:border-box;width:100%;min-width:0;min-height:34px;color:inherit;text-align:left;cursor:pointer;padding-inline:6px;background:0 0;border:none;border-radius:10px;outline:none;flex:1;align-items:center;gap:4px;padding:5px 7px;display:flex}.FOsK3a_rowPick:hover:not(:disabled),.FOsK3a_rowPick:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_rowPick:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_rowCopy{flex-direction:column;flex:1;min-width:0;display:flex}.FOsK3a_rowName{color:inherit;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;font-weight:400;line-height:17px;overflow:hidden}.FOsK3a_menu .FOsK3a_rowName{font-weight:600}.FOsK3a_rowMeta{color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}.FOsK3a_check{color:var(--dsw-alias-label-primary);flex:0 0 15px;place-items:center;display:grid}.FOsK3a_starStatic{color:var(--dsw-alias-state-warn-primary);flex:none;place-items:center;width:14px;display:grid}.FOsK3a_starButton{color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:0;background:0 0;border:none;border-radius:8px;outline:none;flex:none;place-items:center;width:26px;height:26px;display:grid}.FOsK3a_starButton:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_row:hover .FOsK3a_starButton,.FOsK3a_row:focus-within .FOsK3a_starButton{opacity:1}.FOsK3a_starButton[aria-pressed=true]{color:var(--dsw-alias-state-warn-primary);opacity:1}@media (hover:none){.FOsK3a_starButton{opacity:1}}.FOsK3a_star{flex:none}.FOsK3a_divider{background:var(--dsw-alias-border-l2);flex:none;height:.5px;margin:3px 2px}.FOsK3a_effortRow{align-items:center;gap:6px;padding:3px 6px;display:flex}.FOsK3a_effortHeading{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px}.FOsK3a_effortTrack{background:var(--dsw-alias-interactive-bg-hover);scrollbar-width:none;border-radius:999px;align-items:center;gap:1px;min-width:0;margin-left:auto;padding:2px;display:flex;overflow-x:auto}.FOsK3a_effortTrack::-webkit-scrollbar{display:none}.FOsK3a_effortSegment{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:999px;outline:none;flex:none;padding:2px 7px;font-size:11px;line-height:16px}.FOsK3a_effortSegment:hover:not(:disabled){color:var(--dsw-alias-label-primary)}.FOsK3a_effortSegment[aria-checked=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-soft);font-weight:600}.FOsK3a_effortSegment:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.FOsK3a_effortSegment:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:-2px}.FOsK3a_hint{color:var(--dsw-alias-label-tertiary);margin:0;padding:6px 8px;font-size:11px;line-height:16px}.FOsK3a_moreRow{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:10px;outline:none;align-items:center;gap:6px;min-height:32px;padding:5px 8px;font-size:12px;line-height:18px;display:flex}.FOsK3a_moreRow:hover,.FOsK3a_moreRow:focus-visible{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_moreIcon{color:var(--dsw-alias-label-tertiary);flex:none}.FOsK3a_notice{color:var(--dsw-alias-label-tertiary);justify-content:space-between;align-items:flex-start;gap:8px;padding:6px 10px;font-size:12px;line-height:18px;display:flex}.FOsK3a_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600;text-decoration:underline}.FOsK3a_pickerDialog{gap:0;width:min(680px,100%);height:min(640px,80vh);padding:0}.FOsK3a_pickerContent,.FOsK3a_pickerContent>:last-child{flex-direction:column;flex:1;min-height:0;display:flex}.FOsK3a_pickerSearch{padding:0 20px 12px}.FOsK3a_pickerSearchInput{width:100%}.FOsK3a_pickerTabs{border-bottom:.5px solid var(--dsw-alias-border-l2);overscroll-behavior-x:contain;scrollbar-width:none;flex-wrap:nowrap;gap:2px;padding:0 16px;display:flex;overflow-x:auto}.FOsK3a_pickerTabs::-webkit-scrollbar{display:none}.FOsK3a_pickerTab{color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;outline:none;flex:none;padding:6px 10px;font-size:13px;line-height:18px}.FOsK3a_pickerTab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.FOsK3a_pickerTab[aria-selected=true]{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.FOsK3a_pickerTab:focus-visible{outline:2px solid var(--dsw-alias-border-l3)}.FOsK3a_pickerList{flex:1;min-height:0;padding:8px 12px;overflow-y:auto}.FOsK3a_pickerFooter{border-top:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);align-items:center;gap:6px;padding:10px 20px;font-size:12px;line-height:18px;display:flex}.FOsK3a_footerHint{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.FOsK3a_footerDot{color:var(--dsw-alias-label-dimmed)}";
|
|
43
|
-
const tagId = "@lynn123411/dsh-model-submenu/ModelSubmenu.module.css";
|
|
44
|
-
if (typeof document !== "undefined") {
|
|
45
|
-
let tag = document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]");
|
|
46
|
-
if (tag === null) {
|
|
47
|
-
tag = document.createElement("style");
|
|
48
|
-
tag.dataset.plugin = "@lynn123411/dsh-model-submenu";
|
|
49
|
-
tag.dataset.pluginCss = tagId;
|
|
50
|
-
document.head.appendChild(tag);
|
|
51
|
-
}
|
|
52
|
-
tag.textContent = css;
|
|
53
|
-
}
|
|
54
|
-
var ModelSubmenu_module_css_default = {
|
|
55
|
-
"check": "FOsK3a_check",
|
|
56
|
-
"chevron": "FOsK3a_chevron",
|
|
57
|
-
"chevronOpen": "FOsK3a_chevronOpen",
|
|
58
|
-
"divider": "FOsK3a_divider",
|
|
59
|
-
"effortHeading": "FOsK3a_effortHeading",
|
|
60
|
-
"effortRow": "FOsK3a_effortRow",
|
|
61
|
-
"effortSegment": "FOsK3a_effortSegment",
|
|
62
|
-
"effortTrack": "FOsK3a_effortTrack",
|
|
63
|
-
"footerDot": "FOsK3a_footerDot",
|
|
64
|
-
"footerHint": "FOsK3a_footerHint",
|
|
65
|
-
"groups": "FOsK3a_groups",
|
|
66
|
-
"hint": "FOsK3a_hint",
|
|
67
|
-
"menu": "FOsK3a_menu",
|
|
68
|
-
"moreIcon": "FOsK3a_moreIcon",
|
|
69
|
-
"moreRow": "FOsK3a_moreRow",
|
|
70
|
-
"notice": "FOsK3a_notice",
|
|
71
|
-
"pickerContent": "FOsK3a_pickerContent",
|
|
72
|
-
"pickerDialog": "FOsK3a_pickerDialog",
|
|
73
|
-
"pickerFooter": "FOsK3a_pickerFooter",
|
|
74
|
-
"pickerList": "FOsK3a_pickerList",
|
|
75
|
-
"pickerSearch": "FOsK3a_pickerSearch",
|
|
76
|
-
"pickerSearchInput": "FOsK3a_pickerSearchInput",
|
|
77
|
-
"pickerTab": "FOsK3a_pickerTab",
|
|
78
|
-
"pickerTabs": "FOsK3a_pickerTabs",
|
|
79
|
-
"providerName": "FOsK3a_providerName",
|
|
80
|
-
"retry": "FOsK3a_retry",
|
|
81
|
-
"root": "FOsK3a_root",
|
|
82
|
-
"row": "FOsK3a_row",
|
|
83
|
-
"rowBadge": "FOsK3a_rowBadge",
|
|
84
|
-
"rowCopy": "FOsK3a_rowCopy",
|
|
85
|
-
"rowMeta": "FOsK3a_rowMeta",
|
|
86
|
-
"rowName": "FOsK3a_rowName",
|
|
87
|
-
"rowPick": "FOsK3a_rowPick",
|
|
88
|
-
"star": "FOsK3a_star",
|
|
89
|
-
"starButton": "FOsK3a_starButton",
|
|
90
|
-
"starStatic": "FOsK3a_starStatic",
|
|
91
|
-
"trigger": "FOsK3a_trigger",
|
|
92
|
-
"triggerBadge": "FOsK3a_triggerBadge",
|
|
93
|
-
"triggerEffort": "FOsK3a_triggerEffort",
|
|
94
|
-
"triggerIcon": "FOsK3a_triggerIcon",
|
|
95
|
-
"triggerLabel": "FOsK3a_triggerLabel"
|
|
96
|
-
};
|
|
97
|
-
//#endregion
|
|
98
602
|
//#region src/client/ModelRow.tsx
|
|
99
603
|
/**
|
|
100
604
|
* Render one model row.
|
|
101
605
|
* @param props - the row's content and behaviour.
|
|
102
606
|
* @returns the row element.
|
|
103
607
|
*/
|
|
104
|
-
function ModelRow({ name, badge, selected, busy,
|
|
105
|
-
const copy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
106
|
-
className: ModelSubmenu_module_css_default.rowCopy,
|
|
107
|
-
children: [
|
|
108
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
109
|
-
className: ModelSubmenu_module_css_default.rowName,
|
|
110
|
-
children: name
|
|
111
|
-
}),
|
|
112
|
-
badge,
|
|
113
|
-
meta !== void 0 && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
114
|
-
className: ModelSubmenu_module_css_default.rowMeta,
|
|
115
|
-
children: meta
|
|
116
|
-
})
|
|
117
|
-
]
|
|
118
|
-
}) });
|
|
119
|
-
const mark = favorited ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
|
|
120
|
-
filled: true,
|
|
121
|
-
className: ModelSubmenu_module_css_default.star
|
|
122
|
-
}) : null;
|
|
123
|
-
if (star === "static") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
124
|
-
type: "button",
|
|
125
|
-
role: "menuitemradio",
|
|
126
|
-
"aria-checked": selected,
|
|
127
|
-
tabIndex: rowTabIndex,
|
|
128
|
-
className: ModelSubmenu_module_css_default.rowPick,
|
|
129
|
-
title: name,
|
|
130
|
-
disabled: busy,
|
|
131
|
-
onClick: onPick,
|
|
132
|
-
onFocus: onFocusRow,
|
|
133
|
-
"data-selected": selected ? "" : void 0,
|
|
134
|
-
"data-favorited": favorited ? "" : void 0,
|
|
135
|
-
children: [
|
|
136
|
-
copy,
|
|
137
|
-
mark !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
138
|
-
className: ModelSubmenu_module_css_default.starStatic,
|
|
139
|
-
"aria-hidden": "true",
|
|
140
|
-
children: mark
|
|
141
|
-
}),
|
|
142
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
143
|
-
className: ModelSubmenu_module_css_default.check,
|
|
144
|
-
"aria-hidden": !selected,
|
|
145
|
-
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
146
|
-
}),
|
|
147
|
-
children
|
|
148
|
-
]
|
|
149
|
-
});
|
|
608
|
+
function ModelRow({ name, badge, selected, busy, favorited, meta, starLabel, starTabStop = false, rowTabIndex, onPick, onFocusRow, onToggleFavorite, children }) {
|
|
150
609
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
151
|
-
className:
|
|
610
|
+
className: ModelOptions_module_css_default.row,
|
|
152
611
|
"data-selected": selected ? "" : void 0,
|
|
153
612
|
"data-favorited": favorited ? "" : void 0,
|
|
154
613
|
children: [
|
|
@@ -157,16 +616,29 @@ window.__ModuleLoader__.load({
|
|
|
157
616
|
role: "menuitemradio",
|
|
158
617
|
"aria-checked": selected,
|
|
159
618
|
tabIndex: rowTabIndex,
|
|
160
|
-
className:
|
|
619
|
+
className: ModelOptions_module_css_default.rowPick,
|
|
161
620
|
title: name,
|
|
162
621
|
disabled: busy,
|
|
163
622
|
onClick: onPick,
|
|
164
623
|
onFocus: onFocusRow,
|
|
165
|
-
children:
|
|
624
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
625
|
+
className: ModelOptions_module_css_default.rowCopy,
|
|
626
|
+
children: [
|
|
627
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
628
|
+
className: ModelOptions_module_css_default.rowName,
|
|
629
|
+
children: name
|
|
630
|
+
}),
|
|
631
|
+
badge,
|
|
632
|
+
meta !== void 0 && meta !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
633
|
+
className: ModelOptions_module_css_default.rowMeta,
|
|
634
|
+
children: meta
|
|
635
|
+
})
|
|
636
|
+
]
|
|
637
|
+
})
|
|
166
638
|
}),
|
|
167
639
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
168
640
|
type: "button",
|
|
169
|
-
className:
|
|
641
|
+
className: ModelOptions_module_css_default.starButton,
|
|
170
642
|
"aria-pressed": favorited,
|
|
171
643
|
"aria-label": starLabel,
|
|
172
644
|
tabIndex: starTabStop ? 0 : -1,
|
|
@@ -174,11 +646,11 @@ window.__ModuleLoader__.load({
|
|
|
174
646
|
onClick: onToggleFavorite,
|
|
175
647
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StarIcon, {
|
|
176
648
|
filled: favorited,
|
|
177
|
-
className:
|
|
649
|
+
className: ModelOptions_module_css_default.star
|
|
178
650
|
})
|
|
179
651
|
}),
|
|
180
652
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
181
|
-
className:
|
|
653
|
+
className: ModelOptions_module_css_default.check,
|
|
182
654
|
"aria-hidden": !selected,
|
|
183
655
|
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, {}) : null
|
|
184
656
|
}),
|
|
@@ -447,75 +919,6 @@ window.__ModuleLoader__.load({
|
|
|
447
919
|
return rank(searchCandidates(rows), trimmed).map((match) => match.row);
|
|
448
920
|
}
|
|
449
921
|
//#endregion
|
|
450
|
-
//#region src/client/selection.ts
|
|
451
|
-
/**
|
|
452
|
-
* Build the segments for one model's reasoning row.
|
|
453
|
-
* @param reasoning - the catalog's reasoning metadata, undefined when the model has none.
|
|
454
|
-
* @param providerDefaultLabel - the localized label for the "follow the provider" segment.
|
|
455
|
-
* @returns the segments in catalog order; the provider-default one leads when the model declares no default.
|
|
456
|
-
*/
|
|
457
|
-
function effortChoices(reasoning, providerDefaultLabel) {
|
|
458
|
-
if (reasoning === void 0) return [];
|
|
459
|
-
return [...reasoning.defaultEffort === void 0 ? [{
|
|
460
|
-
key: "provider-default",
|
|
461
|
-
effort: void 0,
|
|
462
|
-
label: providerDefaultLabel
|
|
463
|
-
}] : [], ...reasoning.efforts.map((level) => ({
|
|
464
|
-
key: level.id,
|
|
465
|
-
effort: level.id,
|
|
466
|
-
label: level.name
|
|
467
|
-
}))];
|
|
468
|
-
}
|
|
469
|
-
/**
|
|
470
|
-
* The selection a level click submits.
|
|
471
|
-
* @param live - the selection in force right now.
|
|
472
|
-
* @param effort - the clicked level, undefined meaning the provider default.
|
|
473
|
-
* @returns the selection to submit, or null when the level already is the live one.
|
|
474
|
-
*/
|
|
475
|
-
function planEffortCommit(live, effort) {
|
|
476
|
-
if (sameEffort(live.reasoningEffort, effort)) return null;
|
|
477
|
-
return {
|
|
478
|
-
provider: live.provider,
|
|
479
|
-
model: live.model,
|
|
480
|
-
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* The selection a model click submits.
|
|
485
|
-
* @param live - the selection in force right now, null before a first selection resolves.
|
|
486
|
-
* @param provider - the clicked model's provider.
|
|
487
|
-
* @param model - the clicked model id.
|
|
488
|
-
* @param reasoningOf - the catalog's reasoning metadata for one provider and model.
|
|
489
|
-
* @param remembered - the level this browser last used for the target model, if any.
|
|
490
|
-
* @returns the selection to submit, or null when the click changes nothing.
|
|
491
|
-
*/
|
|
492
|
-
function planModelCommit(live, provider, model, reasoningOf, remembered) {
|
|
493
|
-
if (live !== null && sameModel(live, provider, model)) return null;
|
|
494
|
-
const offered = reasoningOf(provider, model);
|
|
495
|
-
return {
|
|
496
|
-
provider,
|
|
497
|
-
model,
|
|
498
|
-
...offered !== void 0 && remembered !== void 0 && offered.efforts.some((level) => level.id === remembered) ? { reasoningEffort: remembered } : {}
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* @param live - one selection.
|
|
503
|
-
* @param provider - the provider to compare.
|
|
504
|
-
* @param model - the model id to compare.
|
|
505
|
-
* @returns whether both name the same model.
|
|
506
|
-
*/
|
|
507
|
-
function sameModel(live, provider, model) {
|
|
508
|
-
return live.provider === provider && live.model === model;
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* @param liveEffort - the level in force, undefined meaning the provider default.
|
|
512
|
-
* @param effort - the level in question.
|
|
513
|
-
* @returns whether they name the same level.
|
|
514
|
-
*/
|
|
515
|
-
function sameEffort(liveEffort, effort) {
|
|
516
|
-
return liveEffort === effort;
|
|
517
|
-
}
|
|
518
|
-
//#endregion
|
|
519
922
|
//#region src/client/ModelPicker.tsx
|
|
520
923
|
/**
|
|
521
924
|
* The model picker: the centered dialog opened from the menu's last row.
|
|
@@ -531,7 +934,7 @@ window.__ModuleLoader__.load({
|
|
|
531
934
|
* @param props - the catalog, the live selection, and the picker's verbs.
|
|
532
935
|
* @returns the dialog; nothing while closed.
|
|
533
936
|
*/
|
|
534
|
-
function ModelPicker({ open, resolveText, state, live, facts, favorites, busy, onPick, onToggleFavorite, onReload, onClose, t }) {
|
|
937
|
+
function ModelPicker({ open, resolveText, state, live, facts, favorites, busy, rememberedOf, onPick, onToggleFavorite, onReload, onClose, t }) {
|
|
535
938
|
const id = (0, react.useId)();
|
|
536
939
|
const searchRef = (0, react.useRef)(null);
|
|
537
940
|
const listRef = (0, react.useRef)(null);
|
|
@@ -588,7 +991,7 @@ window.__ModuleLoader__.load({
|
|
|
588
991
|
}
|
|
589
992
|
};
|
|
590
993
|
const pick = (row) => {
|
|
591
|
-
const selection = planModelCommit(live, row.provider, row.model, reason);
|
|
994
|
+
const selection = planModelCommit(live, row.provider, row.model, reason, rememberedOf?.(row.provider, row.model));
|
|
592
995
|
if (selection === null) {
|
|
593
996
|
onClose();
|
|
594
997
|
return;
|
|
@@ -623,14 +1026,14 @@ window.__ModuleLoader__.load({
|
|
|
623
1026
|
onClose,
|
|
624
1027
|
title: t("picker.title"),
|
|
625
1028
|
closeLabel: t("picker.close"),
|
|
626
|
-
className:
|
|
627
|
-
contentClassName:
|
|
1029
|
+
className: ModelOptions_module_css_default.pickerDialog ?? "",
|
|
1030
|
+
contentClassName: ModelOptions_module_css_default.pickerContent ?? "",
|
|
628
1031
|
children: [
|
|
629
1032
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
630
|
-
className:
|
|
1033
|
+
className: ModelOptions_module_css_default.pickerSearch,
|
|
631
1034
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
632
1035
|
ref: searchRef,
|
|
633
|
-
className:
|
|
1036
|
+
className: ModelOptions_module_css_default.pickerSearchInput ?? "",
|
|
634
1037
|
type: "text",
|
|
635
1038
|
role: "searchbox",
|
|
636
1039
|
"aria-label": t("picker.search"),
|
|
@@ -648,7 +1051,7 @@ window.__ModuleLoader__.load({
|
|
|
648
1051
|
})
|
|
649
1052
|
}),
|
|
650
1053
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
651
|
-
className:
|
|
1054
|
+
className: ModelOptions_module_css_default.pickerTabs,
|
|
652
1055
|
role: "tablist",
|
|
653
1056
|
"aria-label": t("picker.title"),
|
|
654
1057
|
children: tabs.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -662,7 +1065,7 @@ window.__ModuleLoader__.load({
|
|
|
662
1065
|
"aria-selected": entry.id === tab,
|
|
663
1066
|
"aria-controls": `${id}-list`,
|
|
664
1067
|
tabIndex: 0,
|
|
665
|
-
className:
|
|
1068
|
+
className: ModelOptions_module_css_default.pickerTab,
|
|
666
1069
|
onClick: () => {
|
|
667
1070
|
setTab(entry.id);
|
|
668
1071
|
setHighlight(0);
|
|
@@ -680,37 +1083,37 @@ window.__ModuleLoader__.load({
|
|
|
680
1083
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
681
1084
|
ref: listRef,
|
|
682
1085
|
id: `${id}-list`,
|
|
683
|
-
className:
|
|
1086
|
+
className: ModelOptions_module_css_default.pickerList,
|
|
684
1087
|
role: "menu",
|
|
685
1088
|
"aria-label": t("picker.title"),
|
|
686
1089
|
children: [
|
|
687
1090
|
loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
688
|
-
className:
|
|
1091
|
+
className: ModelOptions_module_css_default.notice,
|
|
689
1092
|
children: t("status.loading")
|
|
690
1093
|
}),
|
|
691
1094
|
failed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
692
|
-
className:
|
|
1095
|
+
className: ModelOptions_module_css_default.notice,
|
|
693
1096
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.failed", { message: state.error ?? "" }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
694
1097
|
type: "button",
|
|
695
|
-
className:
|
|
1098
|
+
className: ModelOptions_module_css_default.retry,
|
|
696
1099
|
onClick: onReload,
|
|
697
1100
|
children: t("action.reload")
|
|
698
1101
|
})]
|
|
699
1102
|
}),
|
|
700
1103
|
failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
701
|
-
className:
|
|
1104
|
+
className: ModelOptions_module_css_default.notice,
|
|
702
1105
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.providerFailed", {
|
|
703
1106
|
name: failure.name,
|
|
704
1107
|
message: failure.message
|
|
705
1108
|
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
706
1109
|
type: "button",
|
|
707
|
-
className:
|
|
1110
|
+
className: ModelOptions_module_css_default.retry,
|
|
708
1111
|
onClick: onReload,
|
|
709
1112
|
children: t("action.reload")
|
|
710
1113
|
})]
|
|
711
1114
|
}, failure.id)),
|
|
712
1115
|
visible.length === 0 && !loading && !failed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
713
|
-
className:
|
|
1116
|
+
className: ModelOptions_module_css_default.notice,
|
|
714
1117
|
children: state.groups.length === 0 ? t("status.empty") : t("picker.searchEmpty")
|
|
715
1118
|
}),
|
|
716
1119
|
visible.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -720,12 +1123,11 @@ window.__ModuleLoader__.load({
|
|
|
720
1123
|
name: row.entry.name,
|
|
721
1124
|
badge: row.entry.badge === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
722
1125
|
tone: row.entry.badge.tone,
|
|
723
|
-
className:
|
|
1126
|
+
className: ModelOptions_module_css_default.rowBadge,
|
|
724
1127
|
children: resolveText(row.entry.badge.text)
|
|
725
1128
|
}),
|
|
726
1129
|
selected: live?.provider === row.provider && live.model === row.model,
|
|
727
1130
|
busy,
|
|
728
|
-
star: "interactive",
|
|
729
1131
|
favorited: isFavorite(favorites, row.provider, row.model),
|
|
730
1132
|
meta: metaLine({
|
|
731
1133
|
row,
|
|
@@ -752,20 +1154,20 @@ window.__ModuleLoader__.load({
|
|
|
752
1154
|
]
|
|
753
1155
|
}),
|
|
754
1156
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
755
|
-
className:
|
|
1157
|
+
className: ModelOptions_module_css_default.pickerFooter,
|
|
756
1158
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
757
|
-
className:
|
|
1159
|
+
className: ModelOptions_module_css_default.footerHint,
|
|
758
1160
|
children: [
|
|
759
1161
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["↑", "↓"] }),
|
|
760
1162
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintNavigate") }),
|
|
761
1163
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
762
|
-
className:
|
|
1164
|
+
className: ModelOptions_module_css_default.footerDot,
|
|
763
1165
|
children: "·"
|
|
764
1166
|
}),
|
|
765
1167
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Enter"] }),
|
|
766
1168
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("picker.hintSelect") }),
|
|
767
1169
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
768
|
-
className:
|
|
1170
|
+
className: ModelOptions_module_css_default.footerDot,
|
|
769
1171
|
children: "·"
|
|
770
1172
|
}),
|
|
771
1173
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.ShortcutKeys, { keys: ["Esc"] }),
|
|
@@ -843,65 +1245,46 @@ window.__ModuleLoader__.load({
|
|
|
843
1245
|
}
|
|
844
1246
|
}
|
|
845
1247
|
//#endregion
|
|
846
|
-
//#region src/client/
|
|
847
|
-
/** Unplaced portal card: hidden but laid out at a fixed origin so offsetWidth/offsetHeight are real. */
|
|
848
|
-
const MEASURE_STYLE = {
|
|
849
|
-
visibility: "hidden",
|
|
850
|
-
left: 0,
|
|
851
|
-
top: 0
|
|
852
|
-
};
|
|
853
|
-
const cls = (...parts) => parts.filter((part) => part !== false && part !== void 0).join(" ");
|
|
1248
|
+
//#region src/client/ModelPickerHost.tsx
|
|
854
1249
|
/**
|
|
855
|
-
* The composer's
|
|
856
|
-
* built-in occupant with a provider-scoped menu: one provider's models above,
|
|
857
|
-
* the reasoning row as segments, the cache warning, and the door to the picker
|
|
858
|
-
* that lists every provider.
|
|
1250
|
+
* The composer overlay's picker view: the dialog, its catalog, and its verbs.
|
|
859
1251
|
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
1252
|
+
* It lives in `conversation.input.overlay` rather than beside the row, because
|
|
1253
|
+
* the Host unmounts the model menu as soon as a row is clicked. Everything the
|
|
1254
|
+
* dialog reads arrives through this entry's injected face.
|
|
1255
|
+
*/
|
|
1256
|
+
/**
|
|
1257
|
+
* Render the picker for this session.
|
|
1258
|
+
* @param props - session, store, catalog face, and localized copy.
|
|
1259
|
+
* @returns the dialog while the store names this session; otherwise nothing.
|
|
863
1260
|
*/
|
|
864
|
-
function
|
|
865
|
-
const
|
|
866
|
-
const
|
|
867
|
-
const [pickerOpen, setPickerOpen] = (0, react.useState)(false);
|
|
1261
|
+
function ModelPickerHost({ sessionId, picker, available, directory, load, select, resolveText, factsSource, t }) {
|
|
1262
|
+
const request = (0, react.useSyncExternalStore)(picker.subscribe, picker.getSnapshot);
|
|
1263
|
+
const state = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot());
|
|
868
1264
|
const [favorites, setFavorites] = (0, react.useState)(() => readFavorites(storageOf()));
|
|
869
1265
|
const [memory, setMemory] = (0, react.useState)(() => readEfforts(storageOf()));
|
|
870
1266
|
const [toast, setToast] = (0, react.useState)(null);
|
|
871
1267
|
const toastSeq = (0, react.useRef)(0);
|
|
872
1268
|
const rootRef = (0, react.useRef)(null);
|
|
873
|
-
const triggerRef = (0, react.useRef)(null);
|
|
874
|
-
const menuRef = (0, react.useRef)(null);
|
|
875
|
-
const id = (0, react.useId)();
|
|
876
|
-
const groups = state.groups;
|
|
877
|
-
const current = state.current;
|
|
878
|
-
const currentGroup = current === null ? void 0 : groups.find((group) => group.id === current.provider);
|
|
879
|
-
const currentModel = currentGroup?.models.find((model) => model.id === current?.model);
|
|
880
|
-
const reasoning = currentModel?.reasoning;
|
|
881
|
-
const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
882
|
-
/**
|
|
883
|
-
* The trigger's caption. A model that advertises no levels still says so in
|
|
884
|
-
* the same place: the blue caption beside the name reads 默认 rather than
|
|
885
|
-
* disappearing, so the shape of the control never changes with the model.
|
|
886
|
-
*/
|
|
887
|
-
const effortLabel = reasoning === void 0 || effectiveEffort === void 0 ? t("effort.providerDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
|
|
888
|
-
const choices = (0, react.useMemo)(() => effortChoices(reasoning, t("effort.providerDefault")), [reasoning, t]);
|
|
889
1269
|
const busy = state.status === "selecting";
|
|
890
1270
|
const live = (0, react.useMemo)(() => selectionOf(state), [state]);
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
});
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1271
|
+
const facts = (0, react.useSyncExternalStore)((listener) => factsSource.subscribe(listener), () => factsSource.getSnapshot());
|
|
1272
|
+
const open = request.sessionId === sessionId;
|
|
1273
|
+
const loadRef = (0, react.useRef)(load);
|
|
1274
|
+
loadRef.current = load;
|
|
1275
|
+
(0, react.useEffect)(() => {
|
|
1276
|
+
if (open) loadRef.current();
|
|
1277
|
+
}, [open]);
|
|
1278
|
+
const current = state.current;
|
|
1279
|
+
const rememberedRef = (0, react.useRef)(null);
|
|
1280
|
+
(0, react.useEffect)(() => {
|
|
1281
|
+
if (current === null) return;
|
|
1282
|
+
const key = `${current.provider}/${current.model}/${current.reasoningEffort ?? ""}`;
|
|
1283
|
+
if (rememberedRef.current === key) return;
|
|
1284
|
+
rememberedRef.current = key;
|
|
1285
|
+
setMemory(rememberEffort(storageOf(), current.provider, current.model, current.reasoningEffort));
|
|
1286
|
+
}, [current]);
|
|
1287
|
+
if (!available) return null;
|
|
905
1288
|
const raiseToast = (text) => {
|
|
906
1289
|
toastSeq.current += 1;
|
|
907
1290
|
setToast({
|
|
@@ -909,352 +1292,47 @@ window.__ModuleLoader__.load({
|
|
|
909
1292
|
text
|
|
910
1293
|
});
|
|
911
1294
|
};
|
|
912
|
-
const
|
|
913
|
-
load();
|
|
914
|
-
};
|
|
915
|
-
const submit = (selection, onDone, onRejected) => {
|
|
1295
|
+
const submit = (selection) => {
|
|
916
1296
|
select(selection).then((result) => {
|
|
917
1297
|
if (result === void 0 || result.ok) {
|
|
918
|
-
|
|
919
|
-
|
|
1298
|
+
setMemory(rememberEffort(storageOf(), selection.provider, selection.model, selection.reasoningEffort));
|
|
1299
|
+
picker.close();
|
|
920
1300
|
return;
|
|
921
1301
|
}
|
|
922
|
-
onRejected?.();
|
|
923
1302
|
raiseToast(result.error.code === "session/writer-held" ? t("error.sessionInUse") : t("error.action", { message: `${result.error.code}: ${result.error.message}` }));
|
|
924
1303
|
});
|
|
925
1304
|
};
|
|
926
|
-
/** Close the menu. Closing writes nothing: every choice already committed on click. */
|
|
927
|
-
const closeMenu = (0, react.useCallback)((restoreFocus) => {
|
|
928
|
-
setOpen(false);
|
|
929
|
-
if (restoreFocus) queueMicrotask(() => {
|
|
930
|
-
triggerRef.current?.focus();
|
|
931
|
-
});
|
|
932
|
-
}, []);
|
|
933
|
-
(0, react.useEffect)(() => {
|
|
934
|
-
if (!open) return;
|
|
935
|
-
const closeOutside = (event) => {
|
|
936
|
-
if (rootRef.current?.contains(event.target) === true) return;
|
|
937
|
-
if (menuRef.current?.contains(event.target) === true) return;
|
|
938
|
-
closeMenu(false);
|
|
939
|
-
};
|
|
940
|
-
document.addEventListener("mousedown", closeOutside);
|
|
941
|
-
return () => {
|
|
942
|
-
document.removeEventListener("mousedown", closeOutside);
|
|
943
|
-
};
|
|
944
|
-
}, [open, closeMenu]);
|
|
945
|
-
(0, react.useEffect)(() => {
|
|
946
|
-
if (!open) return;
|
|
947
|
-
const frame = requestAnimationFrame(() => {
|
|
948
|
-
const checked = menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])");
|
|
949
|
-
const first = menuRef.current?.querySelector("[role=\"menuitemradio\"]:not([disabled])");
|
|
950
|
-
const target = checked ?? first ?? triggerRef.current;
|
|
951
|
-
target?.scrollIntoView({ block: "nearest" });
|
|
952
|
-
target?.focus();
|
|
953
|
-
});
|
|
954
|
-
return () => {
|
|
955
|
-
cancelAnimationFrame(frame);
|
|
956
|
-
};
|
|
957
|
-
}, [open]);
|
|
958
|
-
const menuPos = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
|
|
959
|
-
open: open && available,
|
|
960
|
-
anchorRef: triggerRef,
|
|
961
|
-
panelRef: menuRef,
|
|
962
|
-
side: "top",
|
|
963
|
-
align: "end",
|
|
964
|
-
gap: 8,
|
|
965
|
-
margin: 12,
|
|
966
|
-
layoutKey: `${state.status}:${String(menuRows.length)}:${String(choices.length)}`
|
|
967
|
-
});
|
|
968
|
-
if (!available) return null;
|
|
969
|
-
const show = () => {
|
|
970
|
-
setOpen(true);
|
|
971
|
-
reload();
|
|
972
|
-
};
|
|
973
|
-
/** The picker door: the level already landed on click, so only the surface changes. */
|
|
974
|
-
const openPicker = () => {
|
|
975
|
-
setOpen(false);
|
|
976
|
-
setPickerOpen(true);
|
|
977
|
-
};
|
|
978
|
-
/** Record one model's level, so switching away and back restores it. */
|
|
979
|
-
const remember = (provider, model, effort) => {
|
|
980
|
-
setMemory(rememberEffort(storageOf(), provider, model, effort));
|
|
981
|
-
};
|
|
982
|
-
const chooseModel = (provider, model) => {
|
|
983
|
-
const picked = planModelCommit(live, provider, model.id, reason, effortFor(memory, provider, model.id));
|
|
984
|
-
if (picked === null) {
|
|
985
|
-
setOpen(false);
|
|
986
|
-
queueMicrotask(() => {
|
|
987
|
-
triggerRef.current?.focus();
|
|
988
|
-
});
|
|
989
|
-
return;
|
|
990
|
-
}
|
|
991
|
-
setOpen(false);
|
|
992
|
-
submit(picked, () => {
|
|
993
|
-
queueMicrotask(() => {
|
|
994
|
-
triggerRef.current?.focus();
|
|
995
|
-
});
|
|
996
|
-
}, () => {
|
|
997
|
-
setOpen(true);
|
|
998
|
-
});
|
|
999
|
-
};
|
|
1000
|
-
const rowButtons = () => [...menuRef.current?.querySelectorAll("[role=\"menuitemradio\"]:not([disabled])") ?? []];
|
|
1001
|
-
const moveFocus = (offset) => {
|
|
1002
|
-
const items = rowButtons();
|
|
1003
|
-
if (items.length === 0) return;
|
|
1004
|
-
const active = items.findIndex((item) => item === document.activeElement);
|
|
1005
|
-
items[active === -1 ? offset > 0 ? 0 : items.length - 1 : (active + offset + items.length) % items.length]?.focus();
|
|
1006
|
-
};
|
|
1007
|
-
const onEffortKeyDown = (event) => {
|
|
1008
|
-
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
|
|
1009
|
-
if (choices.length === 0 || live === null) return;
|
|
1010
|
-
event.preventDefault();
|
|
1011
|
-
const index = choices.findIndex((choice) => choice.effort === effectiveEffort);
|
|
1012
|
-
const step = event.key === "ArrowRight" ? 1 : -1;
|
|
1013
|
-
const next = choices[((index === -1 ? 0 : index) + step + choices.length) % choices.length];
|
|
1014
|
-
if (next === void 0) return;
|
|
1015
|
-
const pending = planEffortCommit(live, next.effort);
|
|
1016
|
-
if (pending !== null) submit(pending, null);
|
|
1017
|
-
};
|
|
1018
|
-
const onRootKeyDown = (event) => {
|
|
1019
|
-
if (event.key === "Escape" && open) {
|
|
1020
|
-
event.preventDefault();
|
|
1021
|
-
closeMenu(true);
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
if (!open) return;
|
|
1025
|
-
if (event.key === "Tab") {
|
|
1026
|
-
if (event.shiftKey) {
|
|
1027
|
-
event.preventDefault();
|
|
1028
|
-
closeMenu(true);
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const focused = document.activeElement;
|
|
1032
|
-
const rows = rowButtons();
|
|
1033
|
-
if (focused instanceof HTMLButtonElement && rows.includes(focused)) {
|
|
1034
|
-
event.preventDefault();
|
|
1035
|
-
focused.click();
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
if (focused !== triggerRef.current) return;
|
|
1039
|
-
event.preventDefault();
|
|
1040
|
-
(menuRef.current?.querySelector("[role=\"menuitemradio\"][aria-checked=\"true\"]:not([disabled])") ?? rows[0])?.focus();
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
1044
|
-
event.preventDefault();
|
|
1045
|
-
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
1046
|
-
}
|
|
1047
|
-
};
|
|
1048
|
-
/**
|
|
1049
|
-
* The keyboard leaving the seat closes the menu — but that is not one of the
|
|
1050
|
-
* four closing gestures that write a mark, so it closes without committing.
|
|
1051
|
-
* Otherwise clicking the notice text or a divider inside the menu, which moves
|
|
1052
|
-
* focus without unmounting it, would land a level nobody asked to commit.
|
|
1053
|
-
*/
|
|
1054
|
-
const onBlur = (event) => {
|
|
1055
|
-
if (pickerOpen) return;
|
|
1056
|
-
if (event.relatedTarget instanceof Node && (rootRef.current?.contains(event.relatedTarget) === true || menuRef.current?.contains(event.relatedTarget) === true)) return;
|
|
1057
|
-
setOpen(false);
|
|
1058
|
-
};
|
|
1059
|
-
const toggleFavoriteOf = (provider, model) => {
|
|
1060
|
-
setFavorites(toggleFavorite(storageOf(), provider, model));
|
|
1061
|
-
};
|
|
1062
|
-
const waiting = current === null && state.status === "loading";
|
|
1063
|
-
const modelLabel = waiting ? t("trigger.loading") : currentModel?.name ?? (current === null ? t("trigger.fallback") : `${current.provider}/${current.model}`);
|
|
1064
|
-
const triggerAria = waiting ? t("trigger.loading") : current === null ? t("trigger.selectAria") : effortLabel === void 0 ? t("trigger.aria", { model: modelLabel }) : t("trigger.ariaEffort", {
|
|
1065
|
-
model: modelLabel,
|
|
1066
|
-
effort: effortLabel
|
|
1067
|
-
});
|
|
1068
|
-
const providerName = currentGroup?.name ?? state.failures.find((failure) => failure.id === current?.provider)?.name ?? current?.provider ?? "";
|
|
1069
|
-
const providerFailure = current === null ? void 0 : state.failures.find((failure) => failure.id === current.provider);
|
|
1070
1305
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1071
1306
|
ref: rootRef,
|
|
1072
|
-
className:
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}),
|
|
1100
|
-
effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1101
|
-
className: ModelSubmenu_module_css_default.triggerEffort,
|
|
1102
|
-
children: effortLabel
|
|
1103
|
-
}),
|
|
1104
|
-
busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: "ongoing" }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: cls(ModelSubmenu_module_css_default.chevron, open && ModelSubmenu_module_css_default.chevronOpen) })
|
|
1105
|
-
]
|
|
1106
|
-
}),
|
|
1107
|
-
open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
|
|
1108
|
-
ref: menuRef,
|
|
1109
|
-
id: `${id}-menu`,
|
|
1110
|
-
className: ModelSubmenu_module_css_default.menu,
|
|
1111
|
-
style: menuPos ?? MEASURE_STYLE,
|
|
1112
|
-
role: "group",
|
|
1113
|
-
"aria-label": t("menu.aria"),
|
|
1114
|
-
"aria-busy": state.status === "loading" || busy,
|
|
1115
|
-
children: [
|
|
1116
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1117
|
-
className: ModelSubmenu_module_css_default.providerName,
|
|
1118
|
-
title: providerName,
|
|
1119
|
-
children: providerName
|
|
1120
|
-
}),
|
|
1121
|
-
state.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1122
|
-
className: ModelSubmenu_module_css_default.notice,
|
|
1123
|
-
children: t("status.loading")
|
|
1124
|
-
}),
|
|
1125
|
-
state.error !== null && state.status === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1126
|
-
className: ModelSubmenu_module_css_default.notice,
|
|
1127
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.failed", { message: state.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1128
|
-
type: "button",
|
|
1129
|
-
className: ModelSubmenu_module_css_default.retry,
|
|
1130
|
-
onClick: reload,
|
|
1131
|
-
children: t("action.reload")
|
|
1132
|
-
})]
|
|
1133
|
-
}),
|
|
1134
|
-
providerFailure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1135
|
-
className: ModelSubmenu_module_css_default.notice,
|
|
1136
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("status.providerFailed", {
|
|
1137
|
-
name: providerFailure.name,
|
|
1138
|
-
message: providerFailure.message
|
|
1139
|
-
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1140
|
-
type: "button",
|
|
1141
|
-
className: ModelSubmenu_module_css_default.retry,
|
|
1142
|
-
onClick: reload,
|
|
1143
|
-
children: t("action.reload")
|
|
1144
|
-
})]
|
|
1145
|
-
}),
|
|
1146
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1147
|
-
className: cls(ModelSubmenu_module_css_default.groups, "scrollable"),
|
|
1148
|
-
role: "menu",
|
|
1149
|
-
"aria-label": t("menu.aria"),
|
|
1150
|
-
children: [menuRows.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelRow, {
|
|
1151
|
-
name: model.name,
|
|
1152
|
-
badge: badgeOf(model),
|
|
1153
|
-
selected: current !== null && currentGroup !== void 0 && current.provider === currentGroup.id && current.model === model.id,
|
|
1154
|
-
busy,
|
|
1155
|
-
star: "static",
|
|
1156
|
-
favorited: currentGroup !== void 0 && isFavorite(favorites, currentGroup.id, model.id),
|
|
1157
|
-
onPick: () => {
|
|
1158
|
-
if (currentGroup !== void 0) chooseModel(currentGroup.id, model);
|
|
1159
|
-
}
|
|
1160
|
-
}, model.id)), menuRows.length === 0 && state.status === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1161
|
-
className: ModelSubmenu_module_css_default.notice,
|
|
1162
|
-
children: t("status.empty")
|
|
1163
|
-
})]
|
|
1164
|
-
}),
|
|
1165
|
-
choices.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1166
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider }),
|
|
1167
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1168
|
-
className: ModelSubmenu_module_css_default.effortRow,
|
|
1169
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1170
|
-
className: ModelSubmenu_module_css_default.effortHeading,
|
|
1171
|
-
children: t("menu.effort")
|
|
1172
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1173
|
-
className: ModelSubmenu_module_css_default.effortTrack,
|
|
1174
|
-
role: "radiogroup",
|
|
1175
|
-
"aria-label": t("menu.effort"),
|
|
1176
|
-
onKeyDown: onEffortKeyDown,
|
|
1177
|
-
children: choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1178
|
-
type: "button",
|
|
1179
|
-
role: "radio",
|
|
1180
|
-
"aria-checked": choice.effort === effectiveEffort,
|
|
1181
|
-
className: ModelSubmenu_module_css_default.effortSegment,
|
|
1182
|
-
disabled: busy,
|
|
1183
|
-
onClick: () => {
|
|
1184
|
-
if (live !== null) {
|
|
1185
|
-
const pending = planEffortCommit(live, choice.effort);
|
|
1186
|
-
if (pending !== null) submit(pending, null);
|
|
1187
|
-
}
|
|
1188
|
-
},
|
|
1189
|
-
children: choice.label
|
|
1190
|
-
}, choice.key))
|
|
1191
|
-
})]
|
|
1192
|
-
}),
|
|
1193
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider })
|
|
1194
|
-
] }),
|
|
1195
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1196
|
-
className: ModelSubmenu_module_css_default.hint,
|
|
1197
|
-
children: t("menu.hint")
|
|
1198
|
-
}),
|
|
1199
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: ModelSubmenu_module_css_default.divider }),
|
|
1200
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1201
|
-
type: "button",
|
|
1202
|
-
role: "menuitem",
|
|
1203
|
-
className: ModelSubmenu_module_css_default.moreRow,
|
|
1204
|
-
onClick: openPicker,
|
|
1205
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {
|
|
1206
|
-
className: ModelSubmenu_module_css_default.moreIcon,
|
|
1207
|
-
size: 16
|
|
1208
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("menu.more") })]
|
|
1209
|
-
})
|
|
1210
|
-
]
|
|
1211
|
-
}), document.body),
|
|
1212
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
1213
|
-
open: pickerOpen,
|
|
1214
|
-
resolveText,
|
|
1215
|
-
state,
|
|
1216
|
-
live,
|
|
1217
|
-
facts,
|
|
1218
|
-
favorites,
|
|
1219
|
-
busy,
|
|
1220
|
-
t,
|
|
1221
|
-
onPick: (selection) => {
|
|
1222
|
-
submit(selection, () => {
|
|
1223
|
-
setPickerOpen(false);
|
|
1224
|
-
});
|
|
1225
|
-
},
|
|
1226
|
-
onToggleFavorite: toggleFavoriteOf,
|
|
1227
|
-
onReload: reload,
|
|
1228
|
-
onClose: () => {
|
|
1229
|
-
setPickerOpen(false);
|
|
1230
|
-
queueMicrotask(() => {
|
|
1231
|
-
triggerRef.current?.focus();
|
|
1232
|
-
});
|
|
1233
|
-
}
|
|
1234
|
-
}),
|
|
1235
|
-
toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
1236
|
-
text: toast.text,
|
|
1237
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
|
|
1238
|
-
anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
|
|
1239
|
-
onDone: () => {
|
|
1240
|
-
setToast(null);
|
|
1241
|
-
}
|
|
1242
|
-
}, toast.seq)
|
|
1243
|
-
]
|
|
1307
|
+
className: ModelOptions_module_css_default.pickerHost,
|
|
1308
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelPicker, {
|
|
1309
|
+
open,
|
|
1310
|
+
resolveText,
|
|
1311
|
+
state,
|
|
1312
|
+
live,
|
|
1313
|
+
facts,
|
|
1314
|
+
favorites,
|
|
1315
|
+
busy,
|
|
1316
|
+
t,
|
|
1317
|
+
rememberedOf: (provider, model) => effortFor(memory, provider, model),
|
|
1318
|
+
onPick: submit,
|
|
1319
|
+
onToggleFavorite: (provider, model) => {
|
|
1320
|
+
setFavorites(toggleFavorite(storageOf(), provider, model));
|
|
1321
|
+
},
|
|
1322
|
+
onReload: load,
|
|
1323
|
+
onClose: () => {
|
|
1324
|
+
picker.close();
|
|
1325
|
+
}
|
|
1326
|
+
}), toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
1327
|
+
text: toast.text,
|
|
1328
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
|
|
1329
|
+
anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
|
|
1330
|
+
onDone: () => {
|
|
1331
|
+
setToast(null);
|
|
1332
|
+
}
|
|
1333
|
+
}, toast.seq)]
|
|
1244
1334
|
});
|
|
1245
1335
|
}
|
|
1246
|
-
/** The selection in force, or null while the host has not resolved one. */
|
|
1247
|
-
function selectionOf(state) {
|
|
1248
|
-
if (state.current === null) return null;
|
|
1249
|
-
return state.current.reasoningEffort === void 0 ? {
|
|
1250
|
-
provider: state.current.provider,
|
|
1251
|
-
model: state.current.model
|
|
1252
|
-
} : {
|
|
1253
|
-
provider: state.current.provider,
|
|
1254
|
-
model: state.current.model,
|
|
1255
|
-
reasoningEffort: state.current.reasoningEffort
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
1336
|
/** Isolate the browser's storage so a locked-down browser reads as "no favorites". */
|
|
1259
1337
|
function storageOf() {
|
|
1260
1338
|
try {
|
|
@@ -1264,6 +1342,35 @@ window.__ModuleLoader__.load({
|
|
|
1264
1342
|
}
|
|
1265
1343
|
}
|
|
1266
1344
|
//#endregion
|
|
1345
|
+
//#region src/client/picker-state.ts
|
|
1346
|
+
/**
|
|
1347
|
+
* @returns a store that starts closed.
|
|
1348
|
+
*/
|
|
1349
|
+
function createPickerStore() {
|
|
1350
|
+
let snapshot = { sessionId: null };
|
|
1351
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1352
|
+
const publish = (next) => {
|
|
1353
|
+
if (next.sessionId === snapshot.sessionId) return;
|
|
1354
|
+
snapshot = next;
|
|
1355
|
+
for (const listener of listeners) listener();
|
|
1356
|
+
};
|
|
1357
|
+
return {
|
|
1358
|
+
getSnapshot: () => snapshot,
|
|
1359
|
+
subscribe: (listener) => {
|
|
1360
|
+
listeners.add(listener);
|
|
1361
|
+
return () => {
|
|
1362
|
+
listeners.delete(listener);
|
|
1363
|
+
};
|
|
1364
|
+
},
|
|
1365
|
+
open: (sessionId) => {
|
|
1366
|
+
publish({ sessionId });
|
|
1367
|
+
},
|
|
1368
|
+
close: () => {
|
|
1369
|
+
publish({ sessionId: null });
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
//#endregion
|
|
1267
1374
|
//#region src/client/facts-source.ts
|
|
1268
1375
|
const EMPTY = () => ({
|
|
1269
1376
|
contextWindow: void 0,
|
|
@@ -1311,21 +1418,11 @@ window.__ModuleLoader__.load({
|
|
|
1311
1418
|
}
|
|
1312
1419
|
//#endregion
|
|
1313
1420
|
//#region src/client/locales.ts
|
|
1314
|
-
/** Locale bundles for the
|
|
1421
|
+
/** Locale bundles for the all-provider model picker. */
|
|
1315
1422
|
/** Dictionary namespace owned by this plugin. */
|
|
1316
1423
|
const NS = "modelSubmenu";
|
|
1317
1424
|
/** Simplified Chinese dictionary (the key-set source of truth). */
|
|
1318
1425
|
const zh = {
|
|
1319
|
-
"trigger.fallback": "选择模型",
|
|
1320
|
-
"trigger.loading": "正在加载模型…",
|
|
1321
|
-
"trigger.selectAria": "选择模型",
|
|
1322
|
-
"trigger.aria": "选择模型,当前 {model}",
|
|
1323
|
-
"trigger.ariaEffort": "选择模型,当前 {model},思考等级 {effort}",
|
|
1324
|
-
"menu.aria": "模型与思考等级",
|
|
1325
|
-
"menu.effort": "思考",
|
|
1326
|
-
"menu.more": "更多模型…",
|
|
1327
|
-
"menu.hint": "提示:切换模型或思考程度会使既有的提示词缓存失效。建议新建会话,避免额外的 token 消耗。",
|
|
1328
|
-
"effort.providerDefault": "默认",
|
|
1329
1426
|
"status.loading": "正在加载模型…",
|
|
1330
1427
|
"status.empty": "没有可用的模型。",
|
|
1331
1428
|
"status.failed": "模型列表加载失败:{message}",
|
|
@@ -1344,20 +1441,13 @@ window.__ModuleLoader__.load({
|
|
|
1344
1441
|
"picker.hintSelect": "选择",
|
|
1345
1442
|
"picker.hintClose": "关闭",
|
|
1346
1443
|
"meta.effort": "思考",
|
|
1347
|
-
"meta.images": "图片输入"
|
|
1444
|
+
"meta.images": "图片输入",
|
|
1445
|
+
"effort.label": "推理等级",
|
|
1446
|
+
"effort.providerDefault": "Default",
|
|
1447
|
+
"effort.useDefault": "切换为默认档位"
|
|
1348
1448
|
};
|
|
1349
1449
|
/** English dictionary, checked complete against the zh key set. */
|
|
1350
1450
|
const en = {
|
|
1351
|
-
"trigger.fallback": "Select model",
|
|
1352
|
-
"trigger.loading": "Loading models…",
|
|
1353
|
-
"trigger.selectAria": "Select model",
|
|
1354
|
-
"trigger.aria": "Select model, current {model}",
|
|
1355
|
-
"trigger.ariaEffort": "Select model, current {model}, effort {effort}",
|
|
1356
|
-
"menu.aria": "Model and effort",
|
|
1357
|
-
"menu.effort": "Effort",
|
|
1358
|
-
"menu.more": "More models…",
|
|
1359
|
-
"menu.hint": "Changing the model or the effort invalidates the existing prompt cache. Start a new session to avoid the extra token cost.",
|
|
1360
|
-
"effort.providerDefault": "Default",
|
|
1361
1451
|
"status.loading": "Loading models…",
|
|
1362
1452
|
"status.empty": "No models available.",
|
|
1363
1453
|
"status.failed": "Model list failed to load: {message}",
|
|
@@ -1376,14 +1466,17 @@ window.__ModuleLoader__.load({
|
|
|
1376
1466
|
"picker.hintSelect": "select",
|
|
1377
1467
|
"picker.hintClose": "close",
|
|
1378
1468
|
"meta.effort": "Effort",
|
|
1379
|
-
"meta.images": "Images"
|
|
1469
|
+
"meta.images": "Images",
|
|
1470
|
+
"effort.label": "Reasoning effort",
|
|
1471
|
+
"effort.providerDefault": "Default",
|
|
1472
|
+
"effort.useDefault": "Switch to the provider default"
|
|
1380
1473
|
};
|
|
1381
1474
|
//#endregion
|
|
1382
1475
|
//#region src/client/index.ts
|
|
1383
1476
|
/**
|
|
1384
1477
|
* Cordis services required by the browser half. `slots` is the contribution
|
|
1385
1478
|
* registry, `sessions` tells whether a session may select a model,
|
|
1386
|
-
* `modelDirectories` owns the per-session catalog snapshot
|
|
1479
|
+
* `modelDirectories` owns the per-session catalog snapshot both entries render
|
|
1387
1480
|
* from, and `configForms` carries the settings provider's shared describe
|
|
1388
1481
|
* mirror, which answers each model's context window and accepted modalities.
|
|
1389
1482
|
* `remote`, `remote.llm`, and `remote.session` are declared because a service
|
|
@@ -1402,9 +1495,14 @@ window.__ModuleLoader__.load({
|
|
|
1402
1495
|
"slots"
|
|
1403
1496
|
];
|
|
1404
1497
|
/**
|
|
1405
|
-
* Client plugin body: register this plugin's dictionaries, then
|
|
1406
|
-
*
|
|
1407
|
-
* renders
|
|
1498
|
+
* Client plugin body: register this plugin's dictionaries, then contribute the
|
|
1499
|
+
* bridge that turns the Host's model cell into the picker's door and its effort
|
|
1500
|
+
* cell into the reasoning-level slider, the overlay that renders the picker, and
|
|
1501
|
+
* the tint that keeps the collapsed trigger's effort caption on the same color.
|
|
1502
|
+
*
|
|
1503
|
+
* The two are separate entries because the Host unmounts the model menu the
|
|
1504
|
+
* moment a row is clicked; the overlay slot stays mounted, so the dialog
|
|
1505
|
+
* survives the menu's teardown.
|
|
1408
1506
|
* @param ctx - client root context.
|
|
1409
1507
|
*/
|
|
1410
1508
|
function apply(ctx) {
|
|
@@ -1420,37 +1518,62 @@ window.__ModuleLoader__.load({
|
|
|
1420
1518
|
], (scope) => {
|
|
1421
1519
|
const models = scope.modelDirectories;
|
|
1422
1520
|
const sessions = scope.sessions;
|
|
1521
|
+
const picker = createPickerStore();
|
|
1423
1522
|
const facts = createFactsSource({
|
|
1424
1523
|
describe: scope.configForms.describe(),
|
|
1425
1524
|
listProviders: () => scope.remote.llm.listConfigurableProviders()
|
|
1426
1525
|
});
|
|
1427
1526
|
ctx.effect(() => facts.start(), "dsh-model-submenu: model metadata");
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1527
|
+
/**
|
|
1528
|
+
* Build one entry's business face for a session.
|
|
1529
|
+
* @param sessionId - the slot's session scope.
|
|
1530
|
+
* @returns the catalog face plus the session and store both entries need.
|
|
1531
|
+
*/
|
|
1532
|
+
const faceOf = (sessionId) => {
|
|
1533
|
+
const key = sessionId;
|
|
1534
|
+
const directory = models.directoryFor(key);
|
|
1535
|
+
const available = sessions.subagentAddress(key) === void 0;
|
|
1536
|
+
return {
|
|
1537
|
+
sessionId,
|
|
1538
|
+
picker,
|
|
1539
|
+
available,
|
|
1540
|
+
directory: directory.store,
|
|
1541
|
+
factsSource: facts.source,
|
|
1542
|
+
load: () => {
|
|
1543
|
+
if (available) directory.load().catch(() => {});
|
|
1544
|
+
},
|
|
1545
|
+
resolveText: (text) => ctx.locale.resolveText(text),
|
|
1546
|
+
select: (selection) => available ? directory.select(selection) : Promise.resolve(void 0)
|
|
1547
|
+
};
|
|
1548
|
+
};
|
|
1549
|
+
scope.slots.inject("conversation.input.model.options", () => scope.slots.register({
|
|
1550
|
+
name: "conversation.input.model.options",
|
|
1551
|
+
id: "model-menu-bridge",
|
|
1431
1552
|
registrant: "@lynn123411/dsh-model-submenu",
|
|
1432
1553
|
locale: NS,
|
|
1433
|
-
inject:
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
}, ModelSubmenu));
|
|
1554
|
+
inject: faceOf
|
|
1555
|
+
}, ModelMenuBridge));
|
|
1556
|
+
scope.slots.inject("conversation.input.overlay", () => scope.slots.register({
|
|
1557
|
+
name: "conversation.input.overlay",
|
|
1558
|
+
id: "model-picker",
|
|
1559
|
+
registrant: "@lynn123411/dsh-model-submenu",
|
|
1560
|
+
locale: NS,
|
|
1561
|
+
inject: faceOf
|
|
1562
|
+
}, ModelPickerHost));
|
|
1563
|
+
scope.slots.inject("conversation.input.overlay", () => scope.slots.register({
|
|
1564
|
+
name: "conversation.input.overlay",
|
|
1565
|
+
id: "model-effort-tint",
|
|
1566
|
+
registrant: "@lynn123411/dsh-model-submenu",
|
|
1567
|
+
inject: faceOf
|
|
1568
|
+
}, EffortTint));
|
|
1449
1569
|
});
|
|
1450
1570
|
}
|
|
1451
1571
|
//#endregion
|
|
1452
|
-
exports.
|
|
1572
|
+
exports.EffortTint = EffortTint;
|
|
1573
|
+
exports.ModelMenuBridge = ModelMenuBridge;
|
|
1574
|
+
exports.ModelPickerHost = ModelPickerHost;
|
|
1453
1575
|
exports.apply = apply;
|
|
1576
|
+
exports.createPickerStore = createPickerStore;
|
|
1454
1577
|
exports.inject = inject;
|
|
1455
1578
|
return module.exports;
|
|
1456
1579
|
}
|