@nimoxie/dsh-model-organizer 0.5.11 → 0.5.19
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/DEVELOPMENT.md +71 -55
- package/README.md +97 -93
- package/lib/client.js +436 -126
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -100,7 +100,7 @@ window.__ModuleLoader__.load({
|
|
|
100
100
|
"page.capacityInvalid": "容量要写正数,可以用 K / M 后缀(如 256K、1M)。",
|
|
101
101
|
"page.fetchModels": "获取可用模型",
|
|
102
102
|
"page.fetching": "获取中…",
|
|
103
|
-
"page.fetchNone": "
|
|
103
|
+
"page.fetchNone": "这个路由的模型已经都在列表里了。",
|
|
104
104
|
"page.fetchTitle": "选择要添加的模型",
|
|
105
105
|
"page.fetchDescription": "以下是模型提供商的可用模型,勾选要添加的模型。",
|
|
106
106
|
"page.fetchSearch": "搜索模型",
|
|
@@ -126,26 +126,34 @@ window.__ModuleLoader__.load({
|
|
|
126
126
|
"page.keyOptional": "API 密钥(可留空,稍后在右栏填写)",
|
|
127
127
|
"page.modelsField": "模型 ID(至少一个)",
|
|
128
128
|
"page.modelsFieldPlaceholder": "例如 glm-4.6, qwen-max(多个用逗号或空格分隔)",
|
|
129
|
-
"page.modelsFieldHint": "这个路由不在 DSH 的目录里,配置里必须先列出模型,否则它会解析不出模型。填好 API
|
|
129
|
+
"page.modelsFieldHint": "这个路由不在 DSH 的目录里,配置里必须先列出模型,否则它会解析不出模型。填好 API 地址与密钥后可以点「获取可用模型」从网关取一份清单勾选,也可以直接按 ID 添加。",
|
|
130
130
|
"page.modelsRequired": "至少要填一个模型 ID:这个路由不在 DSH 的目录里,没有模型它无法解析。",
|
|
131
|
-
"page.
|
|
132
|
-
"page.
|
|
131
|
+
"page.fetchAvailable": "获取可用模型",
|
|
132
|
+
"page.modelAddById": "添加",
|
|
133
|
+
"page.modelListEmpty": "还没有模型:按 ID 输入后点「添加」,或点「获取可用模型」从网关取一份清单勾选。",
|
|
134
|
+
"page.addModeLabel": "添加方式",
|
|
135
|
+
"page.addModeCatalog": "第三方模型提供商",
|
|
136
|
+
"page.addModeCustom": "自定义模型 API",
|
|
137
|
+
"page.providerPreset": "提供商",
|
|
138
|
+
"page.presetPlaceholder": "选择内置目录里的提供商…",
|
|
139
|
+
"page.presetHint": "地址、协议和模型都由 DSH 内置目录提供,这里只需要密钥;添加后模型列表会显示目录里的模型(只读)。",
|
|
140
|
+
"page.presetRequired": "先选择一个提供商。",
|
|
141
|
+
"page.noPresets": "内置目录里的提供商都已经添加过了。",
|
|
142
|
+
"page.inheritedModels": "这些模型来自 DSH 内置目录(这条路由没有在配置里列出模型),只能看和测试,不能在这里改。",
|
|
143
|
+
"page.baseUrlRequired": "「获取可用模型」需要先填 API 地址。",
|
|
133
144
|
"page.deleteProvider": "删除供应商",
|
|
134
145
|
"page.deleteProviderHint": "删除这条自建路由:它的配置和本页派生的密钥引用会一起移除,无法撤销。官方路由不能在这里删除。",
|
|
135
146
|
"page.deleteProviderBody": "删除后无法撤销:这条供应商的配置会被移除,本页为它派生的密钥引用也会一并清理(别处配置的密钥不会被删)。",
|
|
136
147
|
"page.deleteFailed": "删除失败。",
|
|
137
148
|
"page.test": "测试",
|
|
138
149
|
"page.testing": "测试中…",
|
|
139
|
-
"page.
|
|
140
|
-
"page.
|
|
141
|
-
"page.
|
|
142
|
-
"page.testHint": "让宿主向该供应商询问模型列表,并检查这个模型 ID 是否在其中。自建路由会真的访问网关(用已保存的密钥),目录已描述的路由直接读目录。",
|
|
143
|
-
"page.testOk": "网关可达,且它返回的模型列表里有这个 ID。",
|
|
144
|
-
"page.testMissing": "网关可达,但它返回的模型列表里没有这个 ID(可能拼错,或网关没暴露该模型)。",
|
|
150
|
+
"page.testHint": "测一下这个模型现在能不能用",
|
|
151
|
+
"page.testOk": "可用",
|
|
152
|
+
"page.testMissing": "网关可达,但列表里没有这个 ID",
|
|
145
153
|
"page.testFailed": "测试失败。",
|
|
146
|
-
"page.testNoCatalog": "
|
|
154
|
+
"page.testNoCatalog": "没有 API 地址,无法测试",
|
|
147
155
|
"page.deleteModel": "删除模型",
|
|
148
|
-
"page.testNeedsId": "先填模型 ID
|
|
156
|
+
"page.testNeedsId": "先填模型 ID",
|
|
149
157
|
"page.deleteModelHint": "从配置里删掉这个模型(每条路由至少要留一个)",
|
|
150
158
|
"page.modelDeleted": "模型已删除",
|
|
151
159
|
"page.openOfficial": "官方模型页",
|
|
@@ -286,26 +294,34 @@ window.__ModuleLoader__.load({
|
|
|
286
294
|
"page.keyOptional": "API key (leave empty, fill it on the right later)",
|
|
287
295
|
"page.modelsField": "Model IDs (at least one)",
|
|
288
296
|
"page.modelsFieldPlaceholder": "e.g. glm-4.6, qwen-max (comma or space separated)",
|
|
289
|
-
"page.modelsFieldHint": "DSH's catalog does not describe this route, so the config must list its models — otherwise the route resolves no models. Fill the API address and key, then use Fetch
|
|
297
|
+
"page.modelsFieldHint": "DSH's catalog does not describe this route, so the config must list its models — otherwise the route resolves no models. Fill the API address and key, then use Fetch available models, or add ids by hand.",
|
|
290
298
|
"page.modelsRequired": "List at least one model ID: the catalog does not describe this route, so it cannot resolve without models.",
|
|
291
|
-
"page.
|
|
292
|
-
"page.
|
|
299
|
+
"page.fetchAvailable": "Fetch available models",
|
|
300
|
+
"page.modelAddById": "Add",
|
|
301
|
+
"page.modelListEmpty": "No models yet: type an ID and press Add, or use Fetch available models to pick from the gateway.",
|
|
302
|
+
"page.addModeLabel": "Add mode",
|
|
303
|
+
"page.addModeCatalog": "Third-party provider",
|
|
304
|
+
"page.addModeCustom": "Custom model API",
|
|
305
|
+
"page.providerPreset": "Provider",
|
|
306
|
+
"page.presetPlaceholder": "Pick a provider from the built-in catalog…",
|
|
307
|
+
"page.presetHint": "The address, protocol and models come from DSH's built-in catalog — only the key is needed. After adding, its models appear in the list (read-only).",
|
|
308
|
+
"page.presetRequired": "Pick a provider first.",
|
|
309
|
+
"page.noPresets": "Every provider in the built-in catalog is already added.",
|
|
310
|
+
"page.inheritedModels": "These models come from DSH's built-in catalog (this route lists none in the configuration); they can be viewed and tested here, not edited.",
|
|
311
|
+
"page.baseUrlRequired": "Fetch available models needs the API address first.",
|
|
293
312
|
"page.deleteProvider": "Delete provider",
|
|
294
313
|
"page.deleteProviderHint": "Delete this hand-declared route: its profile and the credential reference this page derived go with it, and it cannot be undone. Built-in routes cannot be deleted here.",
|
|
295
314
|
"page.deleteProviderBody": "This cannot be undone: the provider's profile is removed and the credential reference this page derived is cleaned up (a key configured elsewhere is left alone).",
|
|
296
315
|
"page.deleteFailed": "Deleting failed.",
|
|
297
316
|
"page.test": "Test",
|
|
298
317
|
"page.testing": "Testing…",
|
|
299
|
-
"page.
|
|
300
|
-
"page.
|
|
301
|
-
"page.
|
|
302
|
-
"page.testHint": "Asks the host which models this provider serves and checks whether this model ID is among them. A hand-declared route really calls the gateway (with the stored key); a catalog route answers from the catalog.",
|
|
303
|
-
"page.testOk": "The gateway answered and lists this ID.",
|
|
304
|
-
"page.testMissing": "The gateway answered, but its model list does not contain this ID (typo, or the gateway does not expose it).",
|
|
318
|
+
"page.testHint": "Check whether this model works right now",
|
|
319
|
+
"page.testOk": "Works",
|
|
320
|
+
"page.testMissing": "Gateway reachable, but this ID is not listed",
|
|
305
321
|
"page.testFailed": "The test failed.",
|
|
306
|
-
"page.testNoCatalog": "
|
|
322
|
+
"page.testNoCatalog": "No API address — cannot test",
|
|
307
323
|
"page.deleteModel": "Delete model",
|
|
308
|
-
"page.testNeedsId": "
|
|
324
|
+
"page.testNeedsId": "Enter a model ID first",
|
|
309
325
|
"page.deleteModelHint": "Remove this model from the configuration (every route keeps at least one)",
|
|
310
326
|
"page.modelDeleted": "Model deleted",
|
|
311
327
|
"page.openOfficial": "Official Models page",
|
|
@@ -377,7 +393,7 @@ window.__ModuleLoader__.load({
|
|
|
377
393
|
code changes, so a browser can keep running an old build until a hard reload.
|
|
378
394
|
Kept OFF the UI (it is not user-facing): read it as
|
|
379
395
|
`window.__dshModelOrganizerBuild` when a stale build is suspected. */
|
|
380
|
-
const BUILD = "
|
|
396
|
+
const BUILD = "b82";
|
|
381
397
|
|
|
382
398
|
let officialClasses = null;
|
|
383
399
|
let officialClassesSheets = -1;
|
|
@@ -470,8 +486,11 @@ window.__ModuleLoader__.load({
|
|
|
470
486
|
".dsh-mo-head:focus-visible{outline:none;background:var(--dsw-alias-interactive-bg-hover);}",
|
|
471
487
|
".dsh-mo-list .dsh-mo-head.dsh-mo-head:focus-visible,.dsh-mo-list .dsh-mo-model.dsh-mo-model:focus-visible,.dsh-mo-list .dsh-mo-row.dsh-mo-row:focus-visible{background:transparent;}",
|
|
472
488
|
".dsh-mo-panelhead.dsh-mo-panelhead:focus-visible{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
473
|
-
/*
|
|
474
|
-
currentColor, so
|
|
489
|
+
/* Icon actions (row delete / model test): the kit's icons inherit
|
|
490
|
+
currentColor, so their colour state lives in these rules. */
|
|
491
|
+
/* A native select paints its own popup; colour-scheme keeps that popup dark. */
|
|
492
|
+
".dsh-mo-select{background:var(--dsw-alias-bg-module, rgba(127,127,127,.08));color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:7px;padding:4px 8px;font-size:12px;color-scheme:dark;}",
|
|
493
|
+
".dsh-mo-select option{background:var(--dsw-alias-bg-module, #1f1f1f);color:var(--dsw-alias-label-primary);}",
|
|
475
494
|
".dsh-mo-act{color:var(--dsw-alias-label-secondary);transition:background .12s ease,color .12s ease;}",
|
|
476
495
|
".dsh-mo-act:hover{background:var(--dsw-alias-interactive-bg-hover);}",
|
|
477
496
|
".dsh-mo-act:disabled{opacity:.45;cursor:not-allowed;background:transparent;}",
|
|
@@ -529,6 +548,35 @@ window.__ModuleLoader__.load({
|
|
|
529
548
|
".dsh-mo-model::before{content:'';position:absolute;left:15px;top:50%;width:5px;height:5px;margin-top:-2.5px;border-radius:50%;background:var(--dsw-alias-label-tertiary);opacity:.5;}",
|
|
530
549
|
".dsh-mo-model[aria-checked='true']::before{background:var(--dsw-alias-label-primary);opacity:.9;}"
|
|
531
550
|
];
|
|
551
|
+
|
|
552
|
+
/* Every button on this page, in its dialogs, and in the footer panel shares one
|
|
553
|
+
pointer state. The resting paint is inline (the S.* objects), so the hover
|
|
554
|
+
paint has to outrank it: `!important` is confined to hover/disabled, and
|
|
555
|
+
:where() keeps the scope selector at zero specificity so the variant rules
|
|
556
|
+
below still win. */
|
|
557
|
+
const BTN_SURFACES = [
|
|
558
|
+
'[data-mo="page"] button', '[data-mo="panel"] button',
|
|
559
|
+
'[data-mo="fetch-dialog"] button', '[data-mo="add-dialog"] button',
|
|
560
|
+
'[data-mo="model-dialog"] button', '[data-mo="provider-delete-dialog"] button'
|
|
561
|
+
];
|
|
562
|
+
const BTN = ":where(" + BTN_SURFACES.join(",") + ")";
|
|
563
|
+
const byMarker = function (markers) {
|
|
564
|
+
return ":where(" + markers.map(function (marker) { return "[data-mo='" + marker + "']"; }).join(",") + ")";
|
|
565
|
+
};
|
|
566
|
+
rules.push(BTN + "{transition:background .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,box-shadow .12s ease;}");
|
|
567
|
+
rules.push(BTN + ":not([role='switch']):not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.20))!important;}");
|
|
568
|
+
rules.push(BTN + ":not(:disabled):active{filter:brightness(.96);}");
|
|
569
|
+
rules.push(BTN + ":disabled{cursor:not-allowed;opacity:.55;}");
|
|
570
|
+
/* A switch paints its own on/off fill: repainting it on hover would read as a
|
|
571
|
+
state change, so it gets a ring instead. */
|
|
572
|
+
rules.push(BTN + ":is([role='switch']):not(:disabled):hover{box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-accent, #4f8cff) 40%, transparent);}");
|
|
573
|
+
/* Primary actions keep their accent fill, destructive ones tint the danger
|
|
574
|
+
colour, and the two quiet cross-links accent their frame. */
|
|
575
|
+
rules.push(byMarker(["page-key-save", "fetch-adopt", "add-submit", "model-dialog-save"]) + ":not(:disabled):hover{background:var(--dsw-alias-state-accent, #4f8cff)!important;filter:brightness(1.12);}");
|
|
576
|
+
rules.push(byMarker(["page-model-delete", "page-provider-delete", "model-dialog-delete", "provider-delete-confirm"]) + ":not(:disabled):hover{background:color-mix(in srgb, var(--dsw-alias-state-error, #f85149) 18%, transparent)!important;border-color:var(--dsw-alias-state-error, #f85149)!important;}");
|
|
577
|
+
rules.push(byMarker(["page-open-official", "page-provider-add"]) + ":not(:disabled):hover{color:var(--dsw-alias-label-primary)!important;border-color:var(--dsw-alias-state-accent, #4f8cff)!important;}");
|
|
578
|
+
rules.push(BTN + "[role='radio'][aria-checked='true']:not(:disabled):hover{filter:brightness(1.12);}");
|
|
579
|
+
rules.push(BTN + "[data-mo$='-close']:not(:disabled):hover{color:var(--dsw-alias-label-primary)!important;}");
|
|
532
580
|
/* The shipped check alone is easy to miss: bold the selected row's name
|
|
533
581
|
wherever the official sheet is in use. Settings rows carry no
|
|
534
582
|
aria-checked, so the drag lists are untouched. */
|
|
@@ -564,6 +612,30 @@ window.__ModuleLoader__.load({
|
|
|
564
612
|
return h("span", { "aria-hidden": "true", style: { fontSize: size - 3, lineHeight: 1 } }, glyph);
|
|
565
613
|
}
|
|
566
614
|
|
|
615
|
+
/* One box for the three icon actions: the provider head's delete and each model
|
|
616
|
+
row's test / delete. Height is the switch's (17) on purpose — a taller icon
|
|
617
|
+
button beside the switch is what the page screenshot called out. Width is a
|
|
618
|
+
nudge above the proportional shrink (the same screenshot then called 20 a hair
|
|
619
|
+
too narrow); the height stays equal to the switch so the row never looks taller. */
|
|
620
|
+
const ACTION_BOX = {
|
|
621
|
+
boxSizing: "border-box", flex: "none", width: 19, height: 17, borderRadius: 6,
|
|
622
|
+
/* The detail head aligns its TEXT on the baseline; an icon-only box has no real
|
|
623
|
+
text baseline, so it must opt into center alignment or it floats a pixel or two
|
|
624
|
+
above the switch (the cropped screenshot that showed them out of line). */
|
|
625
|
+
alignSelf: "center",
|
|
626
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center", padding: 0,
|
|
627
|
+
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
|
|
628
|
+
background: "transparent", cursor: "pointer"
|
|
629
|
+
};
|
|
630
|
+
/* The destructive sibling: identical geometry, danger frame. */
|
|
631
|
+
const DELETE_ACTION = Object.assign({}, ACTION_BOX, { borderColor: "var(--dsw-alias-state-error, #f85149)" });
|
|
632
|
+
/* The diagnostic button is a NORMAL button to the eye: same box as the two above,
|
|
633
|
+
but it wears the page buttons' tinted fill (S.pageButton) instead of a transparent
|
|
634
|
+
frame. Its verdict stays in the icon colour (.dsh-mo-act-ok/-miss/-danger). */
|
|
635
|
+
const TEST_ACTION = Object.assign({}, ACTION_BOX, {
|
|
636
|
+
background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.10))"
|
|
637
|
+
});
|
|
638
|
+
|
|
567
639
|
/* --------------------------------- styles --------------------------------- */
|
|
568
640
|
const S = {
|
|
569
641
|
chip: {
|
|
@@ -676,15 +748,6 @@ window.__ModuleLoader__.load({
|
|
|
676
748
|
background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.10))",
|
|
677
749
|
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)"
|
|
678
750
|
},
|
|
679
|
-
switchLabel: {
|
|
680
|
-
display: "inline-flex", alignItems: "center", gap: 6, fontSize: 11,
|
|
681
|
-
color: "var(--dsw-alias-label-secondary)", cursor: "pointer", userSelect: "none"
|
|
682
|
-
},
|
|
683
|
-
linkPillOn: {
|
|
684
|
-
color: "var(--dsw-alias-label-primary)",
|
|
685
|
-
background: "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.20))",
|
|
686
|
-
borderColor: "var(--dsw-alias-state-accent, #4f8cff)"
|
|
687
|
-
},
|
|
688
751
|
pageTitle: { margin: 0, fontSize: 16, fontWeight: 600, color: "var(--dsw-alias-label-primary)" },
|
|
689
752
|
pageSubtitle: { margin: 0, fontSize: 12, lineHeight: "18px", color: "var(--dsw-alias-label-tertiary)" },
|
|
690
753
|
pageGrid: { display: "grid", gridTemplateColumns: "minmax(170px, 210px) minmax(0, 1fr)", gap: 14, alignItems: "start" },
|
|
@@ -706,21 +769,10 @@ window.__ModuleLoader__.load({
|
|
|
706
769
|
pageDetailHead: { display: "flex", alignItems: "baseline", gap: 8, flexWrap: "wrap" },
|
|
707
770
|
pageDetailTitle: { margin: 0, fontSize: 15, fontWeight: 600 },
|
|
708
771
|
pageDetailRoute: { fontSize: 11, color: "var(--dsw-alias-label-tertiary)" },
|
|
709
|
-
pageFacts: { display: "flex", flexDirection: "column", gap: 6 },
|
|
710
|
-
pageFact: { display: "grid", gridTemplateColumns: "84px minmax(0, 1fr)", gap: 10, alignItems: "baseline", fontSize: 12 },
|
|
711
|
-
pageFactLabel: { color: "var(--dsw-alias-label-tertiary)" },
|
|
712
|
-
pageFactValue: { color: "var(--dsw-alias-label-secondary)", wordBreak: "break-all" },
|
|
713
772
|
pageSectionHead: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8 },
|
|
714
773
|
pageSectionLabel: { fontSize: 12, fontWeight: 600 },
|
|
715
774
|
pageHint: { fontSize: 10, color: "var(--dsw-alias-label-tertiary)" },
|
|
716
775
|
pageEmpty: { margin: 0, fontSize: 12, lineHeight: "18px", color: "var(--dsw-alias-label-tertiary)" },
|
|
717
|
-
pageModels: { display: "flex", flexDirection: "column", gap: 4 },
|
|
718
|
-
pageModelRow: {
|
|
719
|
-
display: "flex", alignItems: "center", gap: 8, minWidth: 0, padding: "5px 8px", borderRadius: 8,
|
|
720
|
-
fontSize: 12, background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.06))"
|
|
721
|
-
},
|
|
722
|
-
pageModelId: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
|
|
723
|
-
pageModelName: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", color: "var(--dsw-alias-label-tertiary)" },
|
|
724
776
|
pageSpacer: { flex: "1 1 auto", minWidth: 0 },
|
|
725
777
|
pageChip: { flex: "none", padding: "1px 6px", borderRadius: 6, fontSize: 10, border: "1px solid var(--dsw-alias-border-l1)", color: "var(--dsw-alias-label-tertiary)" },
|
|
726
778
|
pageLoading: { margin: 0, fontSize: 12, color: "var(--dsw-alias-label-tertiary)" },
|
|
@@ -773,8 +825,6 @@ window.__ModuleLoader__.load({
|
|
|
773
825
|
outline: "none"
|
|
774
826
|
},
|
|
775
827
|
pageInputMono: { flex: "1 1 auto", minWidth: 0, fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
|
|
776
|
-
pageModelTop: { display: "flex", alignItems: "center", gap: 6, minWidth: 0 },
|
|
777
|
-
pageModelFields: { display: "grid", gridTemplateColumns: "92px 92px auto", gap: 6, alignItems: "center", paddingLeft: 18 },
|
|
778
828
|
/* A native <select> popup is painted by the platform: in this theme its list came
|
|
779
829
|
out white-on-white. Three radio-style segments are painted by us, so the choice
|
|
780
830
|
is legible whatever the platform does. */
|
|
@@ -782,6 +832,9 @@ window.__ModuleLoader__.load({
|
|
|
782
832
|
"vision", but a switch reads as an on/off state at a glance. */
|
|
783
833
|
switchTrack: {
|
|
784
834
|
boxSizing: "border-box", flex: "none", width: 30, height: 17, padding: 2, borderRadius: 9,
|
|
835
|
+
/* Same reason as ACTION_BOX: the knob gives this flex box a baseline that is
|
|
836
|
+
NOT its box bottom, so baseline rows would offset it from the icon button. */
|
|
837
|
+
alignSelf: "center",
|
|
785
838
|
display: "inline-flex", alignItems: "center", justifyContent: "flex-start", cursor: "pointer",
|
|
786
839
|
background: "var(--dsw-alias-interactive-bg, rgba(127,127,127,.25))",
|
|
787
840
|
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
|
|
@@ -817,12 +870,6 @@ window.__ModuleLoader__.load({
|
|
|
817
870
|
background: "var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.20))",
|
|
818
871
|
borderColor: "var(--dsw-alias-state-accent, #4f8cff)"
|
|
819
872
|
},
|
|
820
|
-
pageSelect: {
|
|
821
|
-
boxSizing: "border-box", padding: "4px 8px", borderRadius: 7, fontSize: 12,
|
|
822
|
-
color: "var(--dsw-alias-label-primary)",
|
|
823
|
-
background: "var(--dsw-alias-bg-module, rgba(127,127,127,.08))",
|
|
824
|
-
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)"
|
|
825
|
-
},
|
|
826
873
|
pageButton: {
|
|
827
874
|
boxSizing: "border-box", flex: "none", padding: "4px 10px", borderRadius: 7, fontSize: 12, cursor: "pointer",
|
|
828
875
|
color: "var(--dsw-alias-label-secondary)",
|
|
@@ -837,36 +884,13 @@ window.__ModuleLoader__.load({
|
|
|
837
884
|
},
|
|
838
885
|
/* Row-level actions and the one destructive action on this page. */
|
|
839
886
|
pageModelActions: { flex: "none", display: "flex", alignItems: "center", gap: 4 },
|
|
840
|
-
/*
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
background: "transparent", cursor: "pointer"
|
|
847
|
-
},
|
|
848
|
-
pageIconActionHead: {
|
|
849
|
-
boxSizing: "border-box", flex: "none", width: 22, height: 17, borderRadius: 6,
|
|
850
|
-
display: "inline-flex", alignItems: "center", justifyContent: "center", padding: 0,
|
|
851
|
-
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
|
|
852
|
-
background: "transparent", cursor: "pointer"
|
|
853
|
-
},
|
|
887
|
+
/* The diagnostic action: same box as the trash, but it wears the normal page
|
|
888
|
+
button's tinted fill; its verdict colour rides the icon (.dsh-mo-act-*), never
|
|
889
|
+
the frame. */
|
|
890
|
+
pageIconAction: TEST_ACTION,
|
|
891
|
+
/* The one trash box; the provider head and the model rows share this object. */
|
|
892
|
+
pageIconDelete: DELETE_ACTION,
|
|
854
893
|
|
|
855
|
-
pageButtonCompact: {
|
|
856
|
-
boxSizing: "border-box", flex: "none", padding: "1px 7px", borderRadius: 6, fontSize: 10, cursor: "pointer",
|
|
857
|
-
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
|
|
858
|
-
background: "transparent", color: "var(--dsw-alias-label-secondary)"
|
|
859
|
-
},
|
|
860
|
-
pageButtonOk: {
|
|
861
|
-
boxSizing: "border-box", flex: "none", padding: "1px 7px", borderRadius: 6, fontSize: 10, cursor: "pointer",
|
|
862
|
-
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-state-success, #3fb950)",
|
|
863
|
-
background: "transparent", color: "var(--dsw-alias-state-success, #3fb950)"
|
|
864
|
-
},
|
|
865
|
-
pageButtonWarn: {
|
|
866
|
-
boxSizing: "border-box", flex: "none", padding: "1px 7px", borderRadius: 6, fontSize: 10, cursor: "pointer",
|
|
867
|
-
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-state-warn-label, #d29922)",
|
|
868
|
-
background: "transparent", color: "var(--dsw-alias-state-warn-label, #d29922)"
|
|
869
|
-
},
|
|
870
894
|
pageButtonDanger: {
|
|
871
895
|
boxSizing: "border-box", flex: "none", padding: "4px 10px", borderRadius: 7, fontSize: 12, cursor: "pointer",
|
|
872
896
|
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-state-error, #f85149)",
|
|
@@ -877,9 +901,17 @@ window.__ModuleLoader__.load({
|
|
|
877
901
|
cursor: "pointer", color: "var(--dsw-alias-label-tertiary)", background: "transparent",
|
|
878
902
|
borderWidth: 1, borderStyle: "solid", borderColor: "transparent"
|
|
879
903
|
},
|
|
904
|
+
/* One line, never wrapped: the control on the right must stay beside its input. */
|
|
905
|
+
pageInlineRow: { display: "flex", alignItems: "center", gap: 6, flexWrap: "nowrap", minWidth: 0 },
|
|
906
|
+
pageInputGrow: {
|
|
907
|
+
boxSizing: "border-box", flex: "1 1 auto", width: "auto", minWidth: 0, padding: "4px 8px", borderRadius: 7, fontSize: 12,
|
|
908
|
+
color: "var(--dsw-alias-label-primary)",
|
|
909
|
+
background: "var(--dsw-alias-bg-module, rgba(127,127,127,.08))",
|
|
910
|
+
borderWidth: 1, borderStyle: "solid", borderColor: "var(--dsw-alias-border-l1)",
|
|
911
|
+
outline: "none"
|
|
912
|
+
},
|
|
880
913
|
pageKeyRow: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
|
|
881
914
|
pageActions: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
|
|
882
|
-
pageHandle: { flex: "none", color: "var(--dsw-alias-label-tertiary)", fontSize: 12, userSelect: "none" },
|
|
883
915
|
pageCheckLabel: { flex: "none", display: "inline-flex", alignItems: "center", gap: 4, fontSize: 11, color: "var(--dsw-alias-label-tertiary)", cursor: "pointer" },
|
|
884
916
|
/* Built-in rail rows: readable, obviously not ours to edit, and never draggable
|
|
885
917
|
(they sit outside the drag list for exactly that reason). */
|
|
@@ -2784,6 +2816,11 @@ window.__ModuleLoader__.load({
|
|
|
2784
2816
|
const cardRef = useRef(null);
|
|
2785
2817
|
const restoreRef = useRef(null);
|
|
2786
2818
|
const closeRef = useRef(props.onClose);
|
|
2819
|
+
/* Every dialog is movable by its header: these panels cover the page, and you oft
|
|
2820
|
+
need to look at what is underneath while they are open. */
|
|
2821
|
+
const offsetState = useState({ x: 0, y: 0 });
|
|
2822
|
+
const offset = offsetState[0];
|
|
2823
|
+
const setOffset = offsetState[1];
|
|
2787
2824
|
closeRef.current = props.onClose;
|
|
2788
2825
|
|
|
2789
2826
|
useEffect(function () {
|
|
@@ -2878,6 +2915,31 @@ window.__ModuleLoader__.load({
|
|
|
2878
2915
|
trapTab(event, event.currentTarget);
|
|
2879
2916
|
}
|
|
2880
2917
|
|
|
2918
|
+
function onCardMouseDown(event) {
|
|
2919
|
+
if (event === null || event === undefined) return;
|
|
2920
|
+
if (event.button !== undefined && event.button !== 0) return;
|
|
2921
|
+
const node = event.target;
|
|
2922
|
+
if (node === null || node === undefined || typeof node.closest !== "function") return;
|
|
2923
|
+
/* Only the header strip drags, and never a control inside it. */
|
|
2924
|
+
if (node.closest("button, a, input, select, textarea, [data-mo-no-drag]") !== null) return;
|
|
2925
|
+
if (node.closest("[data-mo-drag]") === null) return;
|
|
2926
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
2927
|
+
const from = { x: offset.x, y: offset.y };
|
|
2928
|
+
const startX = event.clientX;
|
|
2929
|
+
const startY = event.clientY;
|
|
2930
|
+
const move = function (moveEvent) {
|
|
2931
|
+
setOffset({ x: from.x + (moveEvent.clientX - startX), y: from.y + (moveEvent.clientY - startY) });
|
|
2932
|
+
};
|
|
2933
|
+
const stop = function () {
|
|
2934
|
+
try { document.removeEventListener("mousemove", move, true); } catch (e) {}
|
|
2935
|
+
try { document.removeEventListener("mouseup", stop, true); } catch (e) {}
|
|
2936
|
+
};
|
|
2937
|
+
try {
|
|
2938
|
+
document.addEventListener("mousemove", move, true);
|
|
2939
|
+
document.addEventListener("mouseup", stop, true);
|
|
2940
|
+
} catch (e) {}
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2881
2943
|
return h("div", {
|
|
2882
2944
|
ref: cardRef,
|
|
2883
2945
|
role: "dialog",
|
|
@@ -2886,7 +2948,8 @@ window.__ModuleLoader__.load({
|
|
|
2886
2948
|
"data-mo": props.marker,
|
|
2887
2949
|
tabIndex: -1,
|
|
2888
2950
|
onKeyDown: onKeyDown,
|
|
2889
|
-
|
|
2951
|
+
onMouseDown: onCardMouseDown,
|
|
2952
|
+
style: Object.assign({}, S.pickCard, { transform: "translate(" + offset.x + "px, " + offset.y + "px)" })
|
|
2890
2953
|
}, props.children);
|
|
2891
2954
|
}
|
|
2892
2955
|
|
|
@@ -3049,6 +3112,52 @@ window.__ModuleLoader__.load({
|
|
|
3049
3112
|
return function () { live = false; };
|
|
3050
3113
|
}, [bridge.llm]);
|
|
3051
3114
|
|
|
3115
|
+
/* A route whose profile lists no models may still serve some: an adapter that
|
|
3116
|
+
ships a catalog (every built-in preset) answers discovery without a network call.
|
|
3117
|
+
Those rows are shown read-only — they live in the catalog, not in this document. */
|
|
3118
|
+
const catalogState = useState({});
|
|
3119
|
+
const catalogRows = catalogState[0];
|
|
3120
|
+
const setCatalogRows = catalogState[1];
|
|
3121
|
+
|
|
3122
|
+
useEffect(function () {
|
|
3123
|
+
const llm = bridge.llm;
|
|
3124
|
+
if (llm === undefined || llm === null || current === null) return undefined;
|
|
3125
|
+
/* An explicit models list always wins: never show catalog rows beside it. */
|
|
3126
|
+
if (storedModels.length > 0) return undefined;
|
|
3127
|
+
if (catalogRows[current] !== undefined) return undefined;
|
|
3128
|
+
let live = true;
|
|
3129
|
+
const mark = function (status, models) {
|
|
3130
|
+
if (!live) return;
|
|
3131
|
+
const next = Object.assign({}, catalogRows);
|
|
3132
|
+
next[current] = { status: status, models: models };
|
|
3133
|
+
setCatalogRows(next);
|
|
3134
|
+
};
|
|
3135
|
+
mark("loading", []);
|
|
3136
|
+
try {
|
|
3137
|
+
Promise.resolve(llm.discoverModels(namespace, { provider: current })).then(function (response) {
|
|
3138
|
+
if (!response || !response.ok) { mark("error", []); return; }
|
|
3139
|
+
const found = Array.isArray(response.value) ? response.value : [];
|
|
3140
|
+
const rows = [];
|
|
3141
|
+
for (let i = 0; i < found.length; i++) {
|
|
3142
|
+
const model = found[i];
|
|
3143
|
+
if (model === null || typeof model !== "object") continue;
|
|
3144
|
+
const id = typeof model.id === "string" ? model.id.trim() : "";
|
|
3145
|
+
if (id === "") continue;
|
|
3146
|
+
rows.push({
|
|
3147
|
+
id: id,
|
|
3148
|
+
name: typeof model.name === "string" ? model.name : "",
|
|
3149
|
+
context: model.contextWindow === undefined ? "" : formatCapacity(model.contextWindow),
|
|
3150
|
+
maxTokens: model.maxTokens === undefined ? "" : formatCapacity(model.maxTokens),
|
|
3151
|
+
vision: Array.isArray(model.inputModalities) && model.inputModalities.indexOf("image") !== -1,
|
|
3152
|
+
reasoning: { mode: "inherit", levels: [] }
|
|
3153
|
+
});
|
|
3154
|
+
}
|
|
3155
|
+
mark("ready", rows);
|
|
3156
|
+
}, function () { mark("error", []); });
|
|
3157
|
+
} catch (e) { mark("error", []); }
|
|
3158
|
+
return function () { live = false; };
|
|
3159
|
+
}, [current, storedModels.length]);
|
|
3160
|
+
|
|
3052
3161
|
if (view === undefined || view === null) {
|
|
3053
3162
|
return h("section", { "data-mo": "page", "data-mo-build": BUILD, style: S.pageRoot },
|
|
3054
3163
|
h("p", { style: S.pageLoading }, t("page.loading")));
|
|
@@ -3310,6 +3419,36 @@ window.__ModuleLoader__.load({
|
|
|
3310
3419
|
endpoint, protocol and one-shot credential, which is exactly what the discovery
|
|
3311
3420
|
seam accepts (it never reads stored settings). Filling the ids here is what makes
|
|
3312
3421
|
the create button able to satisfy "a declared route must list its models". */
|
|
3422
|
+
/* Draft models use the same "pick, then adopt" flow as the saved list: the picker
|
|
3423
|
+
only fills this dialog's list and nothing is written before Create. */
|
|
3424
|
+
function addModelsToDraft(rows, clearInput) {
|
|
3425
|
+
const next = addDialog.models.slice();
|
|
3426
|
+
const known = {};
|
|
3427
|
+
for (let i = 0; i < next.length; i++) known[String(next[i].id)] = true;
|
|
3428
|
+
for (let i = 0; i < rows.length; i++) {
|
|
3429
|
+
const id = String(rows[i] !== null && rows[i] !== undefined && rows[i].id !== undefined ? rows[i].id : "").trim();
|
|
3430
|
+
if (id === "" || known[id] === true) continue;
|
|
3431
|
+
known[id] = true;
|
|
3432
|
+
next.push(Object.assign({}, rows[i], { id: id }));
|
|
3433
|
+
}
|
|
3434
|
+
const patch = { models: next, error: "" };
|
|
3435
|
+
if (clearInput === true) patch.modelInput = "";
|
|
3436
|
+
setAdd(Object.assign({}, addDialog, patch));
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
function addManualModels() {
|
|
3440
|
+
const ids = parseModelIds(addDialog.modelInput);
|
|
3441
|
+
if (ids.length === 0) return;
|
|
3442
|
+
addModelsToDraft(ids.map(function (id) { return { id: id, context: "", maxTokens: "", vision: false }; }), true);
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
function removeDraftModel(id) {
|
|
3446
|
+
setAdd(Object.assign({}, addDialog, {
|
|
3447
|
+
models: addDialog.models.filter(function (row) { return String(row.id) !== String(id); }),
|
|
3448
|
+
error: ""
|
|
3449
|
+
}));
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3313
3452
|
function fetchAddModels() {
|
|
3314
3453
|
const llm = bridge.llm;
|
|
3315
3454
|
if (llm === undefined || llm === null) { setAdd(Object.assign({}, addDialog, { error: t("page.fetchUnavailable") })); return; }
|
|
@@ -3328,19 +3467,82 @@ window.__ModuleLoader__.load({
|
|
|
3328
3467
|
return;
|
|
3329
3468
|
}
|
|
3330
3469
|
const found = Array.isArray(response.value) ? response.value : [];
|
|
3331
|
-
const
|
|
3332
|
-
|
|
3333
|
-
|
|
3470
|
+
const known = {};
|
|
3471
|
+
for (let k = 0; k < addDialog.models.length; k++) known[String(addDialog.models[k].id)] = true;
|
|
3472
|
+
const candidates = [];
|
|
3473
|
+
const picked = {};
|
|
3474
|
+
for (let k = 0; k < found.length; k++) {
|
|
3475
|
+
const model = found[k];
|
|
3476
|
+
if (model === null || typeof model !== "object") continue;
|
|
3477
|
+
const id = typeof model.id === "string" ? model.id.trim() : "";
|
|
3478
|
+
if (id === "" || known[id] === true) continue;
|
|
3479
|
+
known[id] = true;
|
|
3480
|
+
candidates.push({
|
|
3481
|
+
id: id,
|
|
3482
|
+
context: model.contextWindow === undefined ? "" : formatCapacity(model.contextWindow),
|
|
3483
|
+
maxTokens: model.maxTokens === undefined ? "" : formatCapacity(model.maxTokens),
|
|
3484
|
+
vision: Array.isArray(model.inputModalities) && model.inputModalities.indexOf("image") !== -1
|
|
3485
|
+
});
|
|
3486
|
+
picked[id] = true;
|
|
3487
|
+
}
|
|
3488
|
+
if (candidates.length === 0) { setAdd(Object.assign({}, addDialog, { error: t("page.fetchNone") })); return; }
|
|
3489
|
+
setPicker({ rows: candidates, picked: picked, query: "", target: "draft" });
|
|
3334
3490
|
}, function (error) {
|
|
3335
3491
|
setBusy("");
|
|
3336
3492
|
setAdd(Object.assign({}, addDialog, { error: String((error && error.message) || error) }));
|
|
3337
3493
|
});
|
|
3338
3494
|
}
|
|
3339
3495
|
|
|
3496
|
+
/* Third-party presets: routes the shipped catalog already describes and this
|
|
3497
|
+
document has not configured yet. Creating one writes an EMPTY profile — its
|
|
3498
|
+
address, protocol and models come from the catalog, so no models list is needed. */
|
|
3499
|
+
const addablePresets = [];
|
|
3500
|
+
if (directory !== null) {
|
|
3501
|
+
for (let i = 0; i < directory.length; i++) {
|
|
3502
|
+
const entry = directory[i];
|
|
3503
|
+
if (entry === null || typeof entry !== "object") continue;
|
|
3504
|
+
if (typeof entry.provider !== "string" || entry.provider === "") continue;
|
|
3505
|
+
/* `declared` means hand-declared by the user; a catalog provider has false. */
|
|
3506
|
+
if (entry.declared === true) continue;
|
|
3507
|
+
if (!Array.isArray(entry.settingsPath) || entry.settingsPath.length === 0) continue;
|
|
3508
|
+
if (entry.settingsNs !== namespace) continue;
|
|
3509
|
+
if (providers[entry.provider] !== undefined) continue;
|
|
3510
|
+
addablePresets.push(entry);
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
function createCatalogProvider() {
|
|
3515
|
+
const preset = String(addDialog.preset || "");
|
|
3516
|
+
if (preset === "") { setAdd(Object.assign({}, addDialog, { error: t("page.presetRequired") })); return; }
|
|
3517
|
+
const known = addablePresets.some(function (entry) { return entry.provider === preset; });
|
|
3518
|
+
if (!known) { setAdd(Object.assign({}, addDialog, { error: t("page.routeTaken") })); return; }
|
|
3519
|
+
const displayName = addDialog.displayName.trim();
|
|
3520
|
+
const secret = addDialog.key.trim();
|
|
3521
|
+
const ref = deriveKeyRef(preset);
|
|
3522
|
+
const profile = {};
|
|
3523
|
+
if (displayName !== "") profile.displayName = displayName;
|
|
3524
|
+
if (secret !== "") profile.apiKeyEnv = ref;
|
|
3525
|
+
setBusy("create");
|
|
3526
|
+
const failed = function (error) {
|
|
3527
|
+
setBusy("");
|
|
3528
|
+
setAdd(Object.assign({}, addDialog, { error: String((error && error.message) || error || t("page.createFailed")) }));
|
|
3529
|
+
};
|
|
3530
|
+
Promise.resolve(mutate([{ op: "set", path: ["providers", preset], value: profile }], revision)).then(function (result) {
|
|
3531
|
+
if (!result || !result.ok) { failed((result && result.error && result.error.message) || t("page.createFailed")); return; }
|
|
3532
|
+
const done = function () { setBusy(""); setAdd(null); setPicked(preset); setField(null); };
|
|
3533
|
+
if (secret === "" || bridge.credentials === null || bridge.credentials === undefined) { done(); return; }
|
|
3534
|
+
Promise.resolve(bridge.credentials.set(ref, secret)).then(done, function (error) {
|
|
3535
|
+
setBusy("");
|
|
3536
|
+
setAdd(Object.assign({}, addDialog, { error: String((error && error.message) || error) }));
|
|
3537
|
+
});
|
|
3538
|
+
}, failed);
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3340
3541
|
/* A brand-new route: the profile and the key reference go in ONE mutate (one
|
|
3341
3542
|
revision, so a concurrent edit cannot land between them), the secret goes to the
|
|
3342
3543
|
credentials service, and the page then selects what it just created. */
|
|
3343
3544
|
function createProvider() {
|
|
3545
|
+
if (addDialog.mode === "catalog") { createCatalogProvider(); return; }
|
|
3344
3546
|
const route = addDialog.route.trim();
|
|
3345
3547
|
if (route === "") { setAdd(Object.assign({}, addDialog, { error: t("page.routeRequired") })); return; }
|
|
3346
3548
|
if (!/^[A-Za-z0-9._-]+$/.test(route)) { setAdd(Object.assign({}, addDialog, { error: t("page.routeInvalid") })); return; }
|
|
@@ -3350,9 +3552,19 @@ window.__ModuleLoader__.load({
|
|
|
3350
3552
|
/* A hand-declared route has no catalog entry, so the host refuses a profile whose
|
|
3351
3553
|
models list is empty ("resolves no models"). The dialog therefore requires the
|
|
3352
3554
|
ids up front instead of creating a route that cannot resolve. */
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3555
|
+
if (addDialog.models.length === 0) { setAdd(Object.assign({}, addDialog, { error: t("page.modelsRequired") })); return; }
|
|
3556
|
+
const built = modelsFromDraft(addDialog.models.map(function (row) {
|
|
3557
|
+
return {
|
|
3558
|
+
id: row.id,
|
|
3559
|
+
name: row.name === undefined ? "" : row.name,
|
|
3560
|
+
context: row.context === undefined ? "" : row.context,
|
|
3561
|
+
maxTokens: row.maxTokens === undefined ? "" : row.maxTokens,
|
|
3562
|
+
vision: row.vision === true,
|
|
3563
|
+
reasoning: { mode: "inherit", levels: [] }
|
|
3564
|
+
};
|
|
3565
|
+
}), t);
|
|
3566
|
+
if (built.error !== undefined) { setAdd(Object.assign({}, addDialog, { error: built.error })); return; }
|
|
3567
|
+
const profile = { api: addDialog.api, models: built.models };
|
|
3356
3568
|
if (displayName !== "") profile.displayName = displayName;
|
|
3357
3569
|
if (baseURL !== "") profile.baseURL = baseURL;
|
|
3358
3570
|
const secret = addDialog.key.trim();
|
|
@@ -3522,7 +3734,7 @@ window.__ModuleLoader__.load({
|
|
|
3522
3734
|
disabled: !writable,
|
|
3523
3735
|
title: t("page.addProvider"),
|
|
3524
3736
|
onClick: function () {
|
|
3525
|
-
setAdd({ route: "", displayName: "", baseURL: "", api: apiChoices[0], key: "", models: "", error: "" });
|
|
3737
|
+
setAdd({ route: "", displayName: "", baseURL: "", api: apiChoices[0], key: "", models: [], modelInput: "", mode: "custom", preset: "", error: "" });
|
|
3526
3738
|
},
|
|
3527
3739
|
style: S.pageAddButton
|
|
3528
3740
|
}, "+ " + t("page.addProvider"))
|
|
@@ -3543,6 +3755,37 @@ window.__ModuleLoader__.load({
|
|
|
3543
3755
|
return chips.join(" · ");
|
|
3544
3756
|
}
|
|
3545
3757
|
|
|
3758
|
+
/* Catalog-served rows: visible, testable, but not ours to edit or reorder. */
|
|
3759
|
+
const inheritedFacts = current === null ? undefined : catalogRows[current];
|
|
3760
|
+
const inheritedModels = draftRows.length === 0 && inheritedFacts !== undefined && inheritedFacts.status === "ready" ? inheritedFacts.models : [];
|
|
3761
|
+
const inheritedList = inheritedModels.length === 0 ? null : h(React.Fragment, null,
|
|
3762
|
+
h("p", { "data-mo": "page-inherited-hint", style: S.pageHint }, t("page.inheritedModels")),
|
|
3763
|
+
h("div", { "data-mo": "page-inherited-list", style: { display: "flex", flexDirection: "column", gap: 2 } },
|
|
3764
|
+
inheritedModels.map(function (row) {
|
|
3765
|
+
const enabled = hidden.models[hiddenModelKey(current, row.id)] !== true;
|
|
3766
|
+
const mark = testStateOf(row);
|
|
3767
|
+
return h("div", { key: row.id, "data-mo": "page-model-inherited", "data-mo-id": row.id, style: S.row },
|
|
3768
|
+
h("span", {
|
|
3769
|
+
"data-mo": "page-model-dot",
|
|
3770
|
+
"data-mo-on": enabled ? "true" : "false",
|
|
3771
|
+
title: (enabled ? t("page.dotOn") : t("page.dotOff")) + " — " + t("page.dotHint"),
|
|
3772
|
+
style: enabled ? S.pageDot : Object.assign({}, S.pageDot, S.pageDotMuted)
|
|
3773
|
+
}),
|
|
3774
|
+
h("span", { style: S.pageRowName }, row.name !== "" ? row.name : row.id),
|
|
3775
|
+
h("span", { style: S.pageRowCount }, modelMeta(row)),
|
|
3776
|
+
h("span", { style: S.pageModelActions },
|
|
3777
|
+
h("button", {
|
|
3778
|
+
"data-mo": "page-model-test",
|
|
3779
|
+
"data-mo-id": row.id,
|
|
3780
|
+
"data-mo-state": mark,
|
|
3781
|
+
className: cx("dsh-mo-act", mark === "busy" ? "dsh-mo-act-busy" : "", mark === "ok" ? "dsh-mo-act-ok" : mark === "miss" ? "dsh-mo-act-miss" : mark === "fail" ? "dsh-mo-act-danger" : ""),
|
|
3782
|
+
"aria-label": t("page.test"),
|
|
3783
|
+
title: testMark(row) === undefined ? t("page.testHint") : testMark(row).message,
|
|
3784
|
+
onClick: function (event) { stopRowEvent(event); startSpinNow(event); testModel(row); },
|
|
3785
|
+
style: S.pageIconAction
|
|
3786
|
+
}, kitIcon(TEST_ICONS[mark], "▶", 14))))
|
|
3787
|
+
})));
|
|
3788
|
+
|
|
3546
3789
|
const modelList = h(DragList, {
|
|
3547
3790
|
ids: draftRows.map(function (row, index) { return "row-" + String(index); }),
|
|
3548
3791
|
disabled: false,
|
|
@@ -3606,7 +3849,7 @@ window.__ModuleLoader__.load({
|
|
|
3606
3849
|
onMouseDown: stopRowEvent,
|
|
3607
3850
|
onClick: function (event) { stopRowEvent(event); startSpinNow(event); testModel(row); },
|
|
3608
3851
|
style: S.pageIconAction
|
|
3609
|
-
}, kitIcon(TEST_ICONS[testStateOf(row)], "▶",
|
|
3852
|
+
}, kitIcon(TEST_ICONS[testStateOf(row)], "▶", 13)),
|
|
3610
3853
|
h("button", {
|
|
3611
3854
|
"data-mo": "page-model-delete",
|
|
3612
3855
|
type: "button",
|
|
@@ -3617,8 +3860,8 @@ window.__ModuleLoader__.load({
|
|
|
3617
3860
|
onMouseDown: stopRowEvent,
|
|
3618
3861
|
className: "dsh-mo-act dsh-mo-act-danger",
|
|
3619
3862
|
onClick: function (event) { stopRowEvent(event); deleteModelRow(row); },
|
|
3620
|
-
style: S.
|
|
3621
|
-
}, kitIcon("IconTrashOutlineRegular", "🗑",
|
|
3863
|
+
style: S.pageIconDelete
|
|
3864
|
+
}, kitIcon("IconTrashOutlineRegular", "🗑", 13)))
|
|
3622
3865
|
);
|
|
3623
3866
|
}
|
|
3624
3867
|
});
|
|
@@ -3645,7 +3888,7 @@ window.__ModuleLoader__.load({
|
|
|
3645
3888
|
title: t("page.deleteProviderHint"),
|
|
3646
3889
|
className: "dsh-mo-act dsh-mo-act-danger",
|
|
3647
3890
|
onClick: function () { setDeleteTarget(current); },
|
|
3648
|
-
style: S.
|
|
3891
|
+
style: S.pageIconDelete
|
|
3649
3892
|
}, kitIcon("IconTrashOutlineRegular", "🗑", 13)),
|
|
3650
3893
|
h(Switch, {
|
|
3651
3894
|
marker: "page-provider-switch",
|
|
@@ -3744,7 +3987,9 @@ window.__ModuleLoader__.load({
|
|
|
3744
3987
|
onClick: fetchModels,
|
|
3745
3988
|
style: S.pageButton
|
|
3746
3989
|
}, busy === "fetch" ? t("page.fetching") : t("page.fetchModels"))),
|
|
3747
|
-
draftRows.length === 0
|
|
3990
|
+
draftRows.length === 0
|
|
3991
|
+
? (inheritedList !== null ? inheritedList : h("p", { style: S.pageEmpty }, t("page.modelsEmptyEdit")))
|
|
3992
|
+
: modelList,
|
|
3748
3993
|
h("div", { style: S.pageActions },
|
|
3749
3994
|
h("button", {
|
|
3750
3995
|
"data-mo": "page-model-add",
|
|
@@ -3771,10 +4016,9 @@ window.__ModuleLoader__.load({
|
|
|
3771
4016
|
h("div", {
|
|
3772
4017
|
"data-mo": "fetch-dialog",
|
|
3773
4018
|
style: S.pickBackdrop,
|
|
3774
|
-
onMouseDown: function (event) { if (event.target === event.currentTarget) setPicker(null); }
|
|
3775
4019
|
},
|
|
3776
4020
|
h(DialogShell, { label: t("page.fetchTitle"), onClose: function () { setPicker(null); } },
|
|
3777
|
-
h("div", { style: S.pickHead },
|
|
4021
|
+
h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
|
|
3778
4022
|
h("h3", { style: S.pickTitle }, t("page.fetchTitle")),
|
|
3779
4023
|
h("button", {
|
|
3780
4024
|
"data-mo": "fetch-close",
|
|
@@ -3791,7 +4035,7 @@ window.__ModuleLoader__.load({
|
|
|
3791
4035
|
value: picker.query,
|
|
3792
4036
|
placeholder: t("page.fetchSearch"),
|
|
3793
4037
|
spellCheck: false,
|
|
3794
|
-
onChange: function (event) { setPicker({
|
|
4038
|
+
onChange: function (event) { setPicker(Object.assign({}, picker, { query: event.target.value })); },
|
|
3795
4039
|
style: S.pageInputWide
|
|
3796
4040
|
}),
|
|
3797
4041
|
h("button", {
|
|
@@ -3800,7 +4044,7 @@ window.__ModuleLoader__.load({
|
|
|
3800
4044
|
onClick: function () {
|
|
3801
4045
|
const next = Object.assign({}, picker.picked);
|
|
3802
4046
|
for (let i = 0; i < visible.length; i++) next[visible[i].id] = allPicked ? undefined : true;
|
|
3803
|
-
setPicker({
|
|
4047
|
+
setPicker(Object.assign({}, picker, { picked: next }));
|
|
3804
4048
|
},
|
|
3805
4049
|
style: S.pageButton
|
|
3806
4050
|
}, allPicked ? t("page.deselectAll") : t("page.selectAll"))),
|
|
@@ -3817,7 +4061,7 @@ window.__ModuleLoader__.load({
|
|
|
3817
4061
|
onChange: function (event) {
|
|
3818
4062
|
const next = Object.assign({}, picker.picked);
|
|
3819
4063
|
next[row.id] = event.target.checked ? true : undefined;
|
|
3820
|
-
setPicker({
|
|
4064
|
+
setPicker(Object.assign({}, picker, { picked: next }));
|
|
3821
4065
|
}
|
|
3822
4066
|
}),
|
|
3823
4067
|
h("span", { style: S.pickId }, row.id),
|
|
@@ -3838,8 +4082,9 @@ window.__ModuleLoader__.load({
|
|
|
3838
4082
|
const chosen = picker.rows.filter(function (row) { return picker.picked[row.id] === true; });
|
|
3839
4083
|
setPicker(null);
|
|
3840
4084
|
if (chosen.length === 0) return;
|
|
3841
|
-
/*
|
|
3842
|
-
|
|
4085
|
+
/* Draft picker: fill the create dialog's list, write nothing yet.
|
|
4086
|
+
Saved picker: adopting IS the write, which keeps that list honest. */
|
|
4087
|
+
if (picker.target === "draft") { addModelsToDraft(chosen, false); return; }
|
|
3843
4088
|
writeModels(draftRows.concat(chosen));
|
|
3844
4089
|
},
|
|
3845
4090
|
style: S.pageButtonPrimary
|
|
@@ -3847,6 +4092,32 @@ window.__ModuleLoader__.load({
|
|
|
3847
4092
|
document.body);
|
|
3848
4093
|
}
|
|
3849
4094
|
|
|
4095
|
+
/* ------------------------ the third-party preset dialog ------------------------ */
|
|
4096
|
+
/* The shipped catalog already describes these routes, so the only thing this page
|
|
4097
|
+
can add is the profile stub plus a key; models and address come from the catalog. */
|
|
4098
|
+
function addModeTabs() {
|
|
4099
|
+
const on = addDialog.mode === "catalog" ? "catalog" : "custom";
|
|
4100
|
+
const options = [{ id: "catalog", label: t("page.addModeCatalog") }, { id: "custom", label: t("page.addModeCustom") }];
|
|
4101
|
+
return h("div", { style: S.pageField },
|
|
4102
|
+
h("span", { style: S.pageFieldLabel }, t("page.addModeLabel")),
|
|
4103
|
+
h("div", { "data-mo": "add-mode", role: "radiogroup", "aria-label": t("page.addModeLabel"), style: S.pageSegments },
|
|
4104
|
+
options.map(function (option) {
|
|
4105
|
+
const active = option.id === on;
|
|
4106
|
+
return h("button", {
|
|
4107
|
+
key: option.id,
|
|
4108
|
+
"data-mo": "add-mode-option",
|
|
4109
|
+
"data-mo-id": option.id,
|
|
4110
|
+
type: "button",
|
|
4111
|
+
role: "radio",
|
|
4112
|
+
"aria-checked": active ? "true" : "false",
|
|
4113
|
+
disabled: option.id === "catalog" && addablePresets.length === 0,
|
|
4114
|
+
title: option.id === "catalog" && addablePresets.length === 0 ? t("page.noPresets") : "",
|
|
4115
|
+
onClick: function () { setAdd(Object.assign({}, addDialog, { mode: option.id, error: "" })); },
|
|
4116
|
+
style: active ? Object.assign({}, S.pageSegment, S.pageSegmentActive) : S.pageSegment
|
|
4117
|
+
}, option.label);
|
|
4118
|
+
})));
|
|
4119
|
+
}
|
|
4120
|
+
|
|
3850
4121
|
/* ------------------------ the add-provider dialog ------------------------ */
|
|
3851
4122
|
let addNode = null;
|
|
3852
4123
|
if (addDialog !== null) {
|
|
@@ -3854,11 +4125,11 @@ window.__ModuleLoader__.load({
|
|
|
3854
4125
|
addNode = ReactDOM.createPortal(
|
|
3855
4126
|
h("div", {
|
|
3856
4127
|
"data-mo": "add-dialog",
|
|
4128
|
+
"data-mo-mode": "custom",
|
|
3857
4129
|
style: S.pickBackdrop,
|
|
3858
|
-
onMouseDown: function (event) { if (event.target === event.currentTarget) setAdd(null); }
|
|
3859
4130
|
},
|
|
3860
4131
|
h(DialogShell, { label: t("page.newTitle"), onClose: function () { setAdd(null); } },
|
|
3861
|
-
h("div", { style: S.pickHead },
|
|
4132
|
+
h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
|
|
3862
4133
|
h("h3", { style: S.pickTitle }, t("page.newTitle")),
|
|
3863
4134
|
h("button", {
|
|
3864
4135
|
"data-mo": "add-close",
|
|
@@ -3868,7 +4139,22 @@ window.__ModuleLoader__.load({
|
|
|
3868
4139
|
style: S.pageIconButton
|
|
3869
4140
|
}, "✕")),
|
|
3870
4141
|
h("p", { style: S.pickDesc }, t("page.newDescription")),
|
|
3871
|
-
|
|
4142
|
+
addModeTabs(),
|
|
4143
|
+
addDialog.mode === "catalog" ? h(React.Fragment, null,
|
|
4144
|
+
h("label", { style: S.pageField },
|
|
4145
|
+
h("span", { style: S.pageFieldLabel }, t("page.providerPreset")),
|
|
4146
|
+
h("select", {
|
|
4147
|
+
"data-mo": "add-preset",
|
|
4148
|
+
"data-mo-focus": "true",
|
|
4149
|
+
className: "dsh-mo-select",
|
|
4150
|
+
value: String(addDialog.preset || ""),
|
|
4151
|
+
onChange: function (event) { setAdd(Object.assign({}, addDialog, { preset: event.target.value, error: "" })); },
|
|
4152
|
+
style: Object.assign({}, S.pageInputGrow, { colorScheme: "dark" })
|
|
4153
|
+
}, [h("option", { key: "", value: "" }, t("page.presetPlaceholder"))].concat(addablePresets.map(function (entry) {
|
|
4154
|
+
return h("option", { key: entry.provider, value: entry.provider }, entry.displayName || entry.provider);
|
|
4155
|
+
})))),
|
|
4156
|
+
h("span", { style: S.pageHint }, t("page.presetHint"))) : null,
|
|
4157
|
+
addDialog.mode === "catalog" ? null : h("label", { style: S.pageField },
|
|
3872
4158
|
h("span", { style: S.pageFieldLabel }, t("page.route")),
|
|
3873
4159
|
h("input", {
|
|
3874
4160
|
"data-mo": "add-route",
|
|
@@ -3889,7 +4175,7 @@ window.__ModuleLoader__.load({
|
|
|
3889
4175
|
onChange: function (event) { setAdd(Object.assign({}, addDialog, { displayName: event.target.value, error: "" })); },
|
|
3890
4176
|
style: S.pageInputWide
|
|
3891
4177
|
})),
|
|
3892
|
-
h("label", { style: S.pageField },
|
|
4178
|
+
addDialog.mode === "catalog" ? null : h("label", { style: S.pageField },
|
|
3893
4179
|
h("span", { style: S.pageFieldLabel }, t("page.baseUrlOptional")),
|
|
3894
4180
|
h("input", {
|
|
3895
4181
|
"data-mo": "add-base-url",
|
|
@@ -3899,7 +4185,7 @@ window.__ModuleLoader__.load({
|
|
|
3899
4185
|
onChange: function (event) { setAdd(Object.assign({}, addDialog, { baseURL: event.target.value, error: "" })); },
|
|
3900
4186
|
style: S.pageInputWide
|
|
3901
4187
|
})),
|
|
3902
|
-
h("div", { style: S.pageField },
|
|
4188
|
+
addDialog.mode === "catalog" ? null : h("div", { style: S.pageField },
|
|
3903
4189
|
h("span", { style: S.pageFieldLabel }, t("page.api")),
|
|
3904
4190
|
h("div", { "data-mo": "add-api", role: "radiogroup", "aria-label": t("page.api"), style: S.pageSegments },
|
|
3905
4191
|
apiChoices.map(function (choice) {
|
|
@@ -3927,17 +4213,24 @@ window.__ModuleLoader__.load({
|
|
|
3927
4213
|
onKeyDown: function (event) { if (event.key === "Enter") createProvider(); },
|
|
3928
4214
|
style: S.pageInputWide
|
|
3929
4215
|
})),
|
|
3930
|
-
h("
|
|
4216
|
+
addDialog.mode === "catalog" ? null : h("div", { style: S.pageField },
|
|
3931
4217
|
h("span", { style: S.pageFieldLabel }, t("page.modelsField")),
|
|
3932
|
-
h("
|
|
3933
|
-
"
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
4218
|
+
h("div", { style: S.pageInlineRow },
|
|
4219
|
+
h("input", {
|
|
4220
|
+
"data-mo": "add-models",
|
|
4221
|
+
value: addDialog.modelInput,
|
|
4222
|
+
placeholder: t("page.modelsFieldPlaceholder"),
|
|
4223
|
+
spellCheck: false,
|
|
4224
|
+
onChange: function (event) { setAdd(Object.assign({}, addDialog, { modelInput: event.target.value, error: "" })); },
|
|
4225
|
+
onKeyDown: function (event) { if (event.key === "Enter") { event.preventDefault(); addManualModels(); } },
|
|
4226
|
+
style: S.pageInputGrow
|
|
4227
|
+
}),
|
|
4228
|
+
h("button", {
|
|
4229
|
+
"data-mo": "add-model-add",
|
|
4230
|
+
type: "button",
|
|
4231
|
+
onClick: addManualModels,
|
|
4232
|
+
style: S.pageButton
|
|
4233
|
+
}, t("page.modelAddById"))),
|
|
3941
4234
|
h("div", { style: S.pageKeyRow },
|
|
3942
4235
|
h("button", {
|
|
3943
4236
|
"data-mo": "add-fetch",
|
|
@@ -3946,8 +4239,27 @@ window.__ModuleLoader__.load({
|
|
|
3946
4239
|
title: t("page.modelsFieldHint"),
|
|
3947
4240
|
onClick: fetchAddModels,
|
|
3948
4241
|
style: S.pageButton
|
|
3949
|
-
}, busy === "addfetch" ? t("page.fetching") : t("page.
|
|
3950
|
-
h("span", { style: S.pageHint }, t("page.modelsFieldHint")))
|
|
4242
|
+
}, busy === "addfetch" ? t("page.fetching") : t("page.fetchAvailable")),
|
|
4243
|
+
h("span", { style: S.pageHint }, t("page.modelsFieldHint"))),
|
|
4244
|
+
addDialog.models.length === 0
|
|
4245
|
+
? h("p", { "data-mo": "add-models-empty", style: S.pageHint }, t("page.modelListEmpty"))
|
|
4246
|
+
: h("div", { "data-mo": "add-models-list", style: S.pickList }, addDialog.models.map(function (row) {
|
|
4247
|
+
const chips = [];
|
|
4248
|
+
if (row.context !== undefined && row.context !== "") chips.push(h("span", { key: "c", style: S.pageChip }, row.context));
|
|
4249
|
+
if (row.vision === true) chips.push(h("span", { key: "v", style: S.pageChip }, t("page.vision")));
|
|
4250
|
+
return h("div", { key: row.id, style: S.pickRow, "data-mo": "add-model-item", "data-mo-id": row.id },
|
|
4251
|
+
h("span", { style: S.pickId }, row.id),
|
|
4252
|
+
h("span", { style: S.pageSpacer }),
|
|
4253
|
+
chips,
|
|
4254
|
+
h("button", {
|
|
4255
|
+
"data-mo": "add-model-remove",
|
|
4256
|
+
"data-mo-id": row.id,
|
|
4257
|
+
type: "button",
|
|
4258
|
+
title: t("page.deleteModelHint"),
|
|
4259
|
+
onClick: function () { removeDraftModel(row.id); },
|
|
4260
|
+
style: S.pageIconAction
|
|
4261
|
+
}, kitIcon("IconTrashOutlineRegular", "🗑", 13)));
|
|
4262
|
+
}))),
|
|
3951
4263
|
addDialog.error === "" ? null : h("p", { "data-mo": "add-error", style: S.pageError }, addDialog.error),
|
|
3952
4264
|
h("div", { style: S.pickFoot },
|
|
3953
4265
|
h("button", {
|
|
@@ -3959,7 +4271,7 @@ window.__ModuleLoader__.load({
|
|
|
3959
4271
|
h("button", {
|
|
3960
4272
|
"data-mo": "add-submit",
|
|
3961
4273
|
type: "button",
|
|
3962
|
-
disabled: busy === "create",
|
|
4274
|
+
disabled: busy === "create" || (addDialog.mode === "catalog" && String(addDialog.preset || "") === ""),
|
|
3963
4275
|
onClick: createProvider,
|
|
3964
4276
|
style: S.pageButtonPrimary
|
|
3965
4277
|
}, busy === "create" ? t("page.working") : t("page.create"))))),
|
|
@@ -4052,10 +4364,9 @@ window.__ModuleLoader__.load({
|
|
|
4052
4364
|
h("div", {
|
|
4053
4365
|
"data-mo": "model-dialog",
|
|
4054
4366
|
style: S.pickBackdrop,
|
|
4055
|
-
onMouseDown: function (event) { if (event.target === event.currentTarget) closeModel(); }
|
|
4056
4367
|
},
|
|
4057
4368
|
h(DialogShell, { label: t("page.modelDialogTitle"), marker: "model-dialog-card", onClose: closeModel },
|
|
4058
|
-
h("div", { style: S.pickHead },
|
|
4369
|
+
h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
|
|
4059
4370
|
h("h3", { style: S.pickTitle }, modelDialog.mode === "new" ? t("page.modelDialogNew") : t("page.modelDialogTitle")),
|
|
4060
4371
|
h("button", {
|
|
4061
4372
|
"data-mo": "model-dialog-close",
|
|
@@ -4208,10 +4519,9 @@ window.__ModuleLoader__.load({
|
|
|
4208
4519
|
h("div", {
|
|
4209
4520
|
"data-mo": "provider-delete-dialog",
|
|
4210
4521
|
style: S.pickBackdrop,
|
|
4211
|
-
onMouseDown: function (event) { if (event.target === event.currentTarget) setDeleteTarget(null); }
|
|
4212
4522
|
},
|
|
4213
4523
|
h(DialogShell, { label: t("page.deleteProvider"), onClose: function () { setDeleteTarget(null); } },
|
|
4214
|
-
h("div", { style: S.pickHead },
|
|
4524
|
+
h("div", { style: Object.assign({}, S.pickHead, { cursor: "move" }), "data-mo-drag": "true" },
|
|
4215
4525
|
h("h3", { style: S.pickTitle }, t("page.deleteProvider") + " · " + deleteTarget),
|
|
4216
4526
|
h("button", {
|
|
4217
4527
|
"data-mo": "provider-delete-close",
|