@fonlan/dsh-model-router 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -10
- package/lib/client.js +844 -239
- package/lib/client.js.map +1 -1
- package/lib/index.js +257 -30
- package/lib/types/server/adapter.d.ts +27 -1
- package/lib/types/server/service.d.ts +45 -5
- package/lib/types/shared/config.d.ts +54 -2
- package/package.json +33 -29
- package/src/client/api.ts +11 -1
- package/src/client/index.tsx +39 -16
- package/src/client/locales.ts +64 -14
- package/src/client/model-seat.css +289 -0
- package/src/client/model-seat.tsx +623 -0
- package/src/client/settings-card.css +470 -0
- package/src/client/settings-card.tsx +468 -0
- package/src/server/adapter.ts +62 -13
- package/src/server/rpc.ts +16 -0
- package/src/server/service.ts +135 -25
- package/src/shared/config.ts +142 -8
- package/src/client/route-switcher.css +0 -136
- package/src/client/route-switcher.tsx +0 -252
- package/src/client/settings-section.css +0 -245
- package/src/client/settings-section.tsx +0 -293
package/lib/client.js
CHANGED
|
@@ -11,7 +11,10 @@ window.__ModuleLoader__.load({
|
|
|
11
11
|
const LOCALE_NS = "model-router";
|
|
12
12
|
const zh = {
|
|
13
13
|
settingsTitle: "模型路由",
|
|
14
|
-
|
|
14
|
+
settingsCardDescription: "模型聚合、提供商切换与优先级顺序",
|
|
15
|
+
expand: "展开",
|
|
16
|
+
collapse: "收起",
|
|
17
|
+
readOnly: "本部署的设置为只读。",
|
|
15
18
|
loading: "加载中…",
|
|
16
19
|
loadFailed: "加载失败:{message}",
|
|
17
20
|
retry: "重试",
|
|
@@ -24,22 +27,47 @@ window.__ModuleLoader__.load({
|
|
|
24
27
|
switchFailed: "切换失败:{message}",
|
|
25
28
|
reorderFailed: "排序失败:{message}",
|
|
26
29
|
providerCount: "{count} 个提供商",
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
seatLabel: "模型选择(含提供商切换)",
|
|
31
|
+
menuAria: "模型、提供商与推理等级",
|
|
32
|
+
menuModel: "模型",
|
|
33
|
+
menuProvider: "提供商",
|
|
34
|
+
menuEffort: "推理等级",
|
|
35
|
+
triggerFallback: "选择模型",
|
|
36
|
+
triggerSelectAria: "选择模型",
|
|
37
|
+
triggerAria: "选择模型,当前 {model}",
|
|
38
|
+
triggerAriaEffort: "选择模型,当前 {model},推理等级 {effort}",
|
|
39
|
+
effortProviderDefault: "Default",
|
|
40
|
+
statusLoading: "正在刷新模型列表…",
|
|
41
|
+
errorAction: "模型操作失败:{message}",
|
|
42
|
+
actionReload: "重新加载",
|
|
43
|
+
warningGroupLoad: "{name} 加载失败:{message}",
|
|
44
|
+
emptyModels: "没有可用的模型。",
|
|
45
|
+
emptyEfforts: "当前模型未提供推理等级。",
|
|
30
46
|
routeMenuTitle: "{model} · 选择提供商",
|
|
31
47
|
routeLoading: "加载路由…",
|
|
32
|
-
unknownProvider: "未知提供商",
|
|
33
48
|
noCredentialShort: "未配置凭据",
|
|
34
49
|
globalRouteHint: "切换后对所有会话的后续请求生效",
|
|
35
|
-
showQuickSwitchLabel: "
|
|
36
|
-
showQuickSwitchDescription: "开启后,当当前模型是 model-router
|
|
50
|
+
showQuickSwitchLabel: "在模型选择菜单中显示提供商切换",
|
|
51
|
+
showQuickSwitchDescription: "开启后,当当前模型是 model-router 路由时,模型选择弹出菜单会显示提供商列表入口,可直接切换该模型的激活提供商。",
|
|
37
52
|
ignorePrefixLabel: "匹配时忽略模型 ID 前缀",
|
|
38
|
-
ignorePrefixDescription: "开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。"
|
|
53
|
+
ignorePrefixDescription: "开启后,deepseek/deepseek-v4-flash 与 deepseek-v4-flash 视为同一模型,统一显示不带前缀的模型 id(默认开启)。",
|
|
54
|
+
sortTitle: "模型显示顺序",
|
|
55
|
+
sortDescription: "模型选择列表中 model-router 分组内模型的排列方式。",
|
|
56
|
+
sortModeCustom: "自定义",
|
|
57
|
+
sortModeName: "按名称",
|
|
58
|
+
sortModeRecent: "最近使用",
|
|
59
|
+
sortModeDescCustom: "按下方拖拽的手动顺序排列",
|
|
60
|
+
sortModeDescName: "按模型名称排序(数字感知)",
|
|
61
|
+
sortModeDescRecent: "最近用过的模型排在最前",
|
|
62
|
+
sortCustomHint: "拖拽调整模型在模型选择列表中的显示顺序",
|
|
63
|
+
sortFailed: "排序设置失败:{message}"
|
|
39
64
|
};
|
|
40
65
|
const en = {
|
|
41
66
|
settingsTitle: "Model Router",
|
|
42
|
-
|
|
67
|
+
settingsCardDescription: "Model aggregation, provider switching and preference order",
|
|
68
|
+
expand: "Expand",
|
|
69
|
+
collapse: "Collapse",
|
|
70
|
+
readOnly: "This deployment stores settings read-only.",
|
|
43
71
|
loading: "Loading…",
|
|
44
72
|
loadFailed: "Load failed: {message}",
|
|
45
73
|
retry: "Retry",
|
|
@@ -52,18 +80,40 @@ window.__ModuleLoader__.load({
|
|
|
52
80
|
switchFailed: "Switch failed: {message}",
|
|
53
81
|
reorderFailed: "Reorder failed: {message}",
|
|
54
82
|
providerCount: "{count} providers",
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
seatLabel: "Model selection (with provider switching)",
|
|
84
|
+
menuAria: "Model, provider and reasoning effort",
|
|
85
|
+
menuModel: "Model",
|
|
86
|
+
menuProvider: "Provider",
|
|
87
|
+
menuEffort: "Effort",
|
|
88
|
+
triggerFallback: "Select model",
|
|
89
|
+
triggerSelectAria: "Select model",
|
|
90
|
+
triggerAria: "Select model, current {model}",
|
|
91
|
+
triggerAriaEffort: "Select model, current {model}, reasoning effort {effort}",
|
|
92
|
+
effortProviderDefault: "Default",
|
|
93
|
+
statusLoading: "Refreshing model list…",
|
|
94
|
+
errorAction: "Model operation failed: {message}",
|
|
95
|
+
actionReload: "Reload",
|
|
96
|
+
warningGroupLoad: "{name} failed to load: {message}",
|
|
97
|
+
emptyModels: "No models available.",
|
|
98
|
+
emptyEfforts: "This model provides no reasoning effort levels.",
|
|
58
99
|
routeMenuTitle: "{model} · Choose provider",
|
|
59
100
|
routeLoading: "Loading routes…",
|
|
60
|
-
unknownProvider: "Unknown provider",
|
|
61
101
|
noCredentialShort: "No credentials",
|
|
62
102
|
globalRouteHint: "Changes apply to subsequent requests in all sessions",
|
|
63
|
-
showQuickSwitchLabel: "Show
|
|
64
|
-
showQuickSwitchDescription: "When enabled,
|
|
103
|
+
showQuickSwitchLabel: "Show provider switching in the model picker",
|
|
104
|
+
showQuickSwitchDescription: "When enabled, the model picker menu shows a provider list entry that switches the active provider, for model-router routes.",
|
|
65
105
|
ignorePrefixLabel: "Ignore model ID prefix when matching",
|
|
66
|
-
ignorePrefixDescription: "When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default)."
|
|
106
|
+
ignorePrefixDescription: "When enabled, deepseek/deepseek-v4-flash and deepseek-v4-flash are treated as the same model and shown without the prefix (on by default).",
|
|
107
|
+
sortTitle: "Model display order",
|
|
108
|
+
sortDescription: "How models inside the model-router group are ordered in the model picker.",
|
|
109
|
+
sortModeCustom: "Custom",
|
|
110
|
+
sortModeName: "By name",
|
|
111
|
+
sortModeRecent: "Recently used",
|
|
112
|
+
sortModeDescCustom: "Manual order via drag below",
|
|
113
|
+
sortModeDescName: "Sort by model name (numeric-aware)",
|
|
114
|
+
sortModeDescRecent: "Most recently used models first",
|
|
115
|
+
sortCustomHint: "Drag to reorder models in the model picker",
|
|
116
|
+
sortFailed: "Sort setting failed: {message}"
|
|
67
117
|
};
|
|
68
118
|
//#endregion
|
|
69
119
|
//#region src/client/api.ts
|
|
@@ -101,12 +151,14 @@ window.__ModuleLoader__.load({
|
|
|
101
151
|
}),
|
|
102
152
|
refresh: () => call("refresh"),
|
|
103
153
|
setShowQuickSwitch: (value) => call("set-show-quick-switch", { value }),
|
|
104
|
-
setIgnoreModelIdPrefix: (value) => call("set-ignore-model-id-prefix", { value })
|
|
154
|
+
setIgnoreModelIdPrefix: (value) => call("set-ignore-model-id-prefix", { value }),
|
|
155
|
+
setModelSort: (mode) => call("set-model-sort", { mode }),
|
|
156
|
+
setModelOrder: (order) => call("set-model-order", { order })
|
|
105
157
|
};
|
|
106
158
|
//#endregion
|
|
107
|
-
//#region \0dsh-css:/
|
|
108
|
-
const css$1 = "/* Model Router settings
|
|
109
|
-
const tagId$1 = "@fonlan/dsh-model-router/settings-
|
|
159
|
+
//#region \0dsh-css:/home/runner/work/dsh-model-router/dsh-model-router/src/client/settings-card.css.mjs
|
|
160
|
+
const css$1 = "/* Model Router settings card — chrome aligned with the built-in plugin cards\n (设置 → 插件 → 插件配置), theme-consistent via DSW alias variables. */\n.mr-settings-card {\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n background: var(--dsw-alias-bg-layer-3, #0d1017);\n border-radius: 12px;\n list-style: none;\n transition: border-color .16s, background .16s;\n}\n\n.mr-settings-card:hover {\n border-color: var(--dsw-alias-label-dimmed, #4a5468);\n}\n\n.mr-settings-card[data-open] {\n background: var(--dsw-alias-bg-layer-2, #141a26);\n border-color: var(--dsw-alias-label-dimmed, #4a5468);\n}\n\n.mr-settings-head {\n appearance: none;\n width: 100%;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: transparent;\n border: 0;\n border-radius: 12px;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n display: flex;\n}\n\n.mr-settings-head:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary, #4f8cff);\n outline-offset: -2px;\n}\n\n.mr-settings-head-text {\n flex-direction: column;\n flex: 1;\n gap: 4px;\n min-width: 0;\n display: flex;\n}\n\n.mr-settings-title {\n color: var(--dsw-alias-label-primary, #e6e6e6);\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n}\n\n.mr-settings-sub {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n font-size: 13px;\n line-height: 1.5;\n}\n\n.mr-settings-chevron {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n flex: none;\n transition: transform .16s;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.mr-settings-chevron[data-open] {\n transform: rotate(180deg);\n}\n\n.mr-settings-body {\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n margin: 0 16px;\n padding: 12px 0 8px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.mr-settings-readonly {\n color: var(--dsw-alias-label-tertiary, #8b93a7);\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n}\n\n/* ── card body: the router controls ─────────────────────────────────────── */\n\n.mr-root {\n display: flex;\n flex-direction: column;\n gap: 12px;\n padding: 0 0 4px;\n}\n\n.mr-quick-switch {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n cursor: pointer;\n}\n\n.mr-quick-switch input[type=\"checkbox\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-quick-switch-copy {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.mr-quick-switch-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-quick-switch-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-toolbar {\n display: flex;\n justify-content: space-between;\n align-items: center;\n}\n\n.mr-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-button {\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n background: var(--dsw-alias-interactive-bg-hover);\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: 8px;\n font-size: 12px;\n line-height: 18px;\n padding: 3px 10px;\n}\n\n.mr-button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n filter: brightness(1.08);\n}\n\n.mr-button:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n.mr-error {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 16px 0;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-models {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.mr-card {\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 12px;\n padding: 10px 12px;\n background: var(--dsw-alias-bg-module-platform);\n transition: opacity 0.12s;\n}\n\n.mr-card-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-card-header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n}\n\n.mr-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n\n.mr-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 12px;\n line-height: 18px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n flex: 1;\n}\n\n.mr-provider-count {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-drag-hint {\n margin: 2px 0 6px;\n color: var(--dsw-alias-label-caption);\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-providers {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-provider {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid transparent;\n border-radius: 10px;\n padding: 2px 8px 2px 4px;\n}\n\n.mr-provider-active {\n background: var(--dsw-alias-interactive-bg-hover);\n border-color: var(--dsw-alias-border-l3);\n}\n\n.mr-provider-nocred {\n opacity: 0.55;\n}\n\n.mr-provider-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-grip {\n color: var(--dsw-alias-label-caption);\n cursor: grab;\n font-size: 12px;\n line-height: 20px;\n flex: none;\n user-select: none;\n}\n\n.mr-grip:active {\n cursor: grabbing;\n}\n\n.mr-provider-button {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 6px;\n color: var(--dsw-alias-label-primary);\n background: none;\n border: none;\n text-align: left;\n cursor: pointer;\n font: inherit;\n padding: 4px 0;\n}\n\n.mr-provider-button:disabled {\n cursor: default;\n}\n\n.mr-provider-name {\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n}\n\n.mr-provider-nocred-label {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n flex: none;\n}\n\n.mr-active-badge {\n flex: none;\n color: var(--dsw-alias-label-inverted, #fff);\n background: var(--dsw-alias-border-brand, #4d6bfe);\n border-radius: 999px;\n font-size: 11px;\n line-height: 16px;\n padding: 0 8px;\n}\n\n/* ── model display order ────────────────────────────────────────────────── */\n\n.mr-sort {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 10px;\n background: var(--dsw-alias-bg-module-platform);\n}\n\n.mr-sort-head {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.mr-sort-title {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-modes {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.mr-sort-mode {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 6px;\n cursor: pointer;\n}\n\n.mr-sort-mode:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-sort-mode input[type=\"radio\"] {\n margin-top: 2px;\n accent-color: var(--dsw-alias-accent);\n}\n\n.mr-sort-mode-copy {\n display: flex;\n flex-direction: column;\n gap: 1px;\n min-width: 0;\n}\n\n.mr-sort-mode-label {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-sort-mode-desc {\n color: var(--dsw-alias-label-tertiary);\n font-size: 12px;\n line-height: 18px;\n}\n\n.mr-sort-custom {\n display: flex;\n flex-direction: column;\n gap: 6px;\n border-top: 1px solid var(--dsw-alias-border-l2, #2a3346);\n padding-top: 8px;\n}\n\n.mr-sort-hint {\n color: var(--dsw-alias-label-caption);\n margin: 0;\n font-size: 11px;\n line-height: 16px;\n}\n\n.mr-sort-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n max-height: 280px;\n overflow-y: auto;\n}\n\n.mr-sort-item {\n display: flex;\n align-items: center;\n gap: 6px;\n border: 1px solid var(--dsw-alias-border-l2, #2a3346);\n border-radius: 8px;\n padding: 4px 8px 4px 4px;\n background: var(--dsw-alias-bg-layer-1, #0d1017);\n}\n\n.mr-sort-item-drop {\n border-color: var(--dsw-alias-border-brand);\n}\n\n.mr-sort-item-busy {\n opacity: 0.6;\n pointer-events: none;\n}\n\n.mr-sort-model-name {\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n line-height: 20px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n min-width: 0;\n}\n\n.mr-sort-model-id {\n color: var(--dsw-alias-label-tertiary);\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n line-height: 16px;\n text-overflow: ellipsis;\n white-space: nowrap;\n overflow: hidden;\n flex: 1;\n text-align: right;\n}\n";
|
|
161
|
+
const tagId$1 = "@fonlan/dsh-model-router/settings-card.css";
|
|
110
162
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
|
|
111
163
|
const tag = document.createElement("style");
|
|
112
164
|
tag.dataset.plugin = "@fonlan/dsh-model-router";
|
|
@@ -115,13 +167,23 @@ window.__ModuleLoader__.load({
|
|
|
115
167
|
document.head.appendChild(tag);
|
|
116
168
|
}
|
|
117
169
|
//#endregion
|
|
118
|
-
//#region src/client/settings-
|
|
170
|
+
//#region src/client/settings-card.tsx
|
|
119
171
|
/**
|
|
120
|
-
* The Model Router
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
* settings
|
|
172
|
+
* The Model Router Settings Card (设置 → 插件 → 插件配置 → 模型路由).
|
|
173
|
+
*
|
|
174
|
+
* Renders its own expandable chrome aligned with the built-in plugin cards
|
|
175
|
+
* (external plugins cannot import PluginCard): a header naming the plugin and
|
|
176
|
+
* what its settings govern, disclosing the router controls in place. All
|
|
177
|
+
* reads/mutations go through the plugin's fenced API; the server persists
|
|
178
|
+
* through the `model-router` settings namespace, so a switch here is live for
|
|
179
|
+
* the next request globally.
|
|
180
|
+
*
|
|
181
|
+
* The bound settings scope (the same `model-router` namespace the host half
|
|
182
|
+
* registers) supplies the card's dispatch state: while the namespace is merely
|
|
183
|
+
* loading the card stays mounted, and when it is unavailable (deployment
|
|
184
|
+
* without the host half) nothing renders — matching the built-in cards. A
|
|
185
|
+
* read-only deployment shows the built-in card's banner and disables the
|
|
186
|
+
* mutation controls.
|
|
125
187
|
*/
|
|
126
188
|
function arrayMove(list, from, to) {
|
|
127
189
|
const next = [...list];
|
|
@@ -146,7 +208,7 @@ window.__ModuleLoader__.load({
|
|
|
146
208
|
}, [ctx]);
|
|
147
209
|
return (0, react.useSyncExternalStore)(subscribe, getSnapshot);
|
|
148
210
|
}
|
|
149
|
-
function
|
|
211
|
+
function makeSettingsCard(ctx) {
|
|
150
212
|
const t = (() => {
|
|
151
213
|
try {
|
|
152
214
|
return ctx.locale.bind(LOCALE_NS);
|
|
@@ -154,8 +216,11 @@ window.__ModuleLoader__.load({
|
|
|
154
216
|
return (key) => key;
|
|
155
217
|
}
|
|
156
218
|
})();
|
|
157
|
-
return function
|
|
219
|
+
return function ModelRouterSettingsCard(props) {
|
|
220
|
+
const { scope } = props;
|
|
221
|
+
const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
158
222
|
useLocaleRevision(ctx);
|
|
223
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
159
224
|
const [state, setState] = (0, react.useState)(null);
|
|
160
225
|
const [error, setError] = (0, react.useState)(null);
|
|
161
226
|
const [loading, setLoading] = (0, react.useState)(true);
|
|
@@ -163,6 +228,8 @@ window.__ModuleLoader__.load({
|
|
|
163
228
|
const [drag, setDrag] = (0, react.useState)(null);
|
|
164
229
|
const [quickSwitchBusy, setQuickSwitchBusy] = (0, react.useState)(false);
|
|
165
230
|
const [ignorePrefixBusy, setIgnorePrefixBusy] = (0, react.useState)(false);
|
|
231
|
+
const [sortBusy, setSortBusy] = (0, react.useState)(false);
|
|
232
|
+
const [modelDrag, setModelDrag] = (0, react.useState)(null);
|
|
166
233
|
const load = (0, react.useCallback)(async () => {
|
|
167
234
|
setLoading(true);
|
|
168
235
|
try {
|
|
@@ -258,101 +325,285 @@ window.__ModuleLoader__.load({
|
|
|
258
325
|
setIgnorePrefixBusy(false);
|
|
259
326
|
}
|
|
260
327
|
}, [state]);
|
|
328
|
+
const changeModelSort = (0, react.useCallback)(async (mode) => {
|
|
329
|
+
if (state === null || state.modelSort === mode) return;
|
|
330
|
+
const previous = state.modelSort;
|
|
331
|
+
setState((prev) => prev === null ? prev : {
|
|
332
|
+
...prev,
|
|
333
|
+
modelSort: mode
|
|
334
|
+
});
|
|
335
|
+
setSortBusy(true);
|
|
336
|
+
try {
|
|
337
|
+
setState(await api.setModelSort(mode));
|
|
338
|
+
setError(null);
|
|
339
|
+
} catch (cause) {
|
|
340
|
+
setState((prev) => prev === null ? prev : {
|
|
341
|
+
...prev,
|
|
342
|
+
modelSort: previous
|
|
343
|
+
});
|
|
344
|
+
setError(t("sortFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
|
|
345
|
+
} finally {
|
|
346
|
+
setSortBusy(false);
|
|
347
|
+
}
|
|
348
|
+
}, [state, t]);
|
|
349
|
+
const moveModel = (0, react.useCallback)(async (from, to) => {
|
|
350
|
+
if (state === null || from === to) return;
|
|
351
|
+
const order = arrayMove(state.models.map((model) => model.id), from, to);
|
|
352
|
+
setState((prev) => prev === null ? prev : {
|
|
353
|
+
...prev,
|
|
354
|
+
modelOrder: order
|
|
355
|
+
});
|
|
356
|
+
setSortBusy(true);
|
|
357
|
+
try {
|
|
358
|
+
setState(await api.setModelOrder(order));
|
|
359
|
+
setError(null);
|
|
360
|
+
} catch (cause) {
|
|
361
|
+
setState((prev) => prev === null ? prev : {
|
|
362
|
+
...prev,
|
|
363
|
+
modelOrder: state.modelOrder
|
|
364
|
+
});
|
|
365
|
+
setError(t("sortFailed", { message: cause instanceof Error ? cause.message : String(cause) }));
|
|
366
|
+
} finally {
|
|
367
|
+
setSortBusy(false);
|
|
368
|
+
}
|
|
369
|
+
}, [state, t]);
|
|
370
|
+
if (snapshot.status === "unavailable") return null;
|
|
371
|
+
const writable = snapshot.writable;
|
|
261
372
|
const models = (0, react.useMemo)(() => state?.models ?? [], [state]);
|
|
262
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("
|
|
263
|
-
className: "mr-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
checked: state?.showQuickSwitch ?? true,
|
|
274
|
-
disabled: loading || quickSwitchBusy,
|
|
275
|
-
onChange: (event) => void toggleQuickSwitch(event.target.checked)
|
|
276
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
277
|
-
className: "mr-quick-switch-copy",
|
|
278
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
279
|
-
className: "mr-quick-switch-label",
|
|
280
|
-
children: t("showQuickSwitchLabel")
|
|
281
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
282
|
-
className: "mr-quick-switch-desc",
|
|
283
|
-
children: t("showQuickSwitchDescription")
|
|
284
|
-
})]
|
|
285
|
-
})]
|
|
286
|
-
}),
|
|
287
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
288
|
-
className: "mr-quick-switch",
|
|
289
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
290
|
-
type: "checkbox",
|
|
291
|
-
checked: state?.ignoreModelIdPrefix ?? true,
|
|
292
|
-
disabled: loading || ignorePrefixBusy,
|
|
293
|
-
onChange: (event) => void toggleIgnorePrefix(event.target.checked)
|
|
294
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
295
|
-
className: "mr-quick-switch-copy",
|
|
296
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
297
|
-
className: "mr-quick-switch-label",
|
|
298
|
-
children: t("ignorePrefixLabel")
|
|
299
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
300
|
-
className: "mr-quick-switch-desc",
|
|
301
|
-
children: t("ignorePrefixDescription")
|
|
302
|
-
})]
|
|
303
|
-
})]
|
|
304
|
-
}),
|
|
305
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
306
|
-
className: "mr-toolbar",
|
|
373
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
374
|
+
className: "mr-settings-card",
|
|
375
|
+
"data-open": open ? "" : void 0,
|
|
376
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
377
|
+
type: "button",
|
|
378
|
+
className: "mr-settings-head",
|
|
379
|
+
"aria-expanded": open,
|
|
380
|
+
"aria-label": (open ? t("collapse") : t("expand")) + ":" + t("settingsTitle"),
|
|
381
|
+
onClick: () => setOpen(!open),
|
|
382
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
383
|
+
className: "mr-settings-head-text",
|
|
307
384
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
308
|
-
className: "mr-
|
|
309
|
-
children: t("
|
|
310
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
onClick: () => void load(),
|
|
314
|
-
disabled: loading,
|
|
315
|
-
children: t("refresh")
|
|
385
|
+
className: "mr-settings-title",
|
|
386
|
+
children: t("settingsTitle")
|
|
387
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
388
|
+
className: "mr-settings-sub",
|
|
389
|
+
children: t("settingsCardDescription")
|
|
316
390
|
})]
|
|
317
|
-
}),
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
children:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
391
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
392
|
+
className: "mr-settings-chevron",
|
|
393
|
+
"data-open": open ? "" : void 0,
|
|
394
|
+
"aria-hidden": "true",
|
|
395
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
396
|
+
width: "14",
|
|
397
|
+
height: "14",
|
|
398
|
+
viewBox: "0 0 14 14",
|
|
399
|
+
fill: "none",
|
|
400
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
401
|
+
d: "M3.5 5.25L7 8.75L10.5 5.25",
|
|
402
|
+
stroke: "currentColor",
|
|
403
|
+
strokeWidth: "1.5",
|
|
404
|
+
strokeLinecap: "round",
|
|
405
|
+
strokeLinejoin: "round"
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
})]
|
|
409
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
410
|
+
className: "mr-settings-body",
|
|
411
|
+
children: [!writable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
412
|
+
className: "mr-settings-readonly",
|
|
413
|
+
role: "status",
|
|
414
|
+
children: t("readOnly")
|
|
415
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
416
|
+
className: "mr-root",
|
|
417
|
+
children: [
|
|
418
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
419
|
+
className: "mr-quick-switch",
|
|
420
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
421
|
+
type: "checkbox",
|
|
422
|
+
checked: state?.showQuickSwitch ?? true,
|
|
423
|
+
disabled: loading || quickSwitchBusy || !writable,
|
|
424
|
+
onChange: (event) => void toggleQuickSwitch(event.target.checked)
|
|
425
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
426
|
+
className: "mr-quick-switch-copy",
|
|
427
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
428
|
+
className: "mr-quick-switch-label",
|
|
429
|
+
children: t("showQuickSwitchLabel")
|
|
430
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
431
|
+
className: "mr-quick-switch-desc",
|
|
432
|
+
children: t("showQuickSwitchDescription")
|
|
433
|
+
})]
|
|
434
|
+
})]
|
|
341
435
|
}),
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
436
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
437
|
+
className: "mr-quick-switch",
|
|
438
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
439
|
+
type: "checkbox",
|
|
440
|
+
checked: state?.ignoreModelIdPrefix ?? true,
|
|
441
|
+
disabled: loading || ignorePrefixBusy || !writable,
|
|
442
|
+
onChange: (event) => void toggleIgnorePrefix(event.target.checked)
|
|
443
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
444
|
+
className: "mr-quick-switch-copy",
|
|
445
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
446
|
+
className: "mr-quick-switch-label",
|
|
447
|
+
children: t("ignorePrefixLabel")
|
|
448
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
449
|
+
className: "mr-quick-switch-desc",
|
|
450
|
+
children: t("ignorePrefixDescription")
|
|
451
|
+
})]
|
|
452
|
+
})]
|
|
453
|
+
}),
|
|
454
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
455
|
+
className: "mr-sort",
|
|
456
|
+
children: [
|
|
457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
458
|
+
className: "mr-sort-head",
|
|
459
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
460
|
+
className: "mr-sort-title",
|
|
461
|
+
children: t("sortTitle")
|
|
462
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
463
|
+
className: "mr-sort-desc",
|
|
464
|
+
children: t("sortDescription")
|
|
465
|
+
})]
|
|
466
|
+
}),
|
|
467
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
468
|
+
className: "mr-sort-modes",
|
|
469
|
+
role: "radiogroup",
|
|
470
|
+
"aria-label": t("sortTitle"),
|
|
471
|
+
children: [
|
|
472
|
+
"custom",
|
|
473
|
+
"name",
|
|
474
|
+
"recent"
|
|
475
|
+
].map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
476
|
+
className: "mr-sort-mode",
|
|
477
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
478
|
+
type: "radio",
|
|
479
|
+
name: "mr-model-sort",
|
|
480
|
+
checked: state?.modelSort === mode,
|
|
481
|
+
disabled: loading || sortBusy || !writable,
|
|
482
|
+
onChange: () => void changeModelSort(mode)
|
|
483
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
484
|
+
className: "mr-sort-mode-copy",
|
|
485
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
486
|
+
className: "mr-sort-mode-label",
|
|
487
|
+
children: t(`sortMode${mode[0].toUpperCase()}${mode.slice(1)}`)
|
|
488
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
489
|
+
className: "mr-sort-mode-desc",
|
|
490
|
+
children: t(`sortModeDesc${mode[0].toUpperCase()}${mode.slice(1)}`)
|
|
491
|
+
})]
|
|
492
|
+
})]
|
|
493
|
+
}, mode))
|
|
494
|
+
}),
|
|
495
|
+
state?.modelSort === "custom" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
496
|
+
className: "mr-sort-custom",
|
|
497
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
498
|
+
className: "mr-sort-hint",
|
|
499
|
+
children: t("sortCustomHint")
|
|
500
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
|
|
501
|
+
className: "mr-sort-list",
|
|
502
|
+
children: models.map((model, index) => {
|
|
503
|
+
const dropTarget = modelDrag !== null && modelDrag.index !== index;
|
|
504
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
505
|
+
className: [
|
|
506
|
+
"mr-sort-item",
|
|
507
|
+
dropTarget ? "mr-sort-item-drop" : "",
|
|
508
|
+
sortBusy ? "mr-sort-item-busy" : ""
|
|
509
|
+
].filter(Boolean).join(" "),
|
|
510
|
+
draggable: !sortBusy && writable,
|
|
511
|
+
onDragStart: (event) => {
|
|
512
|
+
setModelDrag({ index });
|
|
513
|
+
event.dataTransfer.effectAllowed = "move";
|
|
514
|
+
try {
|
|
515
|
+
event.dataTransfer.setData("text/plain", model.id);
|
|
516
|
+
} catch {}
|
|
517
|
+
},
|
|
518
|
+
onDragOver: (event) => {
|
|
519
|
+
if (modelDrag !== null && modelDrag.index !== index) {
|
|
520
|
+
event.preventDefault();
|
|
521
|
+
event.dataTransfer.dropEffect = "move";
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
onDrop: (event) => {
|
|
525
|
+
if (modelDrag !== null && modelDrag.index !== index) {
|
|
526
|
+
event.preventDefault();
|
|
527
|
+
moveModel(modelDrag.index, index);
|
|
528
|
+
}
|
|
529
|
+
setModelDrag(null);
|
|
530
|
+
},
|
|
531
|
+
onDragEnd: () => setModelDrag(null),
|
|
532
|
+
children: [
|
|
533
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
534
|
+
className: "mr-grip",
|
|
535
|
+
"aria-hidden": "true",
|
|
536
|
+
children: "⠿"
|
|
537
|
+
}),
|
|
538
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
539
|
+
className: "mr-sort-model-name",
|
|
540
|
+
children: model.name
|
|
541
|
+
}),
|
|
542
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
543
|
+
className: "mr-sort-model-id",
|
|
544
|
+
children: model.id
|
|
545
|
+
})
|
|
546
|
+
]
|
|
547
|
+
}, model.id);
|
|
548
|
+
})
|
|
549
|
+
})]
|
|
550
|
+
})
|
|
551
|
+
]
|
|
552
|
+
}),
|
|
553
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
554
|
+
className: "mr-toolbar",
|
|
555
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
556
|
+
className: "mr-count",
|
|
557
|
+
children: t("modelsCount", { count: models.length })
|
|
558
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
559
|
+
type: "button",
|
|
560
|
+
className: "mr-button",
|
|
561
|
+
onClick: () => void load(),
|
|
562
|
+
disabled: loading,
|
|
563
|
+
children: t("refresh")
|
|
564
|
+
})]
|
|
565
|
+
}),
|
|
566
|
+
error !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
567
|
+
className: "mr-error",
|
|
568
|
+
role: "alert",
|
|
569
|
+
children: error
|
|
570
|
+
}),
|
|
571
|
+
loading && models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
572
|
+
className: "mr-empty",
|
|
573
|
+
children: t("loading")
|
|
574
|
+
}),
|
|
575
|
+
!loading && models.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
576
|
+
className: "mr-empty",
|
|
577
|
+
children: t("empty")
|
|
578
|
+
}),
|
|
579
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
580
|
+
className: "mr-models",
|
|
581
|
+
children: models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelCard, {
|
|
582
|
+
model,
|
|
583
|
+
t,
|
|
584
|
+
readOnly: !writable,
|
|
585
|
+
busy: busyModel === model.id,
|
|
586
|
+
dragging: drag,
|
|
587
|
+
onDragStart: (index) => setDrag({
|
|
588
|
+
modelId: model.id,
|
|
589
|
+
index
|
|
590
|
+
}),
|
|
591
|
+
onDragEnd: () => setDrag(null),
|
|
592
|
+
onDrop: (index) => {
|
|
593
|
+
if (drag !== null && drag.modelId === model.id) reorder(model, drag.index, index);
|
|
594
|
+
setDrag(null);
|
|
595
|
+
},
|
|
596
|
+
onSwitch: (providerId) => void switchActive(model, providerId)
|
|
597
|
+
}, model.id))
|
|
598
|
+
})
|
|
599
|
+
]
|
|
600
|
+
})]
|
|
601
|
+
})]
|
|
351
602
|
});
|
|
352
603
|
};
|
|
353
604
|
}
|
|
354
605
|
function ModelCard(props) {
|
|
355
|
-
const { model, t, busy, dragging } = props;
|
|
606
|
+
const { model, t, readOnly, busy, dragging } = props;
|
|
356
607
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
357
608
|
className: `mr-card${busy ? " mr-card-busy" : ""}`,
|
|
358
609
|
children: [
|
|
@@ -390,7 +641,7 @@ window.__ModuleLoader__.load({
|
|
|
390
641
|
!selectable ? "mr-provider-nocred" : "",
|
|
391
642
|
dropTarget ? "mr-provider-drop" : ""
|
|
392
643
|
].filter(Boolean).join(" "),
|
|
393
|
-
draggable: !busy && model.providers.length > 1,
|
|
644
|
+
draggable: !busy && !readOnly && model.providers.length > 1,
|
|
394
645
|
title: selectable ? void 0 : t("noCredential"),
|
|
395
646
|
onDragStart: (event) => {
|
|
396
647
|
props.onDragStart(index);
|
|
@@ -421,7 +672,7 @@ window.__ModuleLoader__.load({
|
|
|
421
672
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
422
673
|
type: "button",
|
|
423
674
|
className: "mr-provider-button",
|
|
424
|
-
disabled: !selectable || busy || active,
|
|
675
|
+
disabled: !selectable || busy || active || readOnly,
|
|
425
676
|
onClick: () => props.onSwitch(provider.id),
|
|
426
677
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
427
678
|
className: "mr-provider-name",
|
|
@@ -443,9 +694,9 @@ window.__ModuleLoader__.load({
|
|
|
443
694
|
});
|
|
444
695
|
}
|
|
445
696
|
//#endregion
|
|
446
|
-
//#region \0dsh-css:/
|
|
447
|
-
const css = "
|
|
448
|
-
const tagId = "@fonlan/dsh-model-router/
|
|
697
|
+
//#region \0dsh-css:/home/runner/work/dsh-model-router/dsh-model-router/src/client/model-seat.css.mjs
|
|
698
|
+
const css = "/*\n * Composer model seat (shadows the host ModelSelect). Styles mirror the host\n * ModelSelect.module.css exactly (same tokens, sizes and spacing) so the\n * replacement is visually indistinguishable, plus the provider pane's title\n * and hint rows.\n */\n\n.mr-seat-root {\n min-width: 0;\n position: relative;\n}\n\n.mr-seat-trigger {\n min-width: 0;\n max-width: 220px;\n height: 28px;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n background: transparent;\n border: none;\n border-radius: 24px;\n outline: none;\n align-items: center;\n gap: 4px;\n padding: 0 4px 0 8px;\n font-size: 13px;\n font-weight: 500;\n line-height: 20px;\n display: flex;\n}\n\n.mr-seat-trigger:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-seat-trigger:focus-visible {\n box-shadow: 0 0 0 2px var(--dsw-alias-border-l3);\n}\n\n.mr-seat-trigger:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.mr-seat-triggerLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n overflow: hidden;\n}\n\n.mr-seat-triggerEffort {\n color: var(--dsw-alias-label-caption);\n flex: none;\n}\n\n.mr-seat-chevron {\n color: var(--dsw-alias-label-caption);\n flex: none;\n transition: transform 0.12s;\n}\n\n.mr-seat-chevronOpen {\n transform: rotate(180deg);\n}\n\n.mr-seat-menu {\n z-index: 20;\n border: 1px solid var(--dsw-alias-border-inverted);\n background: var(--dsw-specific-menu);\n width: min(240px, 100vw - 32px);\n max-height: min(360px, 100vh - 96px);\n box-shadow: var(--dsw-shadow-lv3);\n color: var(--dsw-alias-label-primary);\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n border-radius: 12px;\n flex-direction: column;\n padding: 4px;\n display: flex;\n position: absolute;\n bottom: calc(100% + 8px);\n right: 0;\n overflow: hidden;\n}\n\n.mr-seat-status,\n.mr-seat-empty {\n color: var(--dsw-alias-label-tertiary);\n padding: 10px;\n font-size: 13px;\n line-height: 20px;\n}\n\n.mr-seat-error,\n.mr-seat-warning {\n background: var(--dsw-alias-interactive-bg-hover-danger);\n color: var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n justify-content: space-between;\n align-items: flex-start;\n gap: 8px;\n margin-bottom: 4px;\n padding: 7px 8px;\n font-size: 12px;\n line-height: 18px;\n display: flex;\n}\n\n.mr-seat-warning {\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-state-warn-label);\n}\n\n.mr-seat-retry {\n color: inherit;\n font: inherit;\n cursor: pointer;\n background: transparent;\n border: none;\n flex: none;\n padding: 0;\n font-weight: 600;\n}\n\n.mr-seat-groups {\n min-height: 0;\n overflow-y: auto;\n}\n\n.mr-seat-group + .mr-seat-group {\n margin-top: 4px;\n}\n\n.mr-seat-groupTitle {\n z-index: 1;\n background: var(--dsw-specific-menu);\n color: var(--dsw-alias-label-tertiary);\n padding: 5px 8px 3px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n position: sticky;\n top: 0;\n}\n\n/* Provider pane: non-sticky title row above the provider list. */\n.mr-seat-title {\n color: var(--dsw-alias-label-tertiary);\n padding: 5px 8px 3px;\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n}\n\n.mr-seat-option {\n width: 100%;\n min-height: 38px;\n color: inherit;\n text-align: left;\n cursor: pointer;\n background: transparent;\n border: none;\n border-radius: 10px;\n outline: none;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n display: flex;\n}\n\n.mr-seat-option:hover:not(:disabled),\n.mr-seat-option:focus-visible {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-seat-selected {\n background: transparent;\n}\n\n.mr-seat-option:disabled {\n color: var(--dsw-alias-label-dimmed);\n cursor: default;\n}\n\n.mr-seat-optionCopy {\n flex-direction: column;\n flex: 1;\n min-width: 0;\n display: flex;\n}\n\n.mr-seat-modelName {\n color: inherit;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 14px;\n font-weight: 500;\n line-height: 20px;\n overflow: hidden;\n}\n\n.mr-seat-description {\n color: var(--dsw-alias-label-tertiary);\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n line-height: 18px;\n overflow: hidden;\n}\n\n.mr-seat-check {\n color: var(--dsw-alias-label-primary);\n flex: 0 0 18px;\n place-items: center;\n display: grid;\n}\n\n.mr-seat-cell {\n width: 100%;\n height: 40px;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n text-align: left;\n background: transparent;\n border: none;\n border-radius: 10px;\n align-items: center;\n gap: 8px;\n padding: 0 10px;\n font-size: 14px;\n line-height: 22px;\n display: flex;\n}\n\n.mr-seat-cell:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.mr-seat-cellLabel {\n text-overflow: ellipsis;\n white-space: nowrap;\n flex: auto;\n min-width: 0;\n overflow: hidden;\n}\n\n.mr-seat-cellValue {\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n color: var(--dsw-alias-label-tertiary);\n flex: 0 auto;\n overflow: hidden;\n}\n\n.mr-seat-cellChevron {\n color: var(--dsw-alias-label-tertiary);\n flex: none;\n}\n\n/* Global hint footer of the provider pane. */\n.mr-seat-hint {\n color: var(--dsw-alias-label-tertiary);\n padding: 5px 8px 3px;\n font-size: 12px;\n line-height: 18px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin-top: 4px;\n}\n\n/*\n * Mobile portrait: the composer row is an inline-size container. With the\n * quick route switcher gone from the right tool row, let the model seat\n * absorb the remaining row width (same rule the old switcher enforced).\n * The breakpoint matches DSH's own narrow-composer rule (<=460px).\n */\n@container (width <= 460px) {\n [data-slot=\"conversation.input.model\"] > * {\n flex: 1 1 0;\n min-width: 0;\n max-width: 100%;\n }\n\n [data-slot=\"conversation.input.model\"] [aria-haspopup=\"menu\"] {\n width: 100%;\n max-width: 100%;\n }\n}\n";
|
|
699
|
+
const tagId = "@fonlan/dsh-model-router/model-seat.css";
|
|
449
700
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
450
701
|
const tag = document.createElement("style");
|
|
451
702
|
tag.dataset.plugin = "@fonlan/dsh-model-router";
|
|
@@ -454,32 +705,50 @@ window.__ModuleLoader__.load({
|
|
|
454
705
|
document.head.appendChild(tag);
|
|
455
706
|
}
|
|
456
707
|
//#endregion
|
|
457
|
-
//#region src/client/
|
|
708
|
+
//#region src/client/model-seat.tsx
|
|
458
709
|
const ROUTER_PROVIDER_ID = "model-router";
|
|
459
710
|
/**
|
|
460
|
-
* The
|
|
461
|
-
* It
|
|
462
|
-
*
|
|
711
|
+
* The composer model seat, shadowing the host ModelSelect at a lower slot
|
|
712
|
+
* priority. It renders the same two-level Model / Effort menu as the host
|
|
713
|
+
* (model list grouped by provider, reasoning effort levels), plus a third
|
|
714
|
+
* Provider row that opens the model-router provider list — the former quick
|
|
715
|
+
* route-switcher button moved into the model picker, on par with the model
|
|
716
|
+
* and effort menus.
|
|
717
|
+
*
|
|
718
|
+
* Shadowing contract: the host ui-model-selection registers the
|
|
719
|
+
* `conversation.input.model` occupant at priority 0; this entry registers at
|
|
720
|
+
* priority -1, so the renderer projects this component as the single-slot
|
|
721
|
+
* winner while the host entry stays as the abdication fallback. Both entries
|
|
722
|
+
* share the same per-session ModelDirectory, so selections stay consistent
|
|
723
|
+
* with the /model popup and the host picker.
|
|
463
724
|
*/
|
|
464
|
-
function
|
|
725
|
+
function ModelRouterSeat({ locked, available, directory, load, select, t }) {
|
|
465
726
|
const directoryState = (0, react.useSyncExternalStore)((onChange) => directory.subscribe(onChange), () => directory.getSnapshot(), () => directory.getSnapshot());
|
|
466
727
|
const current = directoryState.current;
|
|
728
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
729
|
+
const [pane, setPane] = (0, react.useState)("root");
|
|
730
|
+
const lastActionRef = (0, react.useRef)("load");
|
|
731
|
+
const [toast, setToast] = (0, react.useState)(null);
|
|
732
|
+
const toastSeq = (0, react.useRef)(0);
|
|
733
|
+
const rootRef = (0, react.useRef)(null);
|
|
734
|
+
const triggerRef = (0, react.useRef)(null);
|
|
735
|
+
const itemRefs = (0, react.useRef)([]);
|
|
736
|
+
const id = (0, react.useId)();
|
|
467
737
|
const [routerState, setRouterState] = (0, react.useState)(null);
|
|
468
738
|
const [routerLoading, setRouterLoading] = (0, react.useState)(false);
|
|
469
739
|
const [routerError, setRouterError] = (0, react.useState)(null);
|
|
470
|
-
const [open, setOpen] = (0, react.useState)(false);
|
|
471
740
|
const [busyProvider, setBusyProvider] = (0, react.useState)(null);
|
|
472
741
|
const isRouterModel = available && current?.provider === ROUTER_PROVIDER_ID;
|
|
473
742
|
const modelId = isRouterModel ? current.model : null;
|
|
474
|
-
const loadRouterState = (0, react.useCallback)(async () => {
|
|
475
|
-
setRouterLoading(true);
|
|
743
|
+
const loadRouterState = (0, react.useCallback)(async (silent = false) => {
|
|
744
|
+
if (!silent) setRouterLoading(true);
|
|
476
745
|
try {
|
|
477
746
|
setRouterState(await api.state());
|
|
478
747
|
setRouterError(null);
|
|
479
748
|
} catch (cause) {
|
|
480
749
|
setRouterError(cause instanceof Error ? cause.message : String(cause));
|
|
481
750
|
} finally {
|
|
482
|
-
setRouterLoading(false);
|
|
751
|
+
if (!silent) setRouterLoading(false);
|
|
483
752
|
}
|
|
484
753
|
}, []);
|
|
485
754
|
(0, react.useEffect)(() => {
|
|
@@ -489,16 +758,24 @@ window.__ModuleLoader__.load({
|
|
|
489
758
|
(0, react.useEffect)(() => {
|
|
490
759
|
if (!available) return;
|
|
491
760
|
loadRouterState();
|
|
492
|
-
}, [available]);
|
|
761
|
+
}, [available, loadRouterState]);
|
|
493
762
|
(0, react.useEffect)(() => {
|
|
494
|
-
|
|
763
|
+
setPane("root");
|
|
495
764
|
setBusyProvider(null);
|
|
496
|
-
setRouterState(null);
|
|
497
765
|
setRouterError(null);
|
|
498
766
|
}, [modelId]);
|
|
767
|
+
const refreshTimer = (0, react.useRef)(null);
|
|
499
768
|
(0, react.useEffect)(() => {
|
|
500
769
|
if (!isRouterModel || modelId === null || directoryState.status !== "ready") return;
|
|
501
|
-
|
|
770
|
+
if (refreshTimer.current !== null) clearTimeout(refreshTimer.current);
|
|
771
|
+
refreshTimer.current = setTimeout(() => {
|
|
772
|
+
refreshTimer.current = null;
|
|
773
|
+
loadRouterState(true);
|
|
774
|
+
}, 300);
|
|
775
|
+
return () => {
|
|
776
|
+
if (refreshTimer.current !== null) clearTimeout(refreshTimer.current);
|
|
777
|
+
refreshTimer.current = null;
|
|
778
|
+
};
|
|
502
779
|
}, [
|
|
503
780
|
directoryState.status,
|
|
504
781
|
isRouterModel,
|
|
@@ -536,106 +813,429 @@ window.__ModuleLoader__.load({
|
|
|
536
813
|
routeModel,
|
|
537
814
|
t
|
|
538
815
|
]);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
816
|
+
const choices = (0, react.useMemo)(() => directoryState.groups.flatMap((group) => group.models.map((model) => ({
|
|
817
|
+
group,
|
|
818
|
+
model,
|
|
819
|
+
selection: {
|
|
820
|
+
provider: group.id,
|
|
821
|
+
model: model.id,
|
|
822
|
+
...model.reasoning?.defaultEffort === void 0 ? {} : { reasoningEffort: model.reasoning.defaultEffort }
|
|
823
|
+
}
|
|
824
|
+
}))), [directoryState.groups]);
|
|
825
|
+
const currentChoice = choices[current === null ? -1 : choices.findIndex((c) => c.selection.provider === current?.provider && c.selection.model === current.model)];
|
|
826
|
+
const reasoning = currentChoice?.model.reasoning;
|
|
827
|
+
const effectiveEffort = current?.reasoningEffort ?? reasoning?.defaultEffort;
|
|
828
|
+
const effortLabel = reasoning === void 0 ? void 0 : effectiveEffort === void 0 ? t("effortProviderDefault") : reasoning.efforts.find((level) => level.id === effectiveEffort)?.name ?? effectiveEffort;
|
|
829
|
+
const effortChoices = (0, react.useMemo)(() => reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
|
|
830
|
+
key: "provider-default",
|
|
831
|
+
effort: void 0,
|
|
832
|
+
label: t("effortProviderDefault")
|
|
833
|
+
}] : [], ...reasoning.efforts.map((effort) => ({
|
|
834
|
+
key: `effort:${effort.id}`,
|
|
835
|
+
effort: effort.id,
|
|
836
|
+
label: effort.name,
|
|
837
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
838
|
+
}))], [reasoning, t]);
|
|
839
|
+
const busy = directoryState.status === "selecting";
|
|
840
|
+
const reload = () => {
|
|
841
|
+
lastActionRef.current = "load";
|
|
842
|
+
load();
|
|
843
|
+
};
|
|
844
|
+
(0, react.useEffect)(() => {
|
|
845
|
+
if (!open) return;
|
|
846
|
+
const closeOutside = (event) => {
|
|
847
|
+
if (!rootRef.current?.contains(event.target)) setOpen(false);
|
|
848
|
+
};
|
|
849
|
+
document.addEventListener("mousedown", closeOutside);
|
|
850
|
+
return () => {
|
|
851
|
+
document.removeEventListener("mousedown", closeOutside);
|
|
852
|
+
};
|
|
853
|
+
}, [open]);
|
|
854
|
+
if (!available) return null;
|
|
855
|
+
const show = () => {
|
|
856
|
+
setPane("root");
|
|
857
|
+
setOpen(true);
|
|
858
|
+
reload();
|
|
859
|
+
loadRouterState();
|
|
860
|
+
};
|
|
861
|
+
const close = (restoreFocus = false) => {
|
|
862
|
+
setOpen(false);
|
|
863
|
+
setPane("root");
|
|
864
|
+
if (restoreFocus) queueMicrotask(() => {
|
|
865
|
+
triggerRef.current?.focus();
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
const moveFocus = (offset) => {
|
|
869
|
+
const items = itemRefs.current.filter((item) => item !== null);
|
|
870
|
+
if (items.length === 0) return;
|
|
871
|
+
const active = items.findIndex((item) => item === document.activeElement);
|
|
872
|
+
items[(Math.max(active, 0) + offset + items.length) % items.length]?.focus();
|
|
873
|
+
};
|
|
874
|
+
const onRootKeyDown = (event) => {
|
|
875
|
+
if (event.key === "Escape" && open) {
|
|
876
|
+
event.preventDefault();
|
|
877
|
+
if (pane !== "root") setPane("root");
|
|
878
|
+
else close(true);
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
if (!open) return;
|
|
882
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
883
|
+
event.preventDefault();
|
|
884
|
+
moveFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
const onBlur = (event) => {
|
|
888
|
+
if (event.relatedTarget instanceof Node && rootRef.current?.contains(event.relatedTarget)) return;
|
|
889
|
+
close();
|
|
890
|
+
};
|
|
891
|
+
const settleSelection = (accepted) => {
|
|
892
|
+
if (accepted) {
|
|
893
|
+
if (rootRef.current !== null) close(true);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const message = directory.getSnapshot().error;
|
|
897
|
+
if (message !== null) {
|
|
898
|
+
toastSeq.current += 1;
|
|
899
|
+
setToast({
|
|
900
|
+
seq: toastSeq.current,
|
|
901
|
+
text: t("errorAction", { message })
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
const choose = (selection) => {
|
|
906
|
+
if (current !== null && current.provider === selection.provider && current.model === selection.model) {
|
|
907
|
+
close(true);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
lastActionRef.current = "select";
|
|
911
|
+
select(selection).then(settleSelection);
|
|
912
|
+
};
|
|
913
|
+
const chooseEffort = (effort) => {
|
|
914
|
+
if (current === null) return;
|
|
915
|
+
if (effectiveEffort === effort) {
|
|
916
|
+
close(true);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const selection = {
|
|
920
|
+
provider: current.provider,
|
|
921
|
+
model: current.model,
|
|
922
|
+
...effort === void 0 ? {} : { reasoningEffort: effort }
|
|
923
|
+
};
|
|
924
|
+
lastActionRef.current = "select";
|
|
925
|
+
select(selection).then(settleSelection);
|
|
926
|
+
};
|
|
927
|
+
const modelLabel = currentChoice?.model.name ?? t("triggerFallback");
|
|
928
|
+
const triggerLabel = effortLabel === void 0 ? modelLabel : `${modelLabel} · ${effortLabel}`;
|
|
929
|
+
const triggerAria = currentChoice === void 0 ? t("triggerSelectAria") : effortLabel === void 0 ? t("triggerAria", { model: modelLabel }) : t("triggerAriaEffort", {
|
|
930
|
+
model: modelLabel,
|
|
931
|
+
effort: effortLabel
|
|
932
|
+
});
|
|
933
|
+
const activeProviderName = providers.find((provider) => provider.id === routeModel?.active)?.name ?? routeModel?.active ?? t("routeLoading");
|
|
934
|
+
const providerRowVisible = isRouterModel && routerState !== null && routerState.showQuickSwitch !== false && routerState.providers.length >= 2 && routeModel !== void 0 && providers.length >= 2;
|
|
935
|
+
itemRefs.current = [];
|
|
936
|
+
let itemIndex = 0;
|
|
937
|
+
const itemRef = () => {
|
|
938
|
+
const at = itemIndex++;
|
|
939
|
+
return (node) => {
|
|
940
|
+
itemRefs.current[at] = node;
|
|
941
|
+
};
|
|
942
|
+
};
|
|
943
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
944
|
+
ref: rootRef,
|
|
945
|
+
className: "mr-seat-root",
|
|
946
|
+
onKeyDown: onRootKeyDown,
|
|
947
|
+
onBlur,
|
|
948
|
+
children: [
|
|
949
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
950
|
+
ref: triggerRef,
|
|
951
|
+
type: "button",
|
|
952
|
+
className: "mr-seat-trigger",
|
|
953
|
+
"aria-label": triggerAria,
|
|
954
|
+
"aria-haspopup": "menu",
|
|
955
|
+
"aria-expanded": open,
|
|
956
|
+
"aria-controls": open ? `${id}-menu` : void 0,
|
|
957
|
+
title: triggerLabel,
|
|
958
|
+
disabled: locked,
|
|
959
|
+
onClick: () => {
|
|
960
|
+
if (open) close();
|
|
961
|
+
else show();
|
|
962
|
+
},
|
|
963
|
+
children: [
|
|
964
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
965
|
+
className: "mr-seat-triggerLabel",
|
|
966
|
+
children: modelLabel
|
|
967
|
+
}),
|
|
968
|
+
effortLabel !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
969
|
+
className: "mr-seat-triggerEffort",
|
|
970
|
+
children: effortLabel
|
|
971
|
+
}),
|
|
972
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
973
|
+
className: `mr-seat-chevron${open ? " mr-seat-chevronOpen" : ""}`,
|
|
974
|
+
"aria-hidden": true,
|
|
975
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
976
|
+
})
|
|
977
|
+
]
|
|
564
978
|
}),
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
979
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
980
|
+
id: `${id}-menu`,
|
|
981
|
+
className: "mr-seat-menu",
|
|
982
|
+
role: "menu",
|
|
983
|
+
"aria-label": t("menuAria"),
|
|
984
|
+
"aria-busy": directoryState.status === "loading" || busy,
|
|
985
|
+
children: [
|
|
986
|
+
pane === "root" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
987
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
988
|
+
ref: itemRef(),
|
|
989
|
+
type: "button",
|
|
990
|
+
role: "menuitem",
|
|
991
|
+
className: "mr-seat-cell",
|
|
992
|
+
onClick: () => setPane("model"),
|
|
993
|
+
children: [
|
|
994
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
995
|
+
className: "mr-seat-cellLabel",
|
|
996
|
+
children: t("menuModel")
|
|
997
|
+
}),
|
|
998
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
999
|
+
className: "mr-seat-cellValue",
|
|
1000
|
+
children: modelLabel
|
|
1001
|
+
}),
|
|
1002
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1003
|
+
className: "mr-seat-cellChevron",
|
|
1004
|
+
"aria-hidden": true,
|
|
1005
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
|
|
1006
|
+
})
|
|
1007
|
+
]
|
|
1008
|
+
}),
|
|
1009
|
+
providerRowVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1010
|
+
ref: itemRef(),
|
|
1011
|
+
type: "button",
|
|
1012
|
+
role: "menuitem",
|
|
1013
|
+
className: "mr-seat-cell",
|
|
1014
|
+
onClick: () => setPane("provider"),
|
|
1015
|
+
children: [
|
|
1016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1017
|
+
className: "mr-seat-cellLabel",
|
|
1018
|
+
children: t("menuProvider")
|
|
1019
|
+
}),
|
|
1020
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1021
|
+
className: "mr-seat-cellValue",
|
|
1022
|
+
children: activeProviderName
|
|
1023
|
+
}),
|
|
1024
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1025
|
+
className: "mr-seat-cellChevron",
|
|
1026
|
+
"aria-hidden": true,
|
|
1027
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
|
|
1028
|
+
})
|
|
1029
|
+
]
|
|
1030
|
+
}),
|
|
1031
|
+
reasoning !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1032
|
+
ref: itemRef(),
|
|
1033
|
+
type: "button",
|
|
1034
|
+
role: "menuitem",
|
|
1035
|
+
className: "mr-seat-cell",
|
|
1036
|
+
onClick: () => setPane("effort"),
|
|
1037
|
+
children: [
|
|
1038
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1039
|
+
className: "mr-seat-cellLabel",
|
|
1040
|
+
children: t("menuEffort")
|
|
1041
|
+
}),
|
|
1042
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1043
|
+
className: "mr-seat-cellValue",
|
|
1044
|
+
children: effortLabel
|
|
1045
|
+
}),
|
|
1046
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1047
|
+
className: "mr-seat-cellChevron",
|
|
1048
|
+
"aria-hidden": true,
|
|
1049
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutline14, {})
|
|
1050
|
+
})
|
|
1051
|
+
]
|
|
1052
|
+
})
|
|
1053
|
+
] }),
|
|
1054
|
+
pane === "model" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1055
|
+
directoryState.status === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1056
|
+
className: "mr-seat-status",
|
|
1057
|
+
children: t("statusLoading")
|
|
1058
|
+
}),
|
|
1059
|
+
directoryState.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1060
|
+
className: "mr-seat-error",
|
|
1061
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("errorAction", { message: directoryState.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1062
|
+
type: "button",
|
|
1063
|
+
className: "mr-seat-retry",
|
|
1064
|
+
onClick: reload,
|
|
1065
|
+
children: t("retry")
|
|
1066
|
+
})]
|
|
1067
|
+
}),
|
|
1068
|
+
directoryState.failures.map((failure) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1069
|
+
className: "mr-seat-warning",
|
|
1070
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("warningGroupLoad", {
|
|
1071
|
+
name: failure.name,
|
|
1072
|
+
message: failure.message
|
|
1073
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1074
|
+
type: "button",
|
|
1075
|
+
className: "mr-seat-retry",
|
|
1076
|
+
onClick: reload,
|
|
1077
|
+
children: t("retry")
|
|
1078
|
+
})]
|
|
1079
|
+
}, failure.id)),
|
|
1080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1081
|
+
className: "mr-seat-groups",
|
|
1082
|
+
children: directoryState.groups.map((group) => {
|
|
1083
|
+
const headingId = `${id}-${group.id}`;
|
|
1084
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1085
|
+
role: "group",
|
|
1086
|
+
"aria-labelledby": headingId,
|
|
1087
|
+
className: "mr-seat-group",
|
|
1088
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1089
|
+
className: "mr-seat-groupTitle",
|
|
1090
|
+
id: headingId,
|
|
1091
|
+
children: group.name
|
|
1092
|
+
}), group.models.map((model) => {
|
|
1093
|
+
const selected = current?.provider === group.id && current.model === model.id;
|
|
1094
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1095
|
+
ref: itemRef(),
|
|
1096
|
+
type: "button",
|
|
1097
|
+
role: "menuitemradio",
|
|
1098
|
+
"aria-checked": selected,
|
|
1099
|
+
className: `mr-seat-option${selected ? " mr-seat-selected" : ""}`,
|
|
1100
|
+
title: model.name,
|
|
1101
|
+
disabled: busy,
|
|
1102
|
+
onClick: () => choose({
|
|
1103
|
+
provider: group.id,
|
|
1104
|
+
model: model.id
|
|
1105
|
+
}),
|
|
1106
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1107
|
+
className: "mr-seat-optionCopy",
|
|
1108
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1109
|
+
className: "mr-seat-modelName",
|
|
1110
|
+
children: model.name
|
|
1111
|
+
}), model.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1112
|
+
className: "mr-seat-description",
|
|
1113
|
+
children: model.description
|
|
1114
|
+
})]
|
|
1115
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1116
|
+
className: "mr-seat-check",
|
|
1117
|
+
"aria-hidden": true,
|
|
1118
|
+
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
|
|
1119
|
+
})]
|
|
1120
|
+
}, model.id);
|
|
1121
|
+
})]
|
|
1122
|
+
}, group.id);
|
|
1123
|
+
})
|
|
1124
|
+
}),
|
|
1125
|
+
directoryState.status === "ready" && choices.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1126
|
+
className: "mr-seat-empty",
|
|
1127
|
+
children: t("emptyModels")
|
|
1128
|
+
})
|
|
1129
|
+
] }),
|
|
1130
|
+
pane === "provider" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [routerError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1131
|
+
className: "mr-seat-error",
|
|
1132
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: routerError }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1133
|
+
type: "button",
|
|
1134
|
+
className: "mr-seat-retry",
|
|
1135
|
+
onClick: () => {
|
|
1136
|
+
loadRouterState();
|
|
1137
|
+
load();
|
|
1138
|
+
},
|
|
1139
|
+
children: t("retry")
|
|
1140
|
+
})]
|
|
1141
|
+
}), routeModel === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1142
|
+
className: "mr-seat-status",
|
|
1143
|
+
children: t("routeLoading")
|
|
1144
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1146
|
+
className: "mr-seat-title",
|
|
1147
|
+
children: t("routeMenuTitle", { model: modelId ?? "" })
|
|
1148
|
+
}),
|
|
1149
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1150
|
+
className: "mr-seat-groups",
|
|
1151
|
+
children: providers.map((provider) => {
|
|
1152
|
+
const selected = routeModel.active === provider.id;
|
|
1153
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1154
|
+
ref: itemRef(),
|
|
1155
|
+
type: "button",
|
|
1156
|
+
role: "menuitemradio",
|
|
1157
|
+
"aria-checked": selected,
|
|
1158
|
+
className: `mr-seat-option${selected ? " mr-seat-selected" : ""}`,
|
|
1159
|
+
disabled: !provider.credentialConfigured || busyProvider !== null,
|
|
1160
|
+
onClick: () => void switchProvider(provider.id),
|
|
1161
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1162
|
+
className: "mr-seat-optionCopy",
|
|
1163
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1164
|
+
className: "mr-seat-modelName",
|
|
1165
|
+
children: provider.name
|
|
1166
|
+
}), !provider.credentialConfigured && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1167
|
+
className: "mr-seat-description",
|
|
1168
|
+
children: t("noCredentialShort")
|
|
1169
|
+
})]
|
|
1170
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1171
|
+
className: "mr-seat-check",
|
|
1172
|
+
"aria-hidden": true,
|
|
1173
|
+
children: selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
|
|
1174
|
+
})]
|
|
1175
|
+
}, provider.id);
|
|
1176
|
+
})
|
|
1177
|
+
}),
|
|
1178
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1179
|
+
className: "mr-seat-hint",
|
|
1180
|
+
children: t("globalRouteHint")
|
|
1181
|
+
})
|
|
1182
|
+
] })] }),
|
|
1183
|
+
pane === "effort" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [directoryState.error !== null && lastActionRef.current === "load" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1184
|
+
className: "mr-seat-error",
|
|
1185
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("errorAction", { message: directoryState.error }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1186
|
+
type: "button",
|
|
1187
|
+
className: "mr-seat-retry",
|
|
1188
|
+
onClick: reload,
|
|
1189
|
+
children: t("actionReload")
|
|
1190
|
+
})]
|
|
1191
|
+
}), effortChoices.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1192
|
+
className: "mr-seat-empty",
|
|
1193
|
+
children: t("emptyEfforts")
|
|
1194
|
+
}) : effortChoices.map((level) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1195
|
+
ref: itemRef(),
|
|
1196
|
+
type: "button",
|
|
1197
|
+
role: "menuitemradio",
|
|
1198
|
+
"aria-checked": effectiveEffort === level.effort,
|
|
1199
|
+
className: `mr-seat-option${effectiveEffort === level.effort ? " mr-seat-selected" : ""}`,
|
|
1200
|
+
disabled: busy,
|
|
1201
|
+
onClick: () => chooseEffort(level.effort),
|
|
1202
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1203
|
+
className: "mr-seat-optionCopy",
|
|
1204
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1205
|
+
className: "mr-seat-modelName",
|
|
1206
|
+
children: level.label
|
|
1207
|
+
}), level.description !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1208
|
+
className: "mr-seat-description",
|
|
1209
|
+
children: level.description
|
|
1210
|
+
})]
|
|
1211
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1212
|
+
className: "mr-seat-check",
|
|
1213
|
+
"aria-hidden": true,
|
|
1214
|
+
children: effectiveEffort === level.effort ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline16, {}) : null
|
|
1215
|
+
})]
|
|
1216
|
+
}, level.key))] })
|
|
1217
|
+
]
|
|
1218
|
+
}),
|
|
1219
|
+
toast !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
|
|
1220
|
+
text: toast.text,
|
|
1221
|
+
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutline16, {}),
|
|
1222
|
+
anchor: rootRef.current?.closest("[data-composer-card]") ?? null,
|
|
1223
|
+
onDone: () => setToast(null)
|
|
1224
|
+
}, toast.seq)
|
|
1225
|
+
]
|
|
626
1226
|
});
|
|
627
1227
|
}
|
|
628
|
-
/** Register the
|
|
629
|
-
function
|
|
1228
|
+
/** Register the model seat onto the composer's named model slot (priority -1 shadows the host seat). */
|
|
1229
|
+
function registerModelRouterSeat(ctx) {
|
|
630
1230
|
const t = ctx.locale.bind(LOCALE_NS);
|
|
631
1231
|
ctx.inject(["slots", "modelDirectories"], (scope) => {
|
|
632
1232
|
const models = scope.modelDirectories;
|
|
633
1233
|
const sessions = scope.sessions;
|
|
634
|
-
scope.slots.inject("conversation.input.
|
|
635
|
-
name: "conversation.input.
|
|
636
|
-
id: "model-router-
|
|
637
|
-
|
|
638
|
-
label: () => t("
|
|
1234
|
+
scope.slots.inject("conversation.input.model", () => scope.slots.register({
|
|
1235
|
+
name: "conversation.input.model",
|
|
1236
|
+
id: "model-router-model-seat",
|
|
1237
|
+
priority: -1,
|
|
1238
|
+
label: () => t("seatLabel"),
|
|
639
1239
|
locale: LOCALE_NS,
|
|
640
1240
|
inject: (sessionId) => {
|
|
641
1241
|
const directory = models.directoryFor(sessionId);
|
|
@@ -645,24 +1245,29 @@ window.__ModuleLoader__.load({
|
|
|
645
1245
|
directory: directory.store,
|
|
646
1246
|
load: () => {
|
|
647
1247
|
if (available) directory.load().catch(() => {});
|
|
648
|
-
}
|
|
1248
|
+
},
|
|
1249
|
+
select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false)
|
|
649
1250
|
};
|
|
650
1251
|
}
|
|
651
|
-
},
|
|
1252
|
+
}, ModelRouterSeat));
|
|
652
1253
|
});
|
|
653
1254
|
}
|
|
654
1255
|
//#endregion
|
|
655
1256
|
//#region src/client/index.tsx
|
|
1257
|
+
/** The settings namespace this card edits (must match the host half). */
|
|
1258
|
+
const ROUTER_NS = "model-router";
|
|
656
1259
|
/** Services required before mounting (provided by the client runtime). */
|
|
657
1260
|
const inject = [
|
|
658
1261
|
"slots",
|
|
659
1262
|
"locale",
|
|
660
1263
|
"modelDirectories",
|
|
661
|
-
"sessions"
|
|
1264
|
+
"sessions",
|
|
1265
|
+
"settingsScope",
|
|
1266
|
+
"connection",
|
|
1267
|
+
"remote"
|
|
662
1268
|
];
|
|
663
1269
|
/** Client plugin body. */
|
|
664
1270
|
function apply(ctx) {
|
|
665
|
-
const t = ctx.locale.bind(LOCALE_NS);
|
|
666
1271
|
ctx.effect(() => {
|
|
667
1272
|
const off = ctx.locale.register(LOCALE_NS, {
|
|
668
1273
|
zh,
|
|
@@ -670,15 +1275,15 @@ window.__ModuleLoader__.load({
|
|
|
670
1275
|
});
|
|
671
1276
|
return () => off();
|
|
672
1277
|
}, "model-router: dictionaries");
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
},
|
|
681
|
-
|
|
1278
|
+
const SettingsCard = makeSettingsCard(ctx);
|
|
1279
|
+
const services = ctx;
|
|
1280
|
+
const scope = services.settingsScope.bind({ namespace: ROUTER_NS });
|
|
1281
|
+
services.slots.inject("settings.plugin.item", () => services.slots.register({
|
|
1282
|
+
name: "settings.plugin.item",
|
|
1283
|
+
key: ROUTER_NS,
|
|
1284
|
+
inject: () => ({ scope })
|
|
1285
|
+
}, SettingsCard));
|
|
1286
|
+
registerModelRouterSeat(ctx);
|
|
682
1287
|
}
|
|
683
1288
|
//#endregion
|
|
684
1289
|
exports.apply = apply;
|