dsh-ui-fortifier 0.1.2 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/README.zh.md +10 -3
- package/lib/client.js +1006 -34
- package/lib/index.js +162 -2
- package/lib/types/client/index.d.ts +4 -2
- package/lib/types/client/locales.d.ts +1 -1
- package/lib/types/client/model-picker/ModelPicker.d.ts +23 -0
- package/lib/types/client/model-picker/index.d.ts +11 -0
- package/lib/types/client/open-dsh-folder/OpenDshFolderAction.d.ts +18 -0
- package/lib/types/client/open-dsh-folder/index.d.ts +12 -0
- package/lib/types/client/settings/FortifierSettingsPage.d.ts +1 -1
- package/lib/types/client/settings/index.d.ts +3 -3
- package/lib/types/client/settings-frame/SettingsFrameEnhancer.d.ts +14 -0
- package/lib/types/client/settings-frame/index.d.ts +12 -0
- package/lib/types/index.d.ts +7 -8
- package/lib/types/open-dsh-folder.d.ts +19 -0
- package/lib/types/types.d.ts +23 -0
- package/package.json +43 -34
- package/lib/invariant.js +0 -12
- package/lib/types/client/provider-badge/ProviderBadge.d.ts +0 -4
- package/lib/types/client/provider-badge/index.d.ts +0 -23
- package/lib/types/invariant.d.ts +0 -5
package/lib/client.js
CHANGED
|
@@ -5,25 +5,32 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
7
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
let react_dom = require("react-dom");
|
|
8
11
|
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\settings\FortifierSettingsPage.module.css.mjs
|
|
9
|
-
const css$
|
|
10
|
-
const tagId$
|
|
11
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$
|
|
12
|
+
const css$4 = ".t1_2kq_page{flex-direction:column;gap:8px;max-width:520px;padding:16px;display:flex}.t1_2kq_heading{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;line-height:24px}.t1_2kq_list{flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.t1_2kq_row{justify-content:space-between;align-items:center;gap:12px;padding:10px 12px;display:flex}.t1_2kq_row+.t1_2kq_row{border-top:.5px solid var(--dsw-alias-border-l3)}.t1_2kq_row:hover{background:var(--dsw-alias-interactive-bg-hover)}.t1_2kq_rowLabel{color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}.t1_2kq_switch{box-sizing:border-box;background:var(--dsw-alias-border-l3);cursor:pointer;border:0;border-radius:10px;flex:none;width:36px;height:20px;padding:2px;position:relative}.t1_2kq_switchOn{background:var(--dsw-alias-brand-primary)}.t1_2kq_switch:disabled{cursor:default;opacity:.5}.t1_2kq_switch:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.t1_2kq_thumb{corner-shape:round;background:var(--dsw-alias-label-primary-foreground);border-radius:50%;width:16px;height:16px;transition:transform .12s;display:block}.t1_2kq_switchOn .t1_2kq_thumb{transform:translate(16px)}";
|
|
13
|
+
const tagId$4 = "dsh-ui-fortifier/FortifierSettingsPage.module.css";
|
|
14
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
|
|
12
15
|
const tag = document.createElement("style");
|
|
13
16
|
tag.dataset.plugin = "dsh-ui-fortifier";
|
|
14
|
-
tag.dataset.pluginCss = tagId$
|
|
15
|
-
tag.textContent = css$
|
|
17
|
+
tag.dataset.pluginCss = tagId$4;
|
|
18
|
+
tag.textContent = css$4;
|
|
16
19
|
document.head.appendChild(tag);
|
|
17
20
|
}
|
|
18
21
|
var FortifierSettingsPage_module_css_default = {
|
|
19
22
|
"heading": "t1_2kq_heading",
|
|
20
23
|
"list": "t1_2kq_list",
|
|
21
24
|
"page": "t1_2kq_page",
|
|
22
|
-
"row": "t1_2kq_row"
|
|
25
|
+
"row": "t1_2kq_row",
|
|
26
|
+
"rowLabel": "t1_2kq_rowLabel",
|
|
27
|
+
"switch": "t1_2kq_switch",
|
|
28
|
+
"switchOn": "t1_2kq_switchOn",
|
|
29
|
+
"thumb": "t1_2kq_thumb"
|
|
23
30
|
};
|
|
24
31
|
//#endregion
|
|
25
32
|
//#region src/client/settings/FortifierSettingsPage.tsx
|
|
26
|
-
/**
|
|
33
|
+
/** 设置页:读取配置项自动生成切换按钮行。 */
|
|
27
34
|
function FortifierSettingsPage(props) {
|
|
28
35
|
const t = props.t;
|
|
29
36
|
const rows = props.useToggles((value) => value);
|
|
@@ -34,16 +41,26 @@ window.__ModuleLoader__.load({
|
|
|
34
41
|
children: t("nav")
|
|
35
42
|
}), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("empty") }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
36
43
|
className: FortifierSettingsPage_module_css_default.list,
|
|
37
|
-
children: rows.map((row) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
children: rows.map((row) => {
|
|
45
|
+
const label = t(`config.${row.field}`);
|
|
46
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
47
|
+
className: FortifierSettingsPage_module_css_default.row,
|
|
48
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
49
|
+
className: FortifierSettingsPage_module_css_default.rowLabel,
|
|
50
|
+
children: label
|
|
51
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
52
|
+
type: "button",
|
|
53
|
+
role: "switch",
|
|
54
|
+
"aria-checked": row.value,
|
|
55
|
+
"aria-label": label,
|
|
56
|
+
className: `${FortifierSettingsPage_module_css_default.switch} ${row.value ? FortifierSettingsPage_module_css_default.switchOn : ""}`,
|
|
57
|
+
onClick: () => {
|
|
58
|
+
props.set(row.field, !row.value);
|
|
59
|
+
},
|
|
60
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: FortifierSettingsPage_module_css_default.thumb })
|
|
61
|
+
})]
|
|
62
|
+
}, row.field);
|
|
63
|
+
})
|
|
47
64
|
})]
|
|
48
65
|
});
|
|
49
66
|
}
|
|
@@ -53,13 +70,53 @@ window.__ModuleLoader__.load({
|
|
|
53
70
|
const en = {
|
|
54
71
|
nav: "ui-fortifier",
|
|
55
72
|
empty: "No feature modules registered.",
|
|
56
|
-
"config.provider-label": "
|
|
73
|
+
"config.provider-label": "Input - Provider",
|
|
74
|
+
"config.open-dsh-folder": "Settings top-right - Open .dsh folder",
|
|
75
|
+
"config.settings-frame": "Settings panel - Drag and resize",
|
|
76
|
+
"config.model-picker": "Input - Cascading model picker",
|
|
77
|
+
openDshFolder: "Open .dsh folder",
|
|
78
|
+
"openDshFolder.error": "Could not open the .dsh folder",
|
|
79
|
+
"openDshFolder.pathLabel": "Path: ",
|
|
80
|
+
"settingsFrame.drag": "Drag the settings panel",
|
|
81
|
+
"settingsFrame.resize": "Resize the settings panel",
|
|
82
|
+
"modelPicker.trigger": "Open cascading model picker",
|
|
83
|
+
"modelPicker.searchPlaceholder": "Search providers or models",
|
|
84
|
+
"modelPicker.providers": "Providers",
|
|
85
|
+
"modelPicker.models": "Models",
|
|
86
|
+
"modelPicker.emptyProviders": "No providers loaded.",
|
|
87
|
+
"modelPicker.emptySearch": "No matching providers or models.",
|
|
88
|
+
"modelPicker.emptyModels": "No models for this provider.",
|
|
89
|
+
"modelPicker.loading": "Loading…",
|
|
90
|
+
"modelPicker.loadError": "Failed to load models",
|
|
91
|
+
"modelPicker.retry": "Retry",
|
|
92
|
+
"modelPicker.provider": "Provider: {name}",
|
|
93
|
+
"modelPicker.model": "Model: {name}"
|
|
57
94
|
};
|
|
58
95
|
/** 中文文案。 */
|
|
59
96
|
const zh = {
|
|
60
97
|
nav: "ui-fortifier",
|
|
61
98
|
empty: "没有已注册的功能模块。",
|
|
62
|
-
"config.provider-label": "
|
|
99
|
+
"config.provider-label": "输入框 - 提供商",
|
|
100
|
+
"config.open-dsh-folder": "设置右上角 - 打开 .dsh 文件夹",
|
|
101
|
+
"config.settings-frame": "设置面板 - 拖动与缩放",
|
|
102
|
+
"config.model-picker": "输入框 - 级联模型选择器",
|
|
103
|
+
openDshFolder: "打开 .dsh 文件夹",
|
|
104
|
+
"openDshFolder.error": "无法打开 .dsh 文件夹",
|
|
105
|
+
"openDshFolder.pathLabel": "路径:",
|
|
106
|
+
"settingsFrame.drag": "拖动设置面板",
|
|
107
|
+
"settingsFrame.resize": "缩放设置面板",
|
|
108
|
+
"modelPicker.trigger": "打开级联模型选择器",
|
|
109
|
+
"modelPicker.searchPlaceholder": "搜索提供商或模型",
|
|
110
|
+
"modelPicker.providers": "提供商",
|
|
111
|
+
"modelPicker.models": "模型",
|
|
112
|
+
"modelPicker.emptyProviders": "没有已加载的提供商。",
|
|
113
|
+
"modelPicker.emptySearch": "没有匹配的提供商或模型。",
|
|
114
|
+
"modelPicker.emptyModels": "该提供商没有模型。",
|
|
115
|
+
"modelPicker.loading": "加载中…",
|
|
116
|
+
"modelPicker.loadError": "模型加载失败",
|
|
117
|
+
"modelPicker.retry": "重试",
|
|
118
|
+
"modelPicker.provider": "提供商:{name}",
|
|
119
|
+
"modelPicker.model": "模型:{name}"
|
|
63
120
|
};
|
|
64
121
|
//#endregion
|
|
65
122
|
//#region src/client/settings/index.ts
|
|
@@ -68,10 +125,10 @@ window.__ModuleLoader__.load({
|
|
|
68
125
|
/**
|
|
69
126
|
* 注册设置页并绑定功能开关的 settings 作用域。
|
|
70
127
|
* @param ctx - 需已提供 slots/locale/settingsScope 的客户端上下文。
|
|
71
|
-
* @param
|
|
128
|
+
* @param moduleOrder - 功能模块字段的显示顺序(settings 开关 key)。
|
|
72
129
|
* @returns 功能开关的 settings 绑定,供调用方控制子模块注册。
|
|
73
130
|
*/
|
|
74
|
-
function installSettings(ctx,
|
|
131
|
+
function installSettings(ctx, moduleOrder) {
|
|
75
132
|
ctx.effect(() => ctx.locale.register(UI_FORTIFIER, {
|
|
76
133
|
zh,
|
|
77
134
|
en
|
|
@@ -86,7 +143,7 @@ window.__ModuleLoader__.load({
|
|
|
86
143
|
hooks: { toggles: {
|
|
87
144
|
getSnapshot: () => {
|
|
88
145
|
const value = view()?.value ?? {};
|
|
89
|
-
const rows =
|
|
146
|
+
const rows = moduleOrder.map((field) => ({
|
|
90
147
|
field,
|
|
91
148
|
value: value[field] ?? true
|
|
92
149
|
}));
|
|
@@ -123,13 +180,13 @@ window.__ModuleLoader__.load({
|
|
|
123
180
|
}
|
|
124
181
|
//#endregion
|
|
125
182
|
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\provider-label\ProviderLabel.module.css.mjs
|
|
126
|
-
const css = ".LjXNpG_label{text-overflow:ellipsis;white-space:nowrap;max-width:160px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;overflow:hidden}";
|
|
127
|
-
const tagId = "dsh-ui-fortifier/ProviderLabel.module.css";
|
|
128
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
183
|
+
const css$3 = ".LjXNpG_label{text-overflow:ellipsis;white-space:nowrap;max-width:160px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;overflow:hidden}";
|
|
184
|
+
const tagId$3 = "dsh-ui-fortifier/ProviderLabel.module.css";
|
|
185
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
|
|
129
186
|
const tag = document.createElement("style");
|
|
130
187
|
tag.dataset.plugin = "dsh-ui-fortifier";
|
|
131
|
-
tag.dataset.pluginCss = tagId;
|
|
132
|
-
tag.textContent = css;
|
|
188
|
+
tag.dataset.pluginCss = tagId$3;
|
|
189
|
+
tag.textContent = css$3;
|
|
133
190
|
document.head.appendChild(tag);
|
|
134
191
|
}
|
|
135
192
|
var ProviderLabel_module_css_default = { "label": "LjXNpG_label" };
|
|
@@ -160,24 +217,938 @@ window.__ModuleLoader__.load({
|
|
|
160
217
|
}, ProviderLabel));
|
|
161
218
|
}
|
|
162
219
|
//#endregion
|
|
220
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\open-dsh-folder\OpenDshFolderAction.module.css.mjs
|
|
221
|
+
const css$2 = "._8KsKXW_action{align-items:center;gap:8px;min-width:0;display:flex}._8KsKXW_error{max-width:180px;color:var(--dsw-alias-state-error-primary);text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;overflow:hidden}";
|
|
222
|
+
const tagId$2 = "dsh-ui-fortifier/OpenDshFolderAction.module.css";
|
|
223
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
|
|
224
|
+
const tag = document.createElement("style");
|
|
225
|
+
tag.dataset.plugin = "dsh-ui-fortifier";
|
|
226
|
+
tag.dataset.pluginCss = tagId$2;
|
|
227
|
+
tag.textContent = css$2;
|
|
228
|
+
document.head.appendChild(tag);
|
|
229
|
+
}
|
|
230
|
+
var OpenDshFolderAction_module_css_default = {
|
|
231
|
+
"action": "_8KsKXW_action",
|
|
232
|
+
"error": "_8KsKXW_error"
|
|
233
|
+
};
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/client/open-dsh-folder/OpenDshFolderAction.tsx
|
|
236
|
+
/**
|
|
237
|
+
* 渲染「打开 .dsh 文件夹」操作,点击后请求 Host 打开 .dsh 目录。
|
|
238
|
+
* @param props - header owner 参数、本地化文案与注入的打开回调。
|
|
239
|
+
* @returns 操作按钮及其结果提示。
|
|
240
|
+
*/
|
|
241
|
+
function OpenDshFolderAction(props) {
|
|
242
|
+
const { openDshFolder, t } = props;
|
|
243
|
+
const [state, setState] = (0, react.useState)({ kind: "idle" });
|
|
244
|
+
const open = async () => {
|
|
245
|
+
setState({ kind: "opening" });
|
|
246
|
+
const result = await openDshFolder();
|
|
247
|
+
if (!result.ok) {
|
|
248
|
+
setState({ kind: "error" });
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
setState(result.value.opened ? { kind: "idle" } : {
|
|
252
|
+
kind: "path",
|
|
253
|
+
path: result.value.path
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
257
|
+
className: OpenDshFolderAction_module_css_default.action,
|
|
258
|
+
children: [
|
|
259
|
+
state.kind === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
260
|
+
className: OpenDshFolderAction_module_css_default.error,
|
|
261
|
+
role: "alert",
|
|
262
|
+
children: t("openDshFolder.error")
|
|
263
|
+
}) : null,
|
|
264
|
+
state.kind === "path" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
265
|
+
className: OpenDshFolderAction_module_css_default.error,
|
|
266
|
+
role: "alert",
|
|
267
|
+
children: [t("openDshFolder.pathLabel"), state.path]
|
|
268
|
+
}) : null,
|
|
269
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
270
|
+
variant: "outline",
|
|
271
|
+
size: "sm",
|
|
272
|
+
disabled: state.kind === "opening",
|
|
273
|
+
onClick: () => {
|
|
274
|
+
open();
|
|
275
|
+
},
|
|
276
|
+
children: t("openDshFolder")
|
|
277
|
+
})
|
|
278
|
+
]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/client/open-dsh-folder/index.ts
|
|
283
|
+
/** 本模块文案命名空间(与 host 侧一致)。 */
|
|
284
|
+
const NS$1 = "ui-fortifier";
|
|
285
|
+
/**
|
|
286
|
+
* 注册「打开 .dsh 文件夹」到设置页右上角操作区,仅在回环连接下注册。
|
|
287
|
+
* @param ctx - 需已提供 slots/connection 的客户端上下文。
|
|
288
|
+
* @returns 停止注入并卸载注册的 disposer。
|
|
289
|
+
*/
|
|
290
|
+
function installOpenDshFolder(ctx) {
|
|
291
|
+
const connection = ctx.get("connection");
|
|
292
|
+
if (!connection.isLoopback) return () => {};
|
|
293
|
+
const openDshFolder = () => connection.rpc.call("/api", "uiFortifier/openDshFolder", { args: {} });
|
|
294
|
+
return ctx.slots.inject("settings.action", () => ctx.slots.register({
|
|
295
|
+
name: "settings.action",
|
|
296
|
+
id: "open-dsh-folder",
|
|
297
|
+
order: 1,
|
|
298
|
+
locale: NS$1,
|
|
299
|
+
inject: () => ({ openDshFolder })
|
|
300
|
+
}, OpenDshFolderAction));
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\settings-frame\SettingsFrameEnhancer.module.css.mjs
|
|
304
|
+
const css$1 = ".hB-7Ba_hostAnchor{display:none}.hB-7Ba_dragHandle{box-sizing:border-box;width:22px;height:22px;color:var(--dsw-alias-label-secondary);cursor:grab;vertical-align:middle;touch-action:none;background:0 0;border:none;border-radius:6px;flex:none;justify-content:center;align-items:center;margin:0 8px 0 0;padding:0;display:inline-flex}.hB-7Ba_dragHandle:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.hB-7Ba_gripIcon{flex:none;width:10px;height:14px}.hB-7Ba_resizeHandle{cursor:nwse-resize;touch-action:none;background:0 0;border:none;width:16px;height:16px;margin:0;padding:0;position:absolute;bottom:8px;right:8px}.hB-7Ba_resizeHandle:before,.hB-7Ba_resizeHandle:after{content:\"\";pointer-events:none;position:absolute;bottom:0;right:0}.hB-7Ba_resizeHandle:after{border-right:2px solid var(--dsw-alias-label-secondary);border-bottom:2px solid var(--dsw-alias-label-secondary);width:10px;height:10px}.hB-7Ba_resizeHandle:before{border-right:2px solid var(--dsw-alias-label-tertiary);border-bottom:2px solid var(--dsw-alias-label-tertiary);width:6px;height:6px;bottom:4px;right:4px}.hB-7Ba_resizeHandle:hover:after{border-color:var(--dsw-alias-label-primary)}";
|
|
305
|
+
const tagId$1 = "dsh-ui-fortifier/SettingsFrameEnhancer.module.css";
|
|
306
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
307
|
+
const tag = document.createElement("style");
|
|
308
|
+
tag.dataset.plugin = "dsh-ui-fortifier";
|
|
309
|
+
tag.dataset.pluginCss = tagId$1;
|
|
310
|
+
tag.textContent = css$1;
|
|
311
|
+
document.head.appendChild(tag);
|
|
312
|
+
}
|
|
313
|
+
var SettingsFrameEnhancer_module_css_default = {
|
|
314
|
+
"dragHandle": "hB-7Ba_dragHandle",
|
|
315
|
+
"gripIcon": "hB-7Ba_gripIcon",
|
|
316
|
+
"hostAnchor": "hB-7Ba_hostAnchor",
|
|
317
|
+
"resizeHandle": "hB-7Ba_resizeHandle"
|
|
318
|
+
};
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/client/settings-frame/SettingsFrameEnhancer.tsx
|
|
321
|
+
/**
|
|
322
|
+
* settings-frame 功能:让设置面板可拖动位置与缩放大小。
|
|
323
|
+
* 组件注册在 settings.action 槽(作为宿主获得面板生命周期),两个手柄均经 portal
|
|
324
|
+
* 挂载:拖动手柄进左上角标题行,缩放柄进面板右下角;面板改 fixed 定位后仅改内联
|
|
325
|
+
* 几何,关闭时恢复由 CSS 负责的默认居中尺寸。
|
|
326
|
+
*/
|
|
327
|
+
/** 面板最小宽高,防止缩放过小无法操作。 */
|
|
328
|
+
const MIN_WIDTH = 480;
|
|
329
|
+
const MIN_HEIGHT = 320;
|
|
330
|
+
/** localStorage 中面板几何的存储键。 */
|
|
331
|
+
const FRAME_STORAGE_KEY = "dsh.ui-fortifier.settingsFrame";
|
|
332
|
+
/** 读取持久化的面板几何;缺失或损坏返回 undefined。 */
|
|
333
|
+
function readFrameGeometry() {
|
|
334
|
+
const raw = localStorage.getItem(FRAME_STORAGE_KEY);
|
|
335
|
+
if (raw === null) return void 0;
|
|
336
|
+
try {
|
|
337
|
+
const parsed = JSON.parse(raw);
|
|
338
|
+
if (typeof parsed.left === "number" && Number.isFinite(parsed.left) && typeof parsed.top === "number" && Number.isFinite(parsed.top) && typeof parsed.width === "number" && Number.isFinite(parsed.width) && typeof parsed.height === "number" && Number.isFinite(parsed.height)) return parsed;
|
|
339
|
+
return;
|
|
340
|
+
} catch {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** 写回持久化的面板几何。 */
|
|
345
|
+
function writeFrameGeometry(geometry) {
|
|
346
|
+
localStorage.setItem(FRAME_STORAGE_KEY, JSON.stringify(geometry));
|
|
347
|
+
}
|
|
348
|
+
/** 读取面板当前几何并持久化(拖动/缩放结束后调用)。 */
|
|
349
|
+
function persistGeometry(panel) {
|
|
350
|
+
writeFrameGeometry({
|
|
351
|
+
left: panel.offsetLeft,
|
|
352
|
+
top: panel.offsetTop,
|
|
353
|
+
width: panel.offsetWidth,
|
|
354
|
+
height: panel.offsetHeight
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/** 将面板几何钳制在视口内(左上角不小于 0,右下角不越出视口)。 */
|
|
358
|
+
function constrainPanel(panel) {
|
|
359
|
+
const vw = window.innerWidth;
|
|
360
|
+
const vh = window.innerHeight;
|
|
361
|
+
panel.style.left = `${clamp(panel.offsetLeft, 0, Math.max(0, vw - panel.offsetWidth))}px`;
|
|
362
|
+
panel.style.top = `${clamp(panel.offsetTop, 0, Math.max(0, vh - panel.offsetHeight))}px`;
|
|
363
|
+
}
|
|
364
|
+
/** 窗口尺寸变化时把面板收进视口:先收窄超出视口的宽高(不低于最小尺寸,窗口小于最小尺寸时随窗口),再钳制位置。 */
|
|
365
|
+
function constrainToViewport(panel) {
|
|
366
|
+
const vw = window.innerWidth;
|
|
367
|
+
const vh = window.innerHeight;
|
|
368
|
+
const width = clamp(panel.offsetWidth, Math.min(MIN_WIDTH, vw), vw);
|
|
369
|
+
const height = clamp(panel.offsetHeight, Math.min(MIN_HEIGHT, vh), vh);
|
|
370
|
+
panel.style.width = `${width}px`;
|
|
371
|
+
panel.style.height = `${height}px`;
|
|
372
|
+
panel.style.left = `${clamp(panel.offsetLeft, 0, Math.max(0, vw - width))}px`;
|
|
373
|
+
panel.style.top = `${clamp(panel.offsetTop, 0, Math.max(0, vh - height))}px`;
|
|
374
|
+
}
|
|
375
|
+
/** 将一个数值限制在闭合区间。 */
|
|
376
|
+
function clamp(value, min, max) {
|
|
377
|
+
return Math.min(Math.max(value, min), max);
|
|
378
|
+
}
|
|
379
|
+
/** 六点拖动把手图形(2 列 x 3 行规整点阵)。 */
|
|
380
|
+
function GripDots() {
|
|
381
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
382
|
+
className: SettingsFrameEnhancer_module_css_default.gripIcon,
|
|
383
|
+
viewBox: "0 0 10 14",
|
|
384
|
+
fill: "currentColor",
|
|
385
|
+
"aria-hidden": "true",
|
|
386
|
+
children: [
|
|
387
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
388
|
+
cx: "2.5",
|
|
389
|
+
cy: "2.5",
|
|
390
|
+
r: "1.3"
|
|
391
|
+
}),
|
|
392
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
393
|
+
cx: "7.5",
|
|
394
|
+
cy: "2.5",
|
|
395
|
+
r: "1.3"
|
|
396
|
+
}),
|
|
397
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
398
|
+
cx: "2.5",
|
|
399
|
+
cy: "7",
|
|
400
|
+
r: "1.3"
|
|
401
|
+
}),
|
|
402
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
403
|
+
cx: "7.5",
|
|
404
|
+
cy: "7",
|
|
405
|
+
r: "1.3"
|
|
406
|
+
}),
|
|
407
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
408
|
+
cx: "2.5",
|
|
409
|
+
cy: "11.5",
|
|
410
|
+
r: "1.3"
|
|
411
|
+
}),
|
|
412
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
413
|
+
cx: "7.5",
|
|
414
|
+
cy: "11.5",
|
|
415
|
+
r: "1.3"
|
|
416
|
+
})
|
|
417
|
+
]
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* 渲染「拖动位置/大小」手柄。
|
|
422
|
+
* @param props - header owner 参数与本地化文案。
|
|
423
|
+
* @returns 挂入标题行的拖动手柄与挂入面板右下角的缩放柄。
|
|
424
|
+
*/
|
|
425
|
+
function SettingsFrameEnhancer(props) {
|
|
426
|
+
const { t } = props;
|
|
427
|
+
const anchorRef = (0, react.useRef)(null);
|
|
428
|
+
const dragHandleRef = (0, react.useRef)(null);
|
|
429
|
+
const resizeHandleRef = (0, react.useRef)(null);
|
|
430
|
+
const [panel, setPanel] = (0, react.useState)(null);
|
|
431
|
+
const [headerRow, setHeaderRow] = (0, react.useState)(null);
|
|
432
|
+
(0, react.useEffect)(() => {
|
|
433
|
+
const anchor = anchorRef.current;
|
|
434
|
+
if (anchor === null) return;
|
|
435
|
+
const dialog = anchor.closest("[role=\"dialog\"]");
|
|
436
|
+
if (!(dialog instanceof HTMLElement)) return;
|
|
437
|
+
const rect = dialog.getBoundingClientRect();
|
|
438
|
+
const saved = readFrameGeometry();
|
|
439
|
+
dialog.style.position = "fixed";
|
|
440
|
+
dialog.style.left = `${saved === void 0 ? rect.left : saved.left}px`;
|
|
441
|
+
dialog.style.top = `${saved === void 0 ? rect.top : saved.top}px`;
|
|
442
|
+
dialog.style.width = `${saved === void 0 ? rect.width : saved.width}px`;
|
|
443
|
+
dialog.style.height = `${saved === void 0 ? rect.height : saved.height}px`;
|
|
444
|
+
dialog.style.margin = "0";
|
|
445
|
+
dialog.style.maxWidth = "none";
|
|
446
|
+
constrainToViewport(dialog);
|
|
447
|
+
if (window.innerWidth >= MIN_WIDTH && window.innerHeight >= MIN_HEIGHT) persistGeometry(dialog);
|
|
448
|
+
setPanel(dialog);
|
|
449
|
+
const row = dialog.querySelector("nav")?.firstElementChild;
|
|
450
|
+
setHeaderRow(row instanceof HTMLElement ? row : null);
|
|
451
|
+
let windowResizeFrame = null;
|
|
452
|
+
const onWindowResize = () => {
|
|
453
|
+
if (windowResizeFrame !== null) return;
|
|
454
|
+
windowResizeFrame = requestAnimationFrame(() => {
|
|
455
|
+
windowResizeFrame = null;
|
|
456
|
+
constrainToViewport(dialog);
|
|
457
|
+
if (window.innerWidth >= MIN_WIDTH && window.innerHeight >= MIN_HEIGHT) persistGeometry(dialog);
|
|
458
|
+
});
|
|
459
|
+
};
|
|
460
|
+
window.addEventListener("resize", onWindowResize);
|
|
461
|
+
return () => {
|
|
462
|
+
if (windowResizeFrame !== null) {
|
|
463
|
+
cancelAnimationFrame(windowResizeFrame);
|
|
464
|
+
windowResizeFrame = null;
|
|
465
|
+
}
|
|
466
|
+
window.removeEventListener("resize", onWindowResize);
|
|
467
|
+
dialog.style.position = "";
|
|
468
|
+
dialog.style.left = "";
|
|
469
|
+
dialog.style.top = "";
|
|
470
|
+
dialog.style.width = "";
|
|
471
|
+
dialog.style.height = "";
|
|
472
|
+
dialog.style.margin = "";
|
|
473
|
+
dialog.style.maxWidth = "";
|
|
474
|
+
setPanel(null);
|
|
475
|
+
setHeaderRow(null);
|
|
476
|
+
};
|
|
477
|
+
}, []);
|
|
478
|
+
(0, react.useEffect)(() => {
|
|
479
|
+
const handle = dragHandleRef.current;
|
|
480
|
+
if (handle === null || headerRow === null) return;
|
|
481
|
+
if (headerRow.firstChild === handle) return;
|
|
482
|
+
headerRow.insertBefore(handle, headerRow.firstChild);
|
|
483
|
+
}, [headerRow]);
|
|
484
|
+
const dragState = (0, react.useRef)(null);
|
|
485
|
+
const latest = (0, react.useRef)({
|
|
486
|
+
x: 0,
|
|
487
|
+
y: 0
|
|
488
|
+
});
|
|
489
|
+
const frame = (0, react.useRef)(null);
|
|
490
|
+
const onDragPointerDown = (event) => {
|
|
491
|
+
if (panel === null) return;
|
|
492
|
+
event.preventDefault();
|
|
493
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
494
|
+
dragState.current = {
|
|
495
|
+
offsetX: event.clientX - panel.offsetLeft,
|
|
496
|
+
offsetY: event.clientY - panel.offsetTop
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
const onDragPointerMove = (event) => {
|
|
500
|
+
if (dragState.current === null) return;
|
|
501
|
+
latest.current = {
|
|
502
|
+
x: event.clientX,
|
|
503
|
+
y: event.clientY
|
|
504
|
+
};
|
|
505
|
+
frame.current ??= requestAnimationFrame(() => {
|
|
506
|
+
frame.current = null;
|
|
507
|
+
if (panel === null || dragState.current === null) return;
|
|
508
|
+
panel.style.left = `${latest.current.x - dragState.current.offsetX}px`;
|
|
509
|
+
panel.style.top = `${latest.current.y - dragState.current.offsetY}px`;
|
|
510
|
+
constrainPanel(panel);
|
|
511
|
+
});
|
|
512
|
+
};
|
|
513
|
+
const onDragPointerUp = (event) => {
|
|
514
|
+
if (dragState.current === null) return;
|
|
515
|
+
if (frame.current !== null) {
|
|
516
|
+
cancelAnimationFrame(frame.current);
|
|
517
|
+
frame.current = null;
|
|
518
|
+
}
|
|
519
|
+
dragState.current = null;
|
|
520
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
521
|
+
if (panel === null) return;
|
|
522
|
+
constrainPanel(panel);
|
|
523
|
+
persistGeometry(panel);
|
|
524
|
+
};
|
|
525
|
+
const resizeOrigin = (0, react.useRef)(null);
|
|
526
|
+
const resizeBase = (0, react.useRef)(null);
|
|
527
|
+
const resizeMax = (0, react.useRef)(null);
|
|
528
|
+
const resizeFrame = (0, react.useRef)(null);
|
|
529
|
+
const onResizePointerDown = (event) => {
|
|
530
|
+
if (panel === null) return;
|
|
531
|
+
event.preventDefault();
|
|
532
|
+
event.stopPropagation();
|
|
533
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
534
|
+
resizeOrigin.current = {
|
|
535
|
+
x: event.clientX,
|
|
536
|
+
y: event.clientY
|
|
537
|
+
};
|
|
538
|
+
resizeBase.current = {
|
|
539
|
+
width: panel.offsetWidth,
|
|
540
|
+
height: panel.offsetHeight
|
|
541
|
+
};
|
|
542
|
+
resizeMax.current = {
|
|
543
|
+
width: window.innerWidth - panel.offsetLeft,
|
|
544
|
+
height: window.innerHeight - panel.offsetTop
|
|
545
|
+
};
|
|
546
|
+
};
|
|
547
|
+
const onResizePointerMove = (event) => {
|
|
548
|
+
if (resizeOrigin.current === null || resizeBase.current === null || resizeMax.current === null) return;
|
|
549
|
+
latest.current = {
|
|
550
|
+
x: event.clientX,
|
|
551
|
+
y: event.clientY
|
|
552
|
+
};
|
|
553
|
+
resizeFrame.current ??= requestAnimationFrame(() => {
|
|
554
|
+
resizeFrame.current = null;
|
|
555
|
+
if (panel === null || resizeOrigin.current === null || resizeBase.current === null || resizeMax.current === null) return;
|
|
556
|
+
const width = clamp(resizeBase.current.width + (latest.current.x - resizeOrigin.current.x), MIN_WIDTH, Math.max(MIN_WIDTH, resizeMax.current.width));
|
|
557
|
+
const height = clamp(resizeBase.current.height + (latest.current.y - resizeOrigin.current.y), MIN_HEIGHT, Math.max(MIN_HEIGHT, resizeMax.current.height));
|
|
558
|
+
panel.style.width = `${width}px`;
|
|
559
|
+
panel.style.height = `${height}px`;
|
|
560
|
+
constrainPanel(panel);
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
const onResizePointerUp = (event) => {
|
|
564
|
+
if (resizeOrigin.current === null) return;
|
|
565
|
+
if (resizeFrame.current !== null) {
|
|
566
|
+
cancelAnimationFrame(resizeFrame.current);
|
|
567
|
+
resizeFrame.current = null;
|
|
568
|
+
}
|
|
569
|
+
resizeOrigin.current = null;
|
|
570
|
+
resizeBase.current = null;
|
|
571
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
572
|
+
if (panel !== null) persistGeometry(panel);
|
|
573
|
+
};
|
|
574
|
+
const dragHandle = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
575
|
+
ref: dragHandleRef,
|
|
576
|
+
type: "button",
|
|
577
|
+
className: SettingsFrameEnhancer_module_css_default.dragHandle,
|
|
578
|
+
title: t("settingsFrame.drag"),
|
|
579
|
+
"aria-label": t("settingsFrame.drag"),
|
|
580
|
+
onPointerDown: onDragPointerDown,
|
|
581
|
+
onPointerMove: onDragPointerMove,
|
|
582
|
+
onPointerUp: onDragPointerUp,
|
|
583
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GripDots, {})
|
|
584
|
+
});
|
|
585
|
+
const resizeHandle = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
586
|
+
ref: resizeHandleRef,
|
|
587
|
+
type: "button",
|
|
588
|
+
className: SettingsFrameEnhancer_module_css_default.resizeHandle,
|
|
589
|
+
title: t("settingsFrame.resize"),
|
|
590
|
+
"aria-label": t("settingsFrame.resize"),
|
|
591
|
+
onPointerDown: onResizePointerDown,
|
|
592
|
+
onPointerMove: onResizePointerMove,
|
|
593
|
+
onPointerUp: onResizePointerUp
|
|
594
|
+
});
|
|
595
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
596
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
597
|
+
ref: anchorRef,
|
|
598
|
+
className: SettingsFrameEnhancer_module_css_default.hostAnchor
|
|
599
|
+
}),
|
|
600
|
+
headerRow !== null && (0, react_dom.createPortal)(dragHandle, headerRow),
|
|
601
|
+
panel !== null && (0, react_dom.createPortal)(resizeHandle, panel)
|
|
602
|
+
] });
|
|
603
|
+
}
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/client/settings-frame/index.ts
|
|
606
|
+
/** 本模块文案命名空间(与 host 侧一致)。 */
|
|
607
|
+
const NS = "ui-fortifier";
|
|
608
|
+
/**
|
|
609
|
+
* 注册「拖动位置/大小」增强到设置页右上角操作区。
|
|
610
|
+
* @param ctx - 需已提供 slots 的客户端上下文。
|
|
611
|
+
* @returns 停止注入并卸载注册的 disposer。
|
|
612
|
+
*/
|
|
613
|
+
function installSettingsFrame(ctx) {
|
|
614
|
+
const injected = () => ({});
|
|
615
|
+
return ctx.slots.inject("settings.action", () => ctx.slots.register({
|
|
616
|
+
name: "settings.action",
|
|
617
|
+
id: "settings-frame-enhancer",
|
|
618
|
+
order: 2,
|
|
619
|
+
locale: NS,
|
|
620
|
+
inject: injected
|
|
621
|
+
}, SettingsFrameEnhancer));
|
|
622
|
+
}
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region \0dsh-css:D:\data\code\deepseek-harness\packages\my\dsh-ui-fortifier\src\client\model-picker\ModelPicker.module.css.mjs
|
|
625
|
+
const css = ".AGI7WG_root{flex:none;display:inline-flex}.AGI7WG_trigger{box-sizing:border-box;width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:14px;justify-content:center;align-items:center;padding:0;display:inline-flex}.AGI7WG_trigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.AGI7WG_trigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}.AGI7WG_triggerIcon{flex:none;width:14px;height:14px}.AGI7WG_menu{z-index:1100;box-sizing:border-box;background:var(--dsw-specific-menu);--dsw-elevation-stroke-color:var(--dsw-alias-border-l1);min-width:min(420px,100vw - 32px);max-width:min(520px,100vw - 32px);height:min(420px,100vh - 96px);box-shadow:var(--dsw-elevation-prominent);color:var(--dsw-alias-label-primary);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:16px;flex-direction:column;padding:6px;display:flex;position:fixed;overflow:hidden}.AGI7WG_search{box-sizing:border-box;background:var(--dsw-alias-bg-layer-2);border:.5px solid var(--dsw-alias-border-l3);border-radius:10px;flex:none;align-items:center;gap:6px;height:28px;margin:0 2px 6px;padding:0 8px;display:flex}.AGI7WG_search:focus-within{border-color:var(--dsw-alias-brand-primary)}.AGI7WG_searchIcon{color:var(--dsw-alias-label-tertiary);flex:none;display:inline-flex}.AGI7WG_searchIcon svg{width:13px;height:13px}.AGI7WG_searchInput{min-width:0;color:var(--dsw-alias-label-primary);font:inherit;background:0 0;border:none;outline:none;flex:1;padding:0;font-size:13px;line-height:20px}.AGI7WG_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.AGI7WG_columns{flex:1;min-height:0;display:flex;overflow:hidden}.AGI7WG_providers{flex:none;width:160px;padding-right:4px;overflow-y:auto}.AGI7WG_models{flex:1;min-width:0;padding-left:4px;overflow-y:auto}.AGI7WG_columnTitle{color:var(--dsw-alias-label-tertiary);padding:5px 8px 3px;font-size:12px;font-weight:500;line-height:18px}.AGI7WG_cell{box-sizing:border-box;width:100%;min-height:34px;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:6px 8px;display:flex}.AGI7WG_cell:hover:not(:disabled),.AGI7WG_cell:focus-visible{background:var(--dsw-alias-interactive-bg-hover)}.AGI7WG_cellActive{background:var(--dsw-alias-interactive-bg-active);color:var(--dsw-alias-label-primary)}.AGI7WG_cellSelected{color:var(--dsw-alias-label-primary);font-weight:500}.AGI7WG_cell:disabled{color:var(--dsw-alias-label-dimmed);cursor:default}.AGI7WG_modelName{overflow-wrap:anywhere;flex:1;min-width:0}.AGI7WG_check{color:var(--dsw-alias-brand-primary);flex:none}.AGI7WG_status{color:var(--dsw-alias-label-tertiary);padding:10px;font-size:13px;line-height:20px}.AGI7WG_error{background:var(--dsw-alias-interactive-bg-hover-danger);color:var(--dsw-alias-state-error-primary);border-radius:8px;justify-content:space-between;align-items:flex-start;gap:8px;margin-bottom:4px;padding:7px 8px;font-size:12px;line-height:18px;display:flex}.AGI7WG_retry{color:inherit;font:inherit;cursor:pointer;background:0 0;border:none;flex:none;padding:0;font-weight:600}";
|
|
626
|
+
const tagId = "dsh-ui-fortifier/ModelPicker.module.css";
|
|
627
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
628
|
+
const tag = document.createElement("style");
|
|
629
|
+
tag.dataset.plugin = "dsh-ui-fortifier";
|
|
630
|
+
tag.dataset.pluginCss = tagId;
|
|
631
|
+
tag.textContent = css;
|
|
632
|
+
document.head.appendChild(tag);
|
|
633
|
+
}
|
|
634
|
+
var ModelPicker_module_css_default = {
|
|
635
|
+
"cell": "AGI7WG_cell",
|
|
636
|
+
"cellActive": "AGI7WG_cellActive",
|
|
637
|
+
"cellSelected": "AGI7WG_cellSelected",
|
|
638
|
+
"check": "AGI7WG_check",
|
|
639
|
+
"columnTitle": "AGI7WG_columnTitle",
|
|
640
|
+
"columns": "AGI7WG_columns",
|
|
641
|
+
"error": "AGI7WG_error",
|
|
642
|
+
"menu": "AGI7WG_menu",
|
|
643
|
+
"modelName": "AGI7WG_modelName",
|
|
644
|
+
"models": "AGI7WG_models",
|
|
645
|
+
"providers": "AGI7WG_providers",
|
|
646
|
+
"retry": "AGI7WG_retry",
|
|
647
|
+
"root": "AGI7WG_root",
|
|
648
|
+
"search": "AGI7WG_search",
|
|
649
|
+
"searchIcon": "AGI7WG_searchIcon",
|
|
650
|
+
"searchInput": "AGI7WG_searchInput",
|
|
651
|
+
"status": "AGI7WG_status",
|
|
652
|
+
"trigger": "AGI7WG_trigger",
|
|
653
|
+
"triggerIcon": "AGI7WG_triggerIcon"
|
|
654
|
+
};
|
|
655
|
+
//#endregion
|
|
656
|
+
//#region src/client/model-picker/ModelPicker.tsx
|
|
657
|
+
/**
|
|
658
|
+
* model-picker 功能:输入框右下角(following provider-label)的级联模型选择器。
|
|
659
|
+
* 左列提供商、右列该提供商的模型;顶部搜索框按名称过滤提供商与模型。
|
|
660
|
+
* 选择写回官方共享的 modelDirectories,因此与官方模型选择按钮/斜杠命令互通。
|
|
661
|
+
*/
|
|
662
|
+
/** 名称是否命中搜索关键字。 */
|
|
663
|
+
function matchesQuery(name, query) {
|
|
664
|
+
return query === "" || name.toLowerCase().includes(query);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* 在滚动容器内把目标滚进视野。只调整容器自身的 scrollTop,不联动页面滚动,
|
|
668
|
+
* 因此面板浮在 body 上时不会带动背景页面。
|
|
669
|
+
* @param container - 滚动容器,缺失时不处理。
|
|
670
|
+
* @param target - 目标元素,缺失(如已被搜索过滤掉)时不处理。
|
|
671
|
+
*/
|
|
672
|
+
function scrollIntoViewWithin(container, target) {
|
|
673
|
+
if (container === null || target === null) return;
|
|
674
|
+
const containerRect = container.getBoundingClientRect();
|
|
675
|
+
const targetRect = target.getBoundingClientRect();
|
|
676
|
+
if (targetRect.top < containerRect.top) {
|
|
677
|
+
container.scrollTop -= containerRect.top - targetRect.top;
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (targetRect.bottom > containerRect.bottom) container.scrollTop += targetRect.bottom - containerRect.bottom;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* 未定位态:隐藏但按原点参与布局,使首帧 offsetWidth/offsetHeight 为真实尺寸,
|
|
684
|
+
* 定位不存在时也不会闪现在原点(与官方 ModelSelect 同法)。
|
|
685
|
+
*/
|
|
686
|
+
const MEASURE_STYLE = {
|
|
687
|
+
visibility: "hidden",
|
|
688
|
+
left: 0,
|
|
689
|
+
top: 0
|
|
690
|
+
};
|
|
691
|
+
/**
|
|
692
|
+
* 面板坐标:右边缘对齐触发按钮右边缘,仅当这样越出视口时才平移回边界内。
|
|
693
|
+
* 官方 useAnchoredPosition 固定以触发器左边缘为起点且无对齐选项,故本地实现。
|
|
694
|
+
* @param open - 面板是否展开;关闭时清空坐标。
|
|
695
|
+
* @param anchorRef - 触发按钮。
|
|
696
|
+
* @param panelRef - 面板,用于量取自身尺寸做视口钳位。
|
|
697
|
+
* @param gap - 面板下边缘与触发器上边缘的间距。
|
|
698
|
+
* @param margin - 面板与视口各边的最小距离。
|
|
699
|
+
* @returns 面板的 fixed 坐标;首次量取前为 null。
|
|
700
|
+
*/
|
|
701
|
+
function useRightAlignedMenuPosition(open, anchorRef, panelRef, gap, margin) {
|
|
702
|
+
const [position, setPosition] = (0, react.useState)(null);
|
|
703
|
+
(0, react.useLayoutEffect)(() => {
|
|
704
|
+
if (!open) {
|
|
705
|
+
setPosition(null);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const place = () => {
|
|
709
|
+
const rect = anchorRef.current?.getBoundingClientRect();
|
|
710
|
+
if (rect === void 0) return;
|
|
711
|
+
const panel = panelRef.current;
|
|
712
|
+
const width = panel?.offsetWidth ?? 0;
|
|
713
|
+
const height = panel?.offsetHeight ?? 0;
|
|
714
|
+
let left = rect.right - width;
|
|
715
|
+
let top = rect.top - gap - height;
|
|
716
|
+
if (width > 0) left = Math.min(Math.max(left, margin), window.innerWidth - width - margin);
|
|
717
|
+
if (height > 0) top = Math.min(Math.max(top, margin), window.innerHeight - height - margin);
|
|
718
|
+
setPosition({
|
|
719
|
+
left,
|
|
720
|
+
top
|
|
721
|
+
});
|
|
722
|
+
};
|
|
723
|
+
place();
|
|
724
|
+
window.addEventListener("scroll", place, true);
|
|
725
|
+
window.addEventListener("resize", place);
|
|
726
|
+
const panel = panelRef.current;
|
|
727
|
+
let observer = null;
|
|
728
|
+
if (typeof ResizeObserver !== "undefined" && panel !== null) {
|
|
729
|
+
observer = new ResizeObserver(place);
|
|
730
|
+
observer.observe(panel);
|
|
731
|
+
}
|
|
732
|
+
return () => {
|
|
733
|
+
observer?.disconnect();
|
|
734
|
+
window.removeEventListener("scroll", place, true);
|
|
735
|
+
window.removeEventListener("resize", place);
|
|
736
|
+
};
|
|
737
|
+
}, [
|
|
738
|
+
open,
|
|
739
|
+
anchorRef,
|
|
740
|
+
panelRef,
|
|
741
|
+
gap,
|
|
742
|
+
margin
|
|
743
|
+
]);
|
|
744
|
+
return position;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* 渲染级联模型选择器(左列提供商、右列模型)。
|
|
748
|
+
* @param props - owner 参数、文案与注入面。
|
|
749
|
+
* @returns 图标触发器按钮与展开时的两列面板。
|
|
750
|
+
*/
|
|
751
|
+
function ModelPicker(props) {
|
|
752
|
+
const t = props.t;
|
|
753
|
+
const state = props.useDirectory((value) => value);
|
|
754
|
+
const [picker, setPicker] = (0, react.useState)({
|
|
755
|
+
open: false,
|
|
756
|
+
provider: null,
|
|
757
|
+
query: "",
|
|
758
|
+
error: null
|
|
759
|
+
});
|
|
760
|
+
const [loading, setLoading] = (0, react.useState)(false);
|
|
761
|
+
const rootRef = (0, react.useRef)(null);
|
|
762
|
+
const triggerRef = (0, react.useRef)(null);
|
|
763
|
+
const menuRef = (0, react.useRef)(null);
|
|
764
|
+
const providersRef = (0, react.useRef)(null);
|
|
765
|
+
const modelsRef = (0, react.useRef)(null);
|
|
766
|
+
const currentProviderRef = (0, react.useRef)(null);
|
|
767
|
+
const currentModelRef = (0, react.useRef)(null);
|
|
768
|
+
const searchRef = (0, react.useRef)(null);
|
|
769
|
+
const positionedRef = (0, react.useRef)(false);
|
|
770
|
+
const pendingCurrentScrollRef = (0, react.useRef)(false);
|
|
771
|
+
const position = useRightAlignedMenuPosition(picker.open, triggerRef, menuRef, 8, 12);
|
|
772
|
+
const groups = state.groups;
|
|
773
|
+
const current = state.current;
|
|
774
|
+
const query = picker.query.trim().toLowerCase();
|
|
775
|
+
const hasModelHit = groups.some((group) => group.models.some((model) => matchesQuery(model.name, query)));
|
|
776
|
+
const visibleGroups = (0, react.useMemo)(() => {
|
|
777
|
+
if (query === "") return groups;
|
|
778
|
+
if (hasModelHit) return groups.filter((group) => group.models.some((model) => matchesQuery(model.name, query)));
|
|
779
|
+
return groups.filter((group) => matchesQuery(group.name, query));
|
|
780
|
+
}, [
|
|
781
|
+
groups,
|
|
782
|
+
query,
|
|
783
|
+
hasModelHit
|
|
784
|
+
]);
|
|
785
|
+
const activeGroup = visibleGroups.find((group) => group.id === picker.provider) ?? visibleGroups[0];
|
|
786
|
+
const activeProvider = activeGroup?.id ?? null;
|
|
787
|
+
const activeModels = (0, react.useMemo)(() => {
|
|
788
|
+
if (activeGroup === void 0) return [];
|
|
789
|
+
if (query === "") return activeGroup.models;
|
|
790
|
+
if (hasModelHit) return activeGroup.models.filter((model) => matchesQuery(model.name, query));
|
|
791
|
+
return activeGroup.models;
|
|
792
|
+
}, [
|
|
793
|
+
activeGroup,
|
|
794
|
+
query,
|
|
795
|
+
hasModelHit
|
|
796
|
+
]);
|
|
797
|
+
(0, react.useLayoutEffect)(() => {
|
|
798
|
+
if (!picker.open) {
|
|
799
|
+
positionedRef.current = false;
|
|
800
|
+
pendingCurrentScrollRef.current = false;
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (pendingCurrentScrollRef.current) {
|
|
804
|
+
if (currentModelRef.current === null) return;
|
|
805
|
+
scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
|
|
806
|
+
pendingCurrentScrollRef.current = false;
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (positionedRef.current) return;
|
|
810
|
+
if (currentProviderRef.current === null && currentModelRef.current === null) return;
|
|
811
|
+
scrollIntoViewWithin(providersRef.current, currentProviderRef.current);
|
|
812
|
+
scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
|
|
813
|
+
positionedRef.current = true;
|
|
814
|
+
}, [
|
|
815
|
+
picker.open,
|
|
816
|
+
activeProvider,
|
|
817
|
+
current?.provider,
|
|
818
|
+
current?.model,
|
|
819
|
+
visibleGroups,
|
|
820
|
+
activeModels
|
|
821
|
+
]);
|
|
822
|
+
(0, react.useEffect)(() => {
|
|
823
|
+
if (!picker.open) return;
|
|
824
|
+
const closeOutside = (event) => {
|
|
825
|
+
const target = event.target;
|
|
826
|
+
if (rootRef.current?.contains(target) === true) return;
|
|
827
|
+
if (menuRef.current?.contains(target) === true) return;
|
|
828
|
+
setPicker((p) => ({
|
|
829
|
+
...p,
|
|
830
|
+
open: false
|
|
831
|
+
}));
|
|
832
|
+
};
|
|
833
|
+
document.addEventListener("mousedown", closeOutside);
|
|
834
|
+
return () => {
|
|
835
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
836
|
+
};
|
|
837
|
+
}, [picker.open]);
|
|
838
|
+
const open = () => {
|
|
839
|
+
setPicker((p) => ({
|
|
840
|
+
...p,
|
|
841
|
+
open: true,
|
|
842
|
+
provider: p.provider ?? current?.provider ?? null
|
|
843
|
+
}));
|
|
844
|
+
props.load();
|
|
845
|
+
setLoading(state.status === "loading" || state.status === "idle");
|
|
846
|
+
queueMicrotask(() => {
|
|
847
|
+
searchRef.current?.focus();
|
|
848
|
+
});
|
|
849
|
+
};
|
|
850
|
+
const close = (restoreFocus = false) => {
|
|
851
|
+
setPicker((p) => ({
|
|
852
|
+
...p,
|
|
853
|
+
open: false
|
|
854
|
+
}));
|
|
855
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
856
|
+
triggerRef.current?.focus();
|
|
857
|
+
});
|
|
858
|
+
};
|
|
859
|
+
const chooseProvider = (provider) => {
|
|
860
|
+
setPicker((p) => ({
|
|
861
|
+
...p,
|
|
862
|
+
provider
|
|
863
|
+
}));
|
|
864
|
+
if (provider === current?.provider) {
|
|
865
|
+
if (provider === activeProvider) {
|
|
866
|
+
scrollIntoViewWithin(modelsRef.current, currentModelRef.current);
|
|
867
|
+
pendingCurrentScrollRef.current = false;
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
pendingCurrentScrollRef.current = true;
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
pendingCurrentScrollRef.current = false;
|
|
874
|
+
if (modelsRef.current !== null) modelsRef.current.scrollTop = 0;
|
|
875
|
+
};
|
|
876
|
+
const chooseModel = (modelId) => {
|
|
877
|
+
if (activeProvider === null) return;
|
|
878
|
+
const model = activeGroup?.models.find((m) => m.id === modelId);
|
|
879
|
+
if (model === void 0) return;
|
|
880
|
+
const selection = {
|
|
881
|
+
provider: activeProvider,
|
|
882
|
+
model: modelId,
|
|
883
|
+
...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
|
|
884
|
+
};
|
|
885
|
+
if (current?.provider === activeProvider && current.model === modelId) {
|
|
886
|
+
close(true);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
props.select(selection).then((accepted) => {
|
|
890
|
+
if (accepted) close(true);
|
|
891
|
+
else setPicker((p) => ({
|
|
892
|
+
...p,
|
|
893
|
+
error: t("modelPicker.loadError")
|
|
894
|
+
}));
|
|
895
|
+
});
|
|
896
|
+
};
|
|
897
|
+
const hasError = state.error !== null || picker.error !== null;
|
|
898
|
+
const shownError = state.error ?? picker.error;
|
|
899
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
900
|
+
ref: rootRef,
|
|
901
|
+
className: ModelPicker_module_css_default.root,
|
|
902
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
903
|
+
ref: triggerRef,
|
|
904
|
+
type: "button",
|
|
905
|
+
className: ModelPicker_module_css_default.trigger,
|
|
906
|
+
"aria-label": t("modelPicker.trigger"),
|
|
907
|
+
"aria-haspopup": "dialog",
|
|
908
|
+
"aria-expanded": picker.open,
|
|
909
|
+
title: t("modelPicker.trigger"),
|
|
910
|
+
onClick: () => {
|
|
911
|
+
if (picker.open) close();
|
|
912
|
+
else open();
|
|
913
|
+
},
|
|
914
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
915
|
+
viewBox: "0 0 16 16",
|
|
916
|
+
className: ModelPicker_module_css_default.triggerIcon,
|
|
917
|
+
"aria-hidden": "true",
|
|
918
|
+
fill: "none",
|
|
919
|
+
children: [
|
|
920
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
921
|
+
x: "2",
|
|
922
|
+
y: "2",
|
|
923
|
+
width: "5",
|
|
924
|
+
height: "5",
|
|
925
|
+
rx: "1.5",
|
|
926
|
+
stroke: "currentColor",
|
|
927
|
+
strokeWidth: "1.5"
|
|
928
|
+
}),
|
|
929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
930
|
+
x: "9",
|
|
931
|
+
y: "2",
|
|
932
|
+
width: "5",
|
|
933
|
+
height: "5",
|
|
934
|
+
rx: "1.5",
|
|
935
|
+
stroke: "currentColor",
|
|
936
|
+
strokeWidth: "1.5"
|
|
937
|
+
}),
|
|
938
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
939
|
+
x: "2",
|
|
940
|
+
y: "9",
|
|
941
|
+
width: "5",
|
|
942
|
+
height: "5",
|
|
943
|
+
rx: "1.5",
|
|
944
|
+
stroke: "currentColor",
|
|
945
|
+
strokeWidth: "1.5"
|
|
946
|
+
}),
|
|
947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
948
|
+
x: "9",
|
|
949
|
+
y: "9",
|
|
950
|
+
width: "5",
|
|
951
|
+
height: "5",
|
|
952
|
+
rx: "1.5",
|
|
953
|
+
stroke: "currentColor",
|
|
954
|
+
strokeWidth: "1.5"
|
|
955
|
+
})
|
|
956
|
+
]
|
|
957
|
+
})
|
|
958
|
+
}), picker.open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
959
|
+
ref: menuRef,
|
|
960
|
+
className: ModelPicker_module_css_default.menu,
|
|
961
|
+
role: "dialog",
|
|
962
|
+
"aria-label": t("modelPicker.trigger"),
|
|
963
|
+
style: position ?? MEASURE_STYLE,
|
|
964
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
965
|
+
className: ModelPicker_module_css_default.search,
|
|
966
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
967
|
+
className: ModelPicker_module_css_default.searchIcon,
|
|
968
|
+
"aria-hidden": "true",
|
|
969
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
970
|
+
viewBox: "0 0 16 16",
|
|
971
|
+
fill: "none",
|
|
972
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
973
|
+
cx: "7",
|
|
974
|
+
cy: "7",
|
|
975
|
+
r: "4.5",
|
|
976
|
+
stroke: "currentColor",
|
|
977
|
+
strokeWidth: "1.5"
|
|
978
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
979
|
+
d: "M10.5 10.5 14 14",
|
|
980
|
+
stroke: "currentColor",
|
|
981
|
+
strokeWidth: "1.5",
|
|
982
|
+
strokeLinecap: "round"
|
|
983
|
+
})]
|
|
984
|
+
})
|
|
985
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
986
|
+
ref: searchRef,
|
|
987
|
+
className: ModelPicker_module_css_default.searchInput,
|
|
988
|
+
type: "text",
|
|
989
|
+
value: picker.query,
|
|
990
|
+
placeholder: t("modelPicker.searchPlaceholder"),
|
|
991
|
+
onChange: (event) => {
|
|
992
|
+
setPicker((p) => ({
|
|
993
|
+
...p,
|
|
994
|
+
query: event.target.value
|
|
995
|
+
}));
|
|
996
|
+
},
|
|
997
|
+
"aria-label": t("modelPicker.searchPlaceholder")
|
|
998
|
+
})]
|
|
999
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1000
|
+
className: ModelPicker_module_css_default.columns,
|
|
1001
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
|
|
1002
|
+
ref: providersRef,
|
|
1003
|
+
className: ModelPicker_module_css_default.providers,
|
|
1004
|
+
"aria-label": t("modelPicker.providers"),
|
|
1005
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1006
|
+
className: ModelPicker_module_css_default.columnTitle,
|
|
1007
|
+
children: t("modelPicker.providers")
|
|
1008
|
+
}), state.status === "loading" || loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1009
|
+
className: ModelPicker_module_css_default.status,
|
|
1010
|
+
children: t("modelPicker.loading")
|
|
1011
|
+
}) : groups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1012
|
+
className: ModelPicker_module_css_default.status,
|
|
1013
|
+
children: t("modelPicker.emptyProviders")
|
|
1014
|
+
}) : visibleGroups.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1015
|
+
className: ModelPicker_module_css_default.status,
|
|
1016
|
+
children: t("modelPicker.emptySearch")
|
|
1017
|
+
}) : visibleGroups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1018
|
+
ref: group.id === activeProvider ? currentProviderRef : null,
|
|
1019
|
+
type: "button",
|
|
1020
|
+
className: `${ModelPicker_module_css_default.cell} ${group.id === activeProvider ? ModelPicker_module_css_default.cellActive : ""}`,
|
|
1021
|
+
onClick: () => {
|
|
1022
|
+
chooseProvider(group.id);
|
|
1023
|
+
},
|
|
1024
|
+
children: group.name
|
|
1025
|
+
}, group.id))]
|
|
1026
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1027
|
+
ref: modelsRef,
|
|
1028
|
+
className: ModelPicker_module_css_default.models,
|
|
1029
|
+
"aria-label": t("modelPicker.models"),
|
|
1030
|
+
children: [
|
|
1031
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1032
|
+
className: ModelPicker_module_css_default.columnTitle,
|
|
1033
|
+
children: t("modelPicker.models")
|
|
1034
|
+
}),
|
|
1035
|
+
hasError && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1036
|
+
className: ModelPicker_module_css_default.error,
|
|
1037
|
+
role: "alert",
|
|
1038
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: shownError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1039
|
+
type: "button",
|
|
1040
|
+
className: ModelPicker_module_css_default.retry,
|
|
1041
|
+
onClick: () => {
|
|
1042
|
+
props.load();
|
|
1043
|
+
setPicker((p) => ({
|
|
1044
|
+
...p,
|
|
1045
|
+
error: null
|
|
1046
|
+
}));
|
|
1047
|
+
},
|
|
1048
|
+
children: t("modelPicker.retry")
|
|
1049
|
+
})]
|
|
1050
|
+
}),
|
|
1051
|
+
activeProvider === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1052
|
+
className: ModelPicker_module_css_default.status,
|
|
1053
|
+
children: t("modelPicker.emptyModels")
|
|
1054
|
+
}) : activeModels.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1055
|
+
className: ModelPicker_module_css_default.status,
|
|
1056
|
+
children: t("modelPicker.emptyModels")
|
|
1057
|
+
}) : activeModels.map((model) => {
|
|
1058
|
+
const selected = current?.provider === activeProvider && current.model === model.id;
|
|
1059
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1060
|
+
ref: selected ? currentModelRef : null,
|
|
1061
|
+
type: "button",
|
|
1062
|
+
className: `${ModelPicker_module_css_default.cell} ${selected ? ModelPicker_module_css_default.cellSelected : ""}`,
|
|
1063
|
+
disabled: state.status === "selecting",
|
|
1064
|
+
onClick: () => {
|
|
1065
|
+
chooseModel(model.id);
|
|
1066
|
+
},
|
|
1067
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1068
|
+
className: ModelPicker_module_css_default.modelName,
|
|
1069
|
+
children: model.name
|
|
1070
|
+
}), selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1071
|
+
className: ModelPicker_module_css_default.check,
|
|
1072
|
+
"aria-hidden": "true",
|
|
1073
|
+
children: "✓"
|
|
1074
|
+
}) : null]
|
|
1075
|
+
}, model.id);
|
|
1076
|
+
})
|
|
1077
|
+
]
|
|
1078
|
+
})]
|
|
1079
|
+
})]
|
|
1080
|
+
}), document.body)]
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
//#endregion
|
|
1084
|
+
//#region src/client/model-picker/index.ts
|
|
1085
|
+
/**
|
|
1086
|
+
* 注册模型选择器到 conversation.input.right(provider-label 右侧)。
|
|
1087
|
+
* @param ctx - 需已提供 modelDirectories 服务的客户端上下文。
|
|
1088
|
+
* @returns 停止注入并卸载注册的 disposer。
|
|
1089
|
+
*/
|
|
1090
|
+
function installModelPicker(ctx) {
|
|
1091
|
+
return ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
|
|
1092
|
+
name: "conversation.input.right",
|
|
1093
|
+
id: "model-picker",
|
|
1094
|
+
order: 1e3,
|
|
1095
|
+
locale: "ui-fortifier",
|
|
1096
|
+
inject: (sessionId) => {
|
|
1097
|
+
const directory = ctx.modelDirectories.directoryFor(sessionId);
|
|
1098
|
+
return {
|
|
1099
|
+
hooks: { directory: directory.store },
|
|
1100
|
+
load: () => {
|
|
1101
|
+
directory.load().catch(() => {});
|
|
1102
|
+
},
|
|
1103
|
+
select: (selection) => directory.select(selection).then(() => true, () => false)
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
}, ModelPicker));
|
|
1107
|
+
}
|
|
1108
|
+
//#endregion
|
|
163
1109
|
//#region src/client/index.ts
|
|
164
1110
|
/** 功能模块清单:字段名(settings 开关 key) → 模块安装面。 */
|
|
165
|
-
const modules = {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
1111
|
+
const modules = {
|
|
1112
|
+
"provider-label": {
|
|
1113
|
+
module: installProviderLabel,
|
|
1114
|
+
dispose: void 0
|
|
1115
|
+
},
|
|
1116
|
+
"open-dsh-folder": {
|
|
1117
|
+
module: installOpenDshFolder,
|
|
1118
|
+
dispose: void 0
|
|
1119
|
+
},
|
|
1120
|
+
"settings-frame": {
|
|
1121
|
+
module: installSettingsFrame,
|
|
1122
|
+
dispose: void 0
|
|
1123
|
+
},
|
|
1124
|
+
"model-picker": {
|
|
1125
|
+
module: installModelPicker,
|
|
1126
|
+
dispose: void 0
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
/** 设置页开关行的显示顺序(按位置排序);不在表中的字段排在其后。 */
|
|
1130
|
+
const MODULE_ORDER = [
|
|
1131
|
+
"provider-label",
|
|
1132
|
+
"model-picker",
|
|
1133
|
+
"open-dsh-folder",
|
|
1134
|
+
"settings-frame"
|
|
1135
|
+
];
|
|
169
1136
|
const inject = [
|
|
170
1137
|
"slots",
|
|
171
1138
|
"locale",
|
|
172
1139
|
"settingsScope",
|
|
173
|
-
"modelDirectories"
|
|
1140
|
+
"modelDirectories",
|
|
1141
|
+
"connection",
|
|
1142
|
+
"sessions",
|
|
1143
|
+
"remote",
|
|
1144
|
+
"remote.session"
|
|
174
1145
|
];
|
|
175
1146
|
/**
|
|
176
1147
|
* 客户端插件体:注册设置页并据开关状态挂载各功能模块。
|
|
177
|
-
* @param ctx - 需已提供 slots/locale/settingsScope/modelDirectories 的客户端上下文。
|
|
1148
|
+
* @param ctx - 需已提供 slots/locale/settingsScope/modelDirectories/connection/sessions/remote 的客户端上下文。
|
|
178
1149
|
*/
|
|
179
1150
|
function apply(ctx) {
|
|
180
|
-
const scope = installSettings(ctx,
|
|
1151
|
+
const scope = installSettings(ctx, MODULE_ORDER);
|
|
181
1152
|
for (const field of Object.keys(modules)) {
|
|
182
1153
|
const item = modules[field];
|
|
183
1154
|
const sync = () => {
|
|
@@ -191,6 +1162,7 @@ window.__ModuleLoader__.load({
|
|
|
191
1162
|
}
|
|
192
1163
|
}
|
|
193
1164
|
//#endregion
|
|
1165
|
+
exports.MODULE_ORDER = MODULE_ORDER;
|
|
194
1166
|
exports.apply = apply;
|
|
195
1167
|
exports.inject = inject;
|
|
196
1168
|
exports.modules = modules;
|