@domitor-syh/dsh-ui-skin-switcher 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.en.md +94 -0
- package/README.md +122 -11
- package/lib/client.js +230 -78
- package/lib/index.js +1 -1
- package/lib/invariant.js +1 -5
- package/package.json +15 -42
- package/lib/types/client/index.d.ts +0 -11
- package/lib/types/index.d.ts +0 -9
- package/lib/types/invariant.d.ts +0 -16
package/LICENSE
CHANGED
package/README.en.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<h1 align="center">@domitor-syh/dsh-ui-skin-switcher</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/README-Banner.jpeg" alt="dsh-ui-skin-switcher" width="100%">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center"><a href="./README.md">中文</a> | English</p>
|
|
8
|
+
|
|
9
|
+
A **Claude Desktop-style switcher** plugin for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web GUI: a floating "seat" next to the input box for picking a model and dialing reasoning effort — dropdown for models, slider for effort.
|
|
10
|
+
|
|
11
|
+
> **Compatibility**: supports **DSH 0.1.5 and newer** only.
|
|
12
|
+
|
|
13
|
+
## What it is
|
|
14
|
+
|
|
15
|
+
`dsh-ui-skin-switcher` registers the `conversation.input.model` slot and renders a model + reasoning-effort switcher seat next to the composer input.
|
|
16
|
+
|
|
17
|
+
It rides the exact same channel the first-party selector uses: it reads the provider-grouped model directory and the current selection through `ctx.modelDirectories` — the same source the `/model` panel renders — and submits through that same selection call, so a switch made here is what `/model` shows next and takes effect on the current session.
|
|
18
|
+
|
|
19
|
+
The style pays homage to Claude Desktop's switcher — slider feel, rounded track, hover highlight.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
| Feature | Detail |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| Model switching | Clicking the model entry expands the model list ordered by vendor; the selection applies immediately and the matching reasoning effort updates together |
|
|
26
|
+
| Effort slider | Supports both click and drag, with multiple effort nodes; supported levels differ per model, and adjustments apply immediately |
|
|
27
|
+
| Max-effort dot matrix | Reaching max effort reveals a right-to-left dot-matrix sweep on the track, with the handle glowing |
|
|
28
|
+
| Per-model memory | Remembers the last effort per model, kept across switches |
|
|
29
|
+
| Theme adaptive | All colors bound to DSH theme tokens; readable in light/dark themes and transparent skins |
|
|
30
|
+
|
|
31
|
+
## Video preview
|
|
32
|
+
|
|
33
|
+
<p align="center">
|
|
34
|
+
<video src="https://github.com/user-attachments/assets/5fadc536-b58c-42db-a270-9ca5d4b02cbb" controls muted loop playsinline width="100%"></video>
|
|
35
|
+
</p>
|
|
36
|
+
|
|
37
|
+
**▶︎ [Watch the full demo video](https://github.com/user-attachments/assets/5fadc536-b58c-42db-a270-9ca5d4b02cbb)** — model switching, the effort slider, and the top-level dot-matrix animation.
|
|
38
|
+
|
|
39
|
+
## Compatibility
|
|
40
|
+
|
|
41
|
+
Supports **DSH 0.1.5 and newer** only.
|
|
42
|
+
|
|
43
|
+
| DSH version | Works | Notes |
|
|
44
|
+
| --- | :---: | --- |
|
|
45
|
+
| 0.1.5-alpha.1 … 0.1.5-rc.3 | ✅ | The whole 0.1.5 line; spot-checked at alpha.1, rc.2 (full end-to-end run) and rc.3 |
|
|
46
|
+
| 0.1.6-alpha.1 / alpha.2 | ✅ | Same generation as 0.1.5 |
|
|
47
|
+
| 0.1.7-alpha.1 / alpha.2 | ✅ | Newest published line; spot-checked at alpha.2 |
|
|
48
|
+
| 0.1.3-alpha.2 and earlier | ❌ | Older generation: only `connection.api`, no `remote.session` |
|
|
49
|
+
|
|
50
|
+
The dividing line is **0.1.5**: that release replaced the old `connection.api` face with `ctx.remote.session` / `ctx.modelDirectories` and dropped the `dsh-client-ui-slots` dependency. This plugin is built on the new interfaces, so it cannot load on earlier DSH versions.
|
|
51
|
+
|
|
52
|
+
> How this table was produced: each version's `dsh-client-ui-model-selection` package was unpacked and checked for the `modelDirectories` / `modelCatalog` / `selectModel` interfaces, plus a full end-to-end run on 0.1.5-rc.2.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
With the DSH CLI installed:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
dsh plugin --profile web add @domitor-syh/dsh-ui-skin-switcher
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Restart `dsh web` — the button appears next to the input box.
|
|
63
|
+
|
|
64
|
+
Running dsh from source? Inside the repo root, use your launch command plus `dsh plugin --profile web add @domitor-syh/dsh-ui-skin-switcher`, for example:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
# Normal start
|
|
68
|
+
pnpm dsh web
|
|
69
|
+
# Add the plugin
|
|
70
|
+
pnpm dsh plugin --profile web add @domitor-syh/dsh-ui-skin-switcher
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Verify & uninstall
|
|
74
|
+
|
|
75
|
+
Restart `dsh web` — the button appears next to the input box and takes effect immediately. Verify with `dsh --profile web --dump-config`.
|
|
76
|
+
|
|
77
|
+
Uninstall (if running from source, uninstall the same way you installed): `dsh plugin --profile web remove @domitor-syh/dsh-ui-skin-switcher`, then restart `dsh web`.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
1. **Pick a model** — click the button and pick from the list; the list is grouped by vendor and the current model shows a checkmark.
|
|
82
|
+
2. **Dial effort** — for models declaring `reasoningEfforts`, an effort button appears to the right of the model; click it, then drag the slider or tap the track to switch levels.
|
|
83
|
+
|
|
84
|
+
### Why is there no reasoning-effort switcher after adding a model?
|
|
85
|
+
|
|
86
|
+
The current model does not declare a reasoning effort (`reasoningEfforts`) or vision capability. To keep model declarations accurate, we do not apply a uniform effort preset to every model — check the model vendor's official documentation and patch the underlying config file yourself, or hand the vendor's deep-thinking documentation (or its link) to the LLM and let it modify the config directly.
|
|
87
|
+
|
|
88
|
+
> Taking effect: you edit `settings.yaml` under the DSH home (`~/.dsh`) — **not this plugin's files**; DSH watches that file and hot-reloads, so the change takes effect **immediately with no restart** and persists across restarts.
|
|
89
|
+
|
|
90
|
+
Why do it yourself? To keep declarations accurate — **accuracy over coverage**. Reasoning effort is a per-vendor, per-level wire value; a wrong value silently breaks requests, so we never fill in a guessed preset.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
[MIT](./LICENSE)
|
package/README.md
CHANGED
|
@@ -1,11 +1,122 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
<h1 align="center">dsh-ui-skin-switcher · DeepSeek Harness(DSH)模型与思考强度切换器</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="docs/README-Banner.jpeg" alt="dsh-ui-skin-switcher" width="100%">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">中文 | <a href="README.en.md">English</a></p>
|
|
8
|
+
|
|
9
|
+
一个为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)Web 端打造的、**Claude Desktop 同款风格的切换器**插件:把模型选择与思考强度(推理力度)切换做成输入框旁的一枚悬浮「座位」,下拉选模型、滑动调强度,一键到位。
|
|
10
|
+
|
|
11
|
+
> **兼容性**:仅支持 **DSH 0.1.5 及以上版本**。
|
|
12
|
+
|
|
13
|
+
<div align="center">
|
|
14
|
+
|
|
15
|
+
[是什么](#是什么) · [功能特性](#功能特性) · [视频预览](#视频预览) · [兼容性](#兼容性) · [快速上手](#快速上手) · [使用方法](#使用方法) · [为什么没有思考强度?](#为什么接入模型之后没有思考强度) · [常见问题](#常见问题) · [许可证](#许可证)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
## 是什么
|
|
20
|
+
|
|
21
|
+
`dsh-ui-skin-switcher` 是 DeepSeek Harness 的 Web 端插件,注册 `conversation.input.model` 插件槽,在输入框旁渲染一枚模型 + 思考强度的切换器「座位」。
|
|
22
|
+
|
|
23
|
+
它与官方第一方模型选择器走**同一条通道**:通过 `ctx.modelDirectories` 读取按 provider 分组的模型目录与当前选择(与 `/model` 面板同源),再经同一条选择接口提交——在这里切换后的结果,就是 `/model` 面板里看到的结果,并立即作用于当前会话。
|
|
24
|
+
|
|
25
|
+
样式致敬 Claude Desktop 的切换器:滑动手感、圆角轨道、悬停高亮,都是熟悉的味道。
|
|
26
|
+
|
|
27
|
+
## 功能特性
|
|
28
|
+
|
|
29
|
+
| 能力 | 说明 |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| 模型切换 | 点击模型入口将展开按厂商依次排列的模型列表,选定模型后立即生效,对应的思考强度也会同步变更。 |
|
|
32
|
+
| 思考强度滑块 | 思考强度滑块支持点击与拖动双操作方式,设有多个强度节点;不同模型适配的强度档位存在差异,调节后立即生效。 |
|
|
33
|
+
| 最高强度点阵动画 | 达到最高思考强度时,轨道内浮现从右向左扫过的点阵动画,且滑块伴随着发光 |
|
|
34
|
+
| 逐模型记忆 | 记住每个模型最后一次选择的思考强度,切走再切回,强度不丢 |
|
|
35
|
+
| 主题自适应 | 全部颜色绑定 DSH 主题变量,深浅主题、透明背景皮肤下都清晰可读 |
|
|
36
|
+
|
|
37
|
+
## 视频预览
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<video src="https://github.com/user-attachments/assets/5fadc536-b58c-42db-a270-9ca5d4b02cbb" controls muted loop playsinline width="100%"></video>
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
**▶︎ [点击观看完整演示视频](https://github.com/user-attachments/assets/5fadc536-b58c-42db-a270-9ca5d4b02cbb)** —— 模型切换、思考强度滑块、最高档点阵动画的完整演示。
|
|
44
|
+
|
|
45
|
+
## 兼容性
|
|
46
|
+
|
|
47
|
+
仅支持 **DSH 0.1.5 及以上版本**。
|
|
48
|
+
| DSH 版本 | 可用 | 说明 |
|
|
49
|
+
| --- | :---: | --- |
|
|
50
|
+
| 0.1.5-alpha.1 ~ 0.1.5-rc.3 | ✅ | 0.1.5 系列全系可用;已抽点校验 alpha.1、rc.2(端到端实测)、rc.3 |
|
|
51
|
+
| 0.1.6-alpha.1 / alpha.2 | ✅ | 与 0.1.5 同代接口 |
|
|
52
|
+
| 0.1.7-alpha.1 / alpha.2 | ✅ | 当前最新发布线;已抽点校验 alpha.2 |
|
|
53
|
+
| 0.1.3-alpha.2 及更早 | ❌ | 旧一代接口,只有 `connection.api`,没有 `remote.session` |
|
|
54
|
+
|
|
55
|
+
分界线是 **0.1.5**:从这一版起,DSH 用 `ctx.remote.session` / `ctx.modelDirectories` 取代了旧的 `connection.api` 接口,并移除了 `dsh-client-ui-slots` 依赖。本插件基于新接口实现,因此在更早的 DSH 上无法加载。
|
|
56
|
+
|
|
57
|
+
> 上表依据:对相应版本的 `dsh-client-ui-model-selection` 包逐一解包,校验 `modelDirectories` / `modelCatalog` / `selectModel` 接口是否存在;并在 0.1.5-rc.2 上完成端到端实测。
|
|
58
|
+
本插件通过 DSH 0.1.5 引入的 `ctx.remote.session` / `ctx.modelDirectories` 接口读取模型目录与当前选择(与官方 `/model` 面板同源),在更早的 DSH 上无法加载。
|
|
59
|
+
|
|
60
|
+
## 快速上手
|
|
61
|
+
|
|
62
|
+
### 系统要求
|
|
63
|
+
|
|
64
|
+
- DSH **0.1.5 及以上版本**。
|
|
65
|
+
- 已安装 DeepSeek Harness,`dsh web` 可正常启动。
|
|
66
|
+
- npm 安装无额外要求;从源码运行需要 Node.js >= 22 与 pnpm。
|
|
67
|
+
|
|
68
|
+
### 安装(npm,推荐)
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
dsh plugin --profile web add @domitor-syh/dsh-ui-skin-switcher
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
然后重启 `dsh web`,输入框旁即出现切换器按钮。
|
|
75
|
+
|
|
76
|
+
从源码运行 DSH?在仓库根目录,用「启动命令 + `dsh plugin --profile web add @domitor-syh/dsh-ui-skin-switcher`」即可,例如:
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
# 平时启动
|
|
80
|
+
pnpm dsh web
|
|
81
|
+
# 添加插件
|
|
82
|
+
pnpm dsh plugin --profile web add @domitor-syh/dsh-ui-skin-switcher
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 验证与卸载
|
|
86
|
+
|
|
87
|
+
装好重启 `dsh web`,输入框旁出现切换器按钮即刻生效;也可以用 `dsh --profile web --dump-config` 确认插件配置层已挂载。
|
|
88
|
+
|
|
89
|
+
卸载(如从源码启动,请用同安装的方式去卸载):`dsh plugin --profile web remove @domitor-syh/dsh-ui-skin-switcher`,然后重启 `dsh web`。
|
|
90
|
+
|
|
91
|
+
## 使用方法
|
|
92
|
+
|
|
93
|
+
1. **切模型**:点击输入框旁的按钮,在列表里选一个模型;列表按厂商分组,当前模型带勾选标记。
|
|
94
|
+
2. **调强度**:对声明了 `reasoningEfforts` 的模型,模型右侧会出现思考强度按钮,点击之后——拖动滑块或在轨道上点按即可切换档位。
|
|
95
|
+
|
|
96
|
+
### 为什么接入模型之后没有思考强度?
|
|
97
|
+
|
|
98
|
+
这是因为当前模型没有声明思考强度(`reasoningEfforts`)或识图能力。为了确保声明的准确,我们没有为所有模型设定统一的思考强度档位——请自行前往该模型所属厂商的官方文档查阅,并修改底层配置文件补上声明;或者直接把厂商的深度思考文档(或文档链接)发给大模型,让它替你直接修改。
|
|
99
|
+
|
|
100
|
+
> 生效方式:你修改的是 DSH 主目录(`~/.dsh`)下的 `settings.yaml`,**不是本插件的文件**;DSH 会监听该文件并**热加载,立即生效,无需重启**,下次启动依然有效。
|
|
101
|
+
|
|
102
|
+
为什么要由你自己适配?是为了确保声明的准确——**声明准确 > 声明齐全**。思考强度是每档一个厂商私有线级值,填错会让请求静默失效,所以我们不代填统一档位。
|
|
103
|
+
|
|
104
|
+
## 常见问题
|
|
105
|
+
|
|
106
|
+
<details>
|
|
107
|
+
<summary><strong>为什么我的模型没有出现思考强度滑块?</strong></summary>
|
|
108
|
+
|
|
109
|
+
滑块只为声明了 `reasoningEfforts` 的模型显示。模型目录里没有该声明说明这台模型不支持(或未配置)推理力度切换,此时只显示模型列表。
|
|
110
|
+
|
|
111
|
+
</details>
|
|
112
|
+
|
|
113
|
+
<details>
|
|
114
|
+
<summary><strong>切换主题后切换器颜色不对?</strong></summary>
|
|
115
|
+
|
|
116
|
+
插件颜色全部绑定 DSH 主题变量(`--dsw-alias-*`),随深浅主题自动翻转。若你用了把背景设成透明的皮肤,浮层会自动启用毛玻璃(backdrop-filter)保住可读性。
|
|
117
|
+
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
## 许可证
|
|
121
|
+
|
|
122
|
+
[MIT](./LICENSE)
|
package/lib/client.js
CHANGED
|
@@ -4,6 +4,7 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
8
|
//#region \0rolldown/runtime.js
|
|
8
9
|
var __create = Object.create;
|
|
9
10
|
var __defProp = Object.defineProperty;
|
|
@@ -21,7 +22,7 @@ window.__ModuleLoader__.load({
|
|
|
21
22
|
}
|
|
22
23
|
return to;
|
|
23
24
|
};
|
|
24
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
25
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
26
|
value: mod,
|
|
26
27
|
enumerable: true
|
|
27
28
|
}) : target, mod));
|
|
@@ -39,13 +40,115 @@ window.__ModuleLoader__.load({
|
|
|
39
40
|
* (Off/Max) reasoning-effort slider for models that declare `reasoningEfforts`,
|
|
40
41
|
* remembering each model's last effort.
|
|
41
42
|
*/
|
|
42
|
-
/**
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Required services: the connection RPC face (sessions models/selectModel), the
|
|
45
|
+
* runtime sessions service (subagentAddress), the locale service (which
|
|
46
|
+
* synthesizes the seat's `t`), and the slot registry.
|
|
47
|
+
*/
|
|
44
48
|
const inject = [
|
|
45
|
-
"
|
|
49
|
+
"locale",
|
|
50
|
+
"modelDirectories",
|
|
51
|
+
"remote",
|
|
52
|
+
"remote.session",
|
|
46
53
|
"sessions",
|
|
47
54
|
"slots"
|
|
48
55
|
];
|
|
56
|
+
/** Dictionary namespace owned by this plugin. */
|
|
57
|
+
const NS = "ui-skin-switcher";
|
|
58
|
+
/** Plugin-owned copy: `zh` is the key-set source of truth, `en` mirrors it. */
|
|
59
|
+
const DICTS = {
|
|
60
|
+
zh: {
|
|
61
|
+
"trigger.fallbackModel": "模型",
|
|
62
|
+
"trigger.selectModel": "选择模型",
|
|
63
|
+
"effort.title": "思考强度",
|
|
64
|
+
"effort.default": "默认",
|
|
65
|
+
"menu.models": "模型",
|
|
66
|
+
"status.loading": "加载中…",
|
|
67
|
+
"status.noModels": "暂无可用模型",
|
|
68
|
+
"help.aria": "思考强度说明",
|
|
69
|
+
"help.text": "调节模型推理深度,低强度回复快、内容简洁;高强度思考更全面,适合复杂问题,响应会稍慢。"
|
|
70
|
+
},
|
|
71
|
+
en: {
|
|
72
|
+
"trigger.fallbackModel": "Model",
|
|
73
|
+
"trigger.selectModel": "Select model",
|
|
74
|
+
"effort.title": "Reasoning effort",
|
|
75
|
+
"effort.default": "Default",
|
|
76
|
+
"menu.models": "Models",
|
|
77
|
+
"status.loading": "Loading…",
|
|
78
|
+
"status.noModels": "No models available",
|
|
79
|
+
"help.aria": "About reasoning effort",
|
|
80
|
+
"help.text": "Adjusts how deeply the model reasons: lower effort answers faster and more concisely, while higher effort thinks more thoroughly for complex problems and responds a little slower."
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** Dot-matrix rows to render. */
|
|
84
|
+
const MATRIX_ROWS = 6;
|
|
85
|
+
/** Designed track height in CSS px; the matrix is solved to land near it. */
|
|
86
|
+
const MATRIX_TRACK_DESIGN = 26;
|
|
87
|
+
/** Smallest block edge, in device px, that still reads as a square rather than a dot. */
|
|
88
|
+
const MATRIX_MIN_BLOCK_DEV = 3;
|
|
89
|
+
/** Design gap between blocks, in CSS px at 100% scaling. */
|
|
90
|
+
const MATRIX_GAP_DESIGN = 1;
|
|
91
|
+
/** Design margin above the first and below the last block row, in CSS px. */
|
|
92
|
+
const MATRIX_MARGIN_DESIGN = .5;
|
|
93
|
+
/** Flash cycle length in seconds, matching the `.sk5-sq` animation shorthand. */
|
|
94
|
+
const FLASH_CYCLE_S = 1.45;
|
|
95
|
+
/** Per-cell cycle spread (±8%): drifts the phases apart so no pattern repeats. */
|
|
96
|
+
const FLASH_JITTER = .08;
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the dot-matrix grid in whole device pixels. Rounding the sizes here —
|
|
99
|
+
* instead of handing the browser a fractional remainder — is what keeps the
|
|
100
|
+
* matrix uniform: a fractional pitch rasterizes as alternating 7/8px gaps, and
|
|
101
|
+
* a centered fractional remainder leaves one edge with a different gap.
|
|
102
|
+
*
|
|
103
|
+
* The matrix carries no vertical margin: `rows * block + (rows - 1) * gap` is
|
|
104
|
+
* both the content and the track height, so the first row starts at the track's
|
|
105
|
+
* top edge and the last row ends at its bottom edge. The track is re-heighted to
|
|
106
|
+
* that value because a fixed 26px track is 39 device px at 150%, which no
|
|
107
|
+
* six-row arrangement fills with square blocks and a one-pixel gap.
|
|
108
|
+
* @param wDev - track width in whole device pixels.
|
|
109
|
+
* @param dpr - device pixels per CSS px.
|
|
110
|
+
* @returns the resolved layout.
|
|
111
|
+
*/
|
|
112
|
+
function solveMatrix(wDev, dpr) {
|
|
113
|
+
const gap = Math.max(1, Math.round(MATRIX_GAP_DESIGN * dpr));
|
|
114
|
+
const margin = Math.max(1, Math.round(MATRIX_MARGIN_DESIGN * dpr));
|
|
115
|
+
const rows = MATRIX_ROWS;
|
|
116
|
+
const block = Math.max(MATRIX_MIN_BLOCK_DEV, Math.round((MATRIX_TRACK_DESIGN * dpr - 2 * margin - 5 * gap) / rows));
|
|
117
|
+
const cols = Math.max(1, Math.floor((wDev + gap) / (block + gap)));
|
|
118
|
+
const restX = wDev - (cols * block + (cols - 1) * gap);
|
|
119
|
+
const padX = Math.floor(restX / 2);
|
|
120
|
+
return {
|
|
121
|
+
c: cols,
|
|
122
|
+
r: rows,
|
|
123
|
+
sq: block / dpr,
|
|
124
|
+
gap: gap / dpr,
|
|
125
|
+
radius: 1 / dpr,
|
|
126
|
+
trackHeight: (2 * margin + rows * block + 5 * gap) / dpr,
|
|
127
|
+
trackWDev: wDev,
|
|
128
|
+
padTop: margin / dpr,
|
|
129
|
+
padBottom: margin / dpr,
|
|
130
|
+
padLeft: padX / dpr,
|
|
131
|
+
padRight: (restX - padX) / dpr
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Scatter one cell's phase. A linear rule such as `(7r + 13c) % 29` maps
|
|
136
|
+
* neighbours onto a regular lattice, which the eye reads as diagonal bands;
|
|
137
|
+
* avalanching both coordinates first scatters them instead. The result stays
|
|
138
|
+
* continuous rather than quantised into buckets: with 29 buckets up to 14 cells
|
|
139
|
+
* share a phase and flip in the very same instant, every round — a synchrony
|
|
140
|
+
* the eye picks up even though the arrangement itself is random.
|
|
141
|
+
* @param r - grid row.
|
|
142
|
+
* @param c - grid column.
|
|
143
|
+
* @param seed - keeps independent scatters (phase, brightness) uncorrelated.
|
|
144
|
+
* @returns a value in `[0, 1)`.
|
|
145
|
+
*/
|
|
146
|
+
function cellUnit(r, c, seed) {
|
|
147
|
+
let h = Math.imul(r + 1, 2654435761) ^ Math.imul(c + 1, 2246822507) ^ Math.imul(seed, 668265263);
|
|
148
|
+
h = Math.imul(h ^ h >>> 15, 625341585);
|
|
149
|
+
h ^= h >>> 13;
|
|
150
|
+
return (h >>> 0) / 4294967296;
|
|
151
|
+
}
|
|
49
152
|
/** Insert one stylesheet and return its disposer. */
|
|
50
153
|
function injectCss(css) {
|
|
51
154
|
const tag = document.createElement("style");
|
|
@@ -68,7 +171,7 @@ window.__ModuleLoader__.load({
|
|
|
68
171
|
return g.models.find((m) => m.id === model);
|
|
69
172
|
}
|
|
70
173
|
/** Effort slider / matrix / model list styles, injected once for this plugin. */
|
|
71
|
-
const CSS = ".sk5-root{position:relative;display:inline-flex;}.sk5-triggers{display:inline-flex;align-items:center;gap:2px;}.sk5-trigger{display:inline-flex;align-items:center;padding:4px 8px;border-radius:
|
|
174
|
+
const CSS = ".sk5-root{position:relative;display:inline-flex;}.sk5-triggers{display:inline-flex;align-items:center;gap:2px;}.sk5-trigger{display:inline-flex;align-items:center;padding:4px 8px;border-radius:10px;border:none;background:transparent;color:var(--dsw-alias-label-primary);font-size:12.5px;cursor:pointer;max-width:210px;}.sk5-trigger:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);}.sk5-trigger:disabled{opacity:.55;cursor:not-allowed;}.sk5-triggerName{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.sk5-triggerEffort{color:var(--dsw-alias-brand-primary);}.sk5-drop{position:absolute;bottom:calc(100% + 6px);right:0;border-radius:14px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);box-shadow:0 10px 32px rgb(0 0 0 / .35);z-index:1200;color:var(--dsw-alias-label-primary);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);}.sk5-model{width:256px;}.sk5-modelsTitle{font-size:14px;font-weight:700;color:var(--dsw-alias-label-secondary);padding:12px 14px 6px;text-align:left;}.sk5-list{overflow-y:auto;padding:2px 6px 10px;max-height:min(420px,60vh);}.sk5-group{font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--dsw-alias-label-tertiary);padding:8px 10px 3px;text-align:left;}.sk5-row{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;text-align:left;padding:6px 10px;border-radius:12px;border:none;background:transparent;color:var(--dsw-alias-label-primary);font-size:12.5px;cursor:pointer;}.sk5-row:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent);}.sk5-row:disabled{opacity:.6;cursor:wait;}.sk5-rowDesc{font-size:11px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.sk5-check{font-size:12px;color:var(--dsw-alias-brand-primary);}.sk5-status{padding:14px;font-size:12.5px;opacity:.65;text-align:center;}.sk5-err{padding:8px 4px 0;font-size:11.5px;color:var(--dsw-alias-state-error-primary);text-align:left;}.sk5-effort{width:225px;max-width:calc(100vw - 32px);box-sizing:border-box;padding:10px 12px 12px;}.sk5-titleRow{display:flex;align-items:center;gap:8px;margin-bottom:8px;}.sk5-effortLabel{font-size:13px;font-weight:600;color:var(--dsw-alias-label-secondary);}.sk5-help{position:relative;margin-left:auto;display:inline-flex;}/* The ring is an SVG stroke, not a CSS border or gradient: a vector stroke is anti-aliased by the rasterizer at every size, while a sub-pixel gradient ring at this diameter quantises into visible steps. The ring inherits currentColor, so the existing hover colour change lights it up. */.sk5-helpBtn{position:relative!important;width:16px!important;height:16px!important;min-width:0!important;min-height:0!important;box-sizing:border-box!important;padding:0!important;border:none!important;box-shadow:none!important;outline:none!important;border-radius:50%!important;clip-path:circle(50%);background:transparent!important;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1;display:flex;align-items:center;justify-content:center;cursor:help;flex:none!important;appearance:none;}.sk5-helpRing{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;}.sk5-helpQ{position:relative;}.sk5-helpBtn:hover{color:var(--dsw-alias-label-primary);}.sk5-helpTip{position:absolute;top:calc(100% + 6px);right:0;width:206px;box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 10px;box-shadow:0 8px 24px rgb(0 0 0 / .35);z-index:20;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);opacity:0;visibility:hidden;pointer-events:none;transition:opacity .12s ease,visibility .12s ease;}.sk5-helpBtn:hover ~ .sk5-helpTip{opacity:1;visibility:visible;}.sk5-helpTip:hover{opacity:1;visibility:visible;}.sk5-helpTip::before{content:\"\";position:absolute;top:-8px;left:0;right:0;height:8px;}.sk5-helpTitle{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary);margin-bottom:3px;text-align:left;}.sk5-helpText{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-secondary);text-align:left;}.sk5-effortValue{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);}.sk5-effortValueMax{color:var(--dsw-static-deepseek-400);}@keyframes sk5-nameBlur{from{filter:blur(5px);opacity:.3}to{filter:blur(0);opacity:1}}.sk5-labelsRow{display:flex;justify-content:space-between;margin-bottom:6px;}.sk5-endLabel{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-secondary);}.sk5-track{position:relative;height:26px;border-radius:9px!important;background:color-mix(in srgb,var(--dsw-alias-label-primary) 15%,transparent);cursor:pointer;touch-action:none;user-select:none;-webkit-user-select:none;}.sk5-fill{position:absolute;top:0;bottom:0;left:0;border-radius:9px 0 0 9px!important;transition:width .16s ease,opacity .16s ease;pointer-events:none;}.sk5-fillEnd{border-radius:9px!important;}.sk5-dotHit{position:absolute;top:0;bottom:0;width:16px;transform:translateX(-50%);display:flex;align-items:flex-start;justify-content:center;cursor:pointer;z-index:2;}/* The dot is drawn as an SVG circle rather than a CSS background: at 4px on a 150% display that is a 6-device-pixel disc, and a vector circle is coverage-antialiased far more evenly than a border-radius fill at this size. The span keeps the 50% box radius so the hover glow stays circular. *//* The level marks and the handle are pinned to the theme static white instead of the alias label token: they must stay white in both appearances, so they never follow the light-mode near-black value. Only the top level keeps its brand blue. */.sk5-dot{width:4px!important;height:4px!important;min-width:0!important;min-height:0!important;flex:none!important;border-radius:50%!important;background:transparent!important;color:var(--dsw-static-neutral-bluish-00);opacity:.9;pointer-events:none;transition:opacity .12s ease;}.sk5-dotShape{display:block;width:100%;height:100%;overflow:visible;}/* Hover lights a soft halo with drop-shadow, not box-shadow: drop-shadow follows the painted alpha (our SVG circle), while box-shadow follows the element box — and the host overrides this span's border-radius, so a box-shadow halo came out as a blurred square with diamond corners. The resting state carries no filter at all: any filter, even a zero-blur one, moves the element onto its own rasterization layer and changes how this 6-device-pixel circle is antialiased. */.sk5-dotHit:hover .sk5-dot{opacity:1;}.sk5-dotShape{transition:filter .14s ease;filter:none;}.sk5-dotHit:hover .sk5-dotShape{filter:drop-shadow(0 0 2.5px currentColor);}.sk5-dotMax{color:var(--dsw-static-deepseek-400);}.sk5-handle{position:absolute;top:0;bottom:0;width:20px;border-radius:9px!important;background:var(--dsw-static-neutral-bluish-00);transform:translateX(-50%);box-shadow:0 0 0 transparent;transition:left .16s ease,box-shadow .5s ease,background-color .5s ease;z-index:3;cursor:grab;}.sk5-handleMax{background:color-mix(in srgb, var(--dsw-static-deepseek-450) 25%, var(--dsw-static-neutral-bluish-00));box-shadow:0 0 10px color-mix(in srgb, var(--dsw-static-deepseek-400) 90%, transparent);}/* Day mode only: a soft grey-black halo around the white knob, so that where it meets an equally white dropdown panel its own pixel edge is masked instead of showing. Night mode keeps the bare handle, and the top level keeps its blue glow. The host removes data-ds-dark-theme from <body> in the light appearance, so :not() is the day-mode test. */body:not([data-ds-dark-theme]) .sk5-handle:not(.sk5-handleMax){box-shadow:0 0 3px rgb(0 0 0 / .28);}.sk5-matrix{position:absolute;top:0;left:0;right:0;bottom:0;overflow:hidden;border-radius:9px!important;pointer-events:none;z-index:1;box-sizing:border-box;display:grid;gap:1px;justify-content:start;align-content:start;}.sk5-cell{width:100%;height:100%;opacity:0;animation:sk5-appear .15s ease forwards;}@keyframes sk5-appear{from{opacity:0}to{opacity:1}}.sk5-sq{width:100%;height:100%;border-radius:1px;animation:sk5-flash 1.45s infinite ease-in-out;}/* One flash cycle: hold blue 250ms, jump to light, hold light 480ms, fade back over 720ms. */@keyframes sk5-flash{0%,17.24%{background-color:var(--dsw-static-deepseek-500)}17.25%,50.34%{background-color:var(--flash-light,var(--dsw-static-deepseek-400))}100%{background-color:var(--dsw-static-deepseek-500)}}";
|
|
72
175
|
/** Per-model (provider\u0000model) effort memory, shared across the whole page. */
|
|
73
176
|
const effortMemory = /* @__PURE__ */ new Map();
|
|
74
177
|
/** Two-level mask curve: right side opaque, left transparent, transition over 5%→75%. */
|
|
@@ -85,45 +188,45 @@ window.__ModuleLoader__.load({
|
|
|
85
188
|
* @param ctx - client root context carrying connection and slots.
|
|
86
189
|
*/
|
|
87
190
|
function apply(ctx) {
|
|
88
|
-
|
|
191
|
+
/**
|
|
192
|
+
* The first-party model directory service. It carries the same host catalog
|
|
193
|
+
* and the same session selection projection the official `/model` selector
|
|
194
|
+
* renders, so this seat always shows what `/model` shows. DSH 0.1.5 replaced
|
|
195
|
+
* the older `connection.api.sessions` RPC face with this service.
|
|
196
|
+
*/
|
|
197
|
+
const directories = ctx.modelDirectories;
|
|
89
198
|
const runtimeSessions = ctx.sessions;
|
|
90
199
|
ctx.effect(() => injectCss(CSS));
|
|
200
|
+
ctx.effect(() => ctx.locale.register(NS, DICTS), "ui-skin-switcher: dictionaries");
|
|
91
201
|
ctx.slots.inject("conversation.input.model", () => ctx.slots.register({
|
|
92
202
|
name: "conversation.input.model",
|
|
93
|
-
priority: -1
|
|
203
|
+
priority: -1,
|
|
204
|
+
locale: NS
|
|
94
205
|
}, (props) => {
|
|
95
|
-
const { locked, sessionId } = props;
|
|
206
|
+
const { locked, sessionId, t } = props;
|
|
207
|
+
/**
|
|
208
|
+
* Read one dictionary key through the framework-injected `t` seat; the
|
|
209
|
+
* Chinese table backs a host that shipped no locale service.
|
|
210
|
+
*/
|
|
211
|
+
const tr = (key) => typeof t === "function" ? t(key) : DICTS.zh[key] ?? key;
|
|
96
212
|
const [menu, setMenu] = react.useState(null);
|
|
97
|
-
const [data, setData] = react.useState(null);
|
|
98
213
|
const [busy, setBusy] = react.useState(false);
|
|
99
214
|
const [error, setError] = react.useState(null);
|
|
100
215
|
const [hoverIdx, setHoverIdx] = react.useState(null);
|
|
101
|
-
const [grid, setGrid] = react.useState(
|
|
102
|
-
c: 0,
|
|
103
|
-
r: 0
|
|
104
|
-
});
|
|
216
|
+
const [grid, setGrid] = react.useState(null);
|
|
105
217
|
const rootRef = react.useRef(null);
|
|
106
218
|
const trackRef = react.useRef(null);
|
|
107
219
|
const dragIdxRef = react.useRef(null);
|
|
108
220
|
const busyRef = react.useRef(false);
|
|
109
221
|
const pendingRef = react.useRef(null);
|
|
110
222
|
const [settledIdx, setSettledIdx] = react.useState(null);
|
|
111
|
-
|
|
223
|
+
/** This session's shared directory — the official selector's own data source. */
|
|
224
|
+
const directory = react.useMemo(() => directories.directoryFor(sessionId), [sessionId]);
|
|
225
|
+
const state = react.useSyncExternalStore(react.useCallback((listener) => directory.store.subscribe(listener), [directory]), react.useCallback(() => directory.store.getSnapshot(), [directory]));
|
|
226
|
+
/** Composer trigger row owns this width; the slider handle spans half of it. */
|
|
112
227
|
const HANDLE_W = 20;
|
|
113
228
|
const load = () => {
|
|
114
|
-
|
|
115
|
-
const r = resp.result;
|
|
116
|
-
if (!r.ok) {
|
|
117
|
-
setError(msg(r.error));
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
setData({
|
|
121
|
-
groups: r.value.groups,
|
|
122
|
-
failures: r.value.failures,
|
|
123
|
-
current: r.value.current
|
|
124
|
-
});
|
|
125
|
-
setError(null);
|
|
126
|
-
}, (e) => setError(msg(e)));
|
|
229
|
+
directory.load().catch(() => {});
|
|
127
230
|
};
|
|
128
231
|
react.useEffect(() => {
|
|
129
232
|
load();
|
|
@@ -143,14 +246,10 @@ window.__ModuleLoader__.load({
|
|
|
143
246
|
if (menu !== "effort") return;
|
|
144
247
|
const measure = () => {
|
|
145
248
|
if (trackRef.current !== null) {
|
|
146
|
-
const w = trackRef.current.
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
setGrid((g) => g.c === cn && g.r === rn ? g : {
|
|
151
|
-
c: cn,
|
|
152
|
-
r: rn
|
|
153
|
-
});
|
|
249
|
+
const w = trackRef.current.getBoundingClientRect().width;
|
|
250
|
+
const dpr = window.devicePixelRatio > 0 ? window.devicePixelRatio : 1;
|
|
251
|
+
const next = solveMatrix(Math.floor(w * dpr), dpr);
|
|
252
|
+
setGrid((g) => g !== null && g.c === next.c && g.r === next.r && g.sq === next.sq && g.gap === next.gap && g.trackWDev === next.trackWDev && g.padTop === next.padTop && g.padBottom === next.padBottom && g.padLeft === next.padLeft && g.padRight === next.padRight ? g : next);
|
|
154
253
|
}
|
|
155
254
|
};
|
|
156
255
|
measure();
|
|
@@ -159,33 +258,22 @@ window.__ModuleLoader__.load({
|
|
|
159
258
|
}, [menu]);
|
|
160
259
|
react.useEffect(() => {
|
|
161
260
|
setHoverIdx(null);
|
|
162
|
-
}, [
|
|
261
|
+
}, [state.current === null ? null : state.current.reasoningEffort]);
|
|
163
262
|
const apply = (sel, keepOpen) => {
|
|
164
263
|
pendingRef.current = sel;
|
|
165
264
|
if (busyRef.current) return;
|
|
166
265
|
busyRef.current = true;
|
|
167
266
|
setBusy(true);
|
|
168
267
|
setError(null);
|
|
169
|
-
|
|
170
|
-
sessionId,
|
|
268
|
+
directory.select({
|
|
171
269
|
provider: sel.provider,
|
|
172
270
|
model: sel.model,
|
|
173
271
|
...sel.reasoningEffort === void 0 ? {} : { reasoningEffort: sel.reasoningEffort }
|
|
174
|
-
}).then((
|
|
272
|
+
}).then(() => {
|
|
175
273
|
busyRef.current = false;
|
|
176
274
|
setBusy(false);
|
|
177
|
-
const r = resp.result;
|
|
178
|
-
if (!r.ok) {
|
|
179
|
-
setError(msg(r.error));
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
const selected = r.value.selected;
|
|
183
275
|
if (!keepOpen) setMenu(null);
|
|
184
|
-
|
|
185
|
-
...d,
|
|
186
|
-
current: selected
|
|
187
|
-
});
|
|
188
|
-
effortMemory.set(sel.provider + "\0" + sel.model, selected.reasoningEffort);
|
|
276
|
+
effortMemory.set(sel.provider + "\0" + sel.model, sel.reasoningEffort);
|
|
189
277
|
const next = pendingRef.current;
|
|
190
278
|
if (next !== null && next !== sel) {
|
|
191
279
|
pendingRef.current = null;
|
|
@@ -199,17 +287,20 @@ window.__ModuleLoader__.load({
|
|
|
199
287
|
pendingRef.current = null;
|
|
200
288
|
});
|
|
201
289
|
};
|
|
202
|
-
const groups =
|
|
203
|
-
const current =
|
|
290
|
+
const groups = state.groups;
|
|
291
|
+
const current = state.current;
|
|
292
|
+
/** Selection failures are local; catalog failures live on the shared store. */
|
|
293
|
+
const shownError = error !== null ? error : state.error;
|
|
294
|
+
const loaded = state.status !== "idle";
|
|
204
295
|
const curGroup = current === null ? void 0 : groups.find((g) => g.id === current.provider);
|
|
205
296
|
const curModel = current === null ? void 0 : findModel(groups, current.provider, current.model);
|
|
206
|
-
const modelLabel = curModel !== void 0 ? curModel.name : current !== null && current.model !== "" ? current.model : "
|
|
297
|
+
const modelLabel = curModel !== void 0 ? curModel.name : current !== null && current.model !== "" ? current.model : tr("trigger.fallbackModel");
|
|
207
298
|
const reasoning = curModel !== void 0 ? curModel.reasoning : void 0;
|
|
208
299
|
const effectiveEffort = current !== null && current.reasoningEffort !== void 0 ? current.reasoningEffort : reasoning !== void 0 && reasoning.defaultEffort !== void 0 ? reasoning.defaultEffort : void 0;
|
|
209
300
|
const choices = reasoning === void 0 ? [] : [...reasoning.defaultEffort === void 0 ? [{
|
|
210
301
|
key: "default",
|
|
211
302
|
effort: void 0,
|
|
212
|
-
label: "
|
|
303
|
+
label: tr("effort.default")
|
|
213
304
|
}] : [], ...reasoning.efforts.map((e) => ({
|
|
214
305
|
key: "e:" + e.id,
|
|
215
306
|
effort: e.id,
|
|
@@ -223,12 +314,12 @@ window.__ModuleLoader__.load({
|
|
|
223
314
|
const displayIdx = settledIdx === null ? activeIdx : settledIdx;
|
|
224
315
|
const safeDisplayIdx = choices.length > 0 ? Math.max(0, Math.min(choices.length - 1, displayIdx)) : 0;
|
|
225
316
|
const displayChoice = choices.length > 0 ? choices[safeDisplayIdx] : null;
|
|
226
|
-
const displayLabel = displayChoice !== null ? displayChoice.label : "
|
|
317
|
+
const displayLabel = displayChoice !== null ? displayChoice.label : tr("effort.default");
|
|
227
318
|
const inMax = choices.length > 1 && safeDisplayIdx === choices.length - 1;
|
|
228
319
|
const matrixVisible = inMax && atMaxPos;
|
|
229
320
|
const posPct = choices.length > 1 ? posIdx / (choices.length - 1) * 100 : 0;
|
|
230
321
|
const matrixCells = react.useMemo(() => {
|
|
231
|
-
if (!inMax || grid
|
|
322
|
+
if (!inMax || grid === null) return [];
|
|
232
323
|
const deep = [
|
|
233
324
|
65,
|
|
234
325
|
118,
|
|
@@ -239,6 +330,17 @@ window.__ModuleLoader__.load({
|
|
|
239
330
|
158,
|
|
240
331
|
254
|
|
241
332
|
];
|
|
333
|
+
/**
|
|
334
|
+
* How far the lightest cells reach past deepseek-400 (103,158,254)
|
|
335
|
+
* toward deepseek-300 (183,200,254): a small lift reads lighter without
|
|
336
|
+
* going pale. This is the knob for "a touch lighter".
|
|
337
|
+
*/
|
|
338
|
+
const lightLift = .3;
|
|
339
|
+
const lightPeak = lightMax.map((v, i) => Math.round(v + ([
|
|
340
|
+
183,
|
|
341
|
+
200,
|
|
342
|
+
254
|
|
343
|
+
][i] - v) * lightLift));
|
|
242
344
|
const revealMs = 1e3;
|
|
243
345
|
const revealCycles = 3;
|
|
244
346
|
const revealDepth = .6;
|
|
@@ -262,15 +364,23 @@ window.__ModuleLoader__.load({
|
|
|
262
364
|
}
|
|
263
365
|
const out = [];
|
|
264
366
|
for (let r = 0; r < grid.r; r++) for (let c = 0; c < grid.c; c++) {
|
|
265
|
-
const k = .45 + (r
|
|
266
|
-
const lc = deep.map((v, i) => Math.round(v + (
|
|
267
|
-
const flashDelay = ((r
|
|
367
|
+
const k = .45 + cellUnit(r, c, 1) * .5;
|
|
368
|
+
const lc = deep.map((v, i) => Math.round(v + (lightPeak[i] - v) * k));
|
|
369
|
+
const flashDelay = (cellUnit(r, c, 2) * 1.38 + .35).toFixed(3) + "s";
|
|
370
|
+
/**
|
|
371
|
+
* Per-cell cycle length, jittered around the stylesheet's 1.45s base.
|
|
372
|
+
* With one shared duration every cell keeps a fixed phase, so the
|
|
373
|
+
* whole pattern repeats exactly each round and a local area replays
|
|
374
|
+
* the same order; the spread drifts the phases apart instead.
|
|
375
|
+
*/
|
|
376
|
+
const flashDuration = (FLASH_CYCLE_S * (.92 + cellUnit(r, c, 3) * 2 * FLASH_JITTER)).toFixed(3) + "s";
|
|
268
377
|
const fx = grid.c > 1 ? c / (grid.c - 1) : 0;
|
|
269
378
|
const appearDelay = (revealMs * (integrate(grid.c > 1 ? (grid.c - 1 - c) / (grid.c - 1) : 0, phases[r]) / fulls[r]) / 1e3).toFixed(3) + "s";
|
|
270
379
|
out.push({
|
|
271
380
|
key: r + "-" + c,
|
|
272
381
|
flashLight: "rgb(" + lc[0] + "," + lc[1] + "," + lc[2] + ")",
|
|
273
382
|
animationDelay: flashDelay,
|
|
383
|
+
flashDuration,
|
|
274
384
|
appearDelay,
|
|
275
385
|
fade: fadeAt(fx)
|
|
276
386
|
});
|
|
@@ -285,6 +395,21 @@ window.__ModuleLoader__.load({
|
|
|
285
395
|
const insetPx = HANDLE_W / 2;
|
|
286
396
|
const posFrac = posPct / 100;
|
|
287
397
|
const dotLeft = (i) => "calc(10px + (100% - 20px) * " + (choices.length > 1 ? i / (choices.length - 1) : 0) + ")";
|
|
398
|
+
/**
|
|
399
|
+
* Level dots are laid out in whole device pixels. Their proportional
|
|
400
|
+
* `left` is fractional by nature, and a 6-device-pixel circle that
|
|
401
|
+
* straddles two pixels reads as a rounded square: snapping both axes to
|
|
402
|
+
* the device grid gives the rasterizer a symmetric circle to draw.
|
|
403
|
+
*/
|
|
404
|
+
const dpr = typeof window !== "undefined" && window.devicePixelRatio > 0 ? window.devicePixelRatio : 1;
|
|
405
|
+
const dotDev = 4 * dpr;
|
|
406
|
+
const insetDev = insetPx * dpr;
|
|
407
|
+
const dotLeftSnapped = (i) => {
|
|
408
|
+
if (grid === null) return dotLeft(i);
|
|
409
|
+
const frac = choices.length > 1 ? i / (choices.length - 1) : 0;
|
|
410
|
+
return Math.round(insetDev + (grid.trackWDev - 2 * insetDev) * frac) / dpr + "px";
|
|
411
|
+
};
|
|
412
|
+
const dotTopSnapped = grid === null ? void 0 : Math.round((grid.trackHeight * dpr - dotDev) / 2) / dpr + "px";
|
|
288
413
|
const handleLeft = "calc(10px + (100% - 20px) * " + posFrac + ")";
|
|
289
414
|
const fillWidth = handleLeft;
|
|
290
415
|
const fillHidden = inMax && atMaxPos;
|
|
@@ -310,7 +435,7 @@ window.__ModuleLoader__.load({
|
|
|
310
435
|
const el = trackRef.current;
|
|
311
436
|
if (el === null || choices.length < 2) return 0;
|
|
312
437
|
const rect = el.getBoundingClientRect();
|
|
313
|
-
const usable = Math.max(rect.width -
|
|
438
|
+
const usable = Math.max(rect.width - 20, 1);
|
|
314
439
|
const frac = Math.max(0, Math.min(1, (ev.clientX - rect.left - insetPx) / usable));
|
|
315
440
|
const idx = Math.round(frac * (choices.length - 1));
|
|
316
441
|
return Math.max(0, Math.min(choices.length - 1, idx));
|
|
@@ -352,7 +477,7 @@ window.__ModuleLoader__.load({
|
|
|
352
477
|
window.addEventListener("pointercancel", onWinCancel);
|
|
353
478
|
window.addEventListener("blur", onWinCancel);
|
|
354
479
|
};
|
|
355
|
-
const triggerTitle = curGroup !== void 0 && curModel !== void 0 ? curGroup.name + " / " + curModel.name + (effectiveEffort !== void 0 ? " · " + displayLabel : "") : "
|
|
480
|
+
const triggerTitle = curGroup !== void 0 && curModel !== void 0 ? curGroup.name + " / " + curModel.name + (effectiveEffort !== void 0 ? " · " + displayLabel : "") : tr("trigger.selectModel");
|
|
356
481
|
return react.createElement("div", {
|
|
357
482
|
ref: rootRef,
|
|
358
483
|
className: "sk5-root"
|
|
@@ -368,7 +493,7 @@ window.__ModuleLoader__.load({
|
|
|
368
493
|
type: "button",
|
|
369
494
|
className: "sk5-trigger sk5-triggerEffort",
|
|
370
495
|
disabled: locked || busy || current === null,
|
|
371
|
-
title: "
|
|
496
|
+
title: tr("effort.title"),
|
|
372
497
|
"aria-haspopup": "menu",
|
|
373
498
|
"aria-expanded": menu === "effort",
|
|
374
499
|
onClick: () => setMenu((m) => m === "effort" ? null : "effort")
|
|
@@ -378,8 +503,8 @@ window.__ModuleLoader__.load({
|
|
|
378
503
|
}, displayLabel)) : null), menu === "model" ? react.createElement("div", {
|
|
379
504
|
className: "sk5-drop sk5-model",
|
|
380
505
|
role: "menu",
|
|
381
|
-
"aria-label": "
|
|
382
|
-
}, react.createElement("div", { className: "sk5-modelsTitle" }, "
|
|
506
|
+
"aria-label": tr("trigger.selectModel")
|
|
507
|
+
}, react.createElement("div", { className: "sk5-modelsTitle" }, tr("menu.models")), react.createElement("div", { className: "sk5-list" }, !loaded && shownError === null ? react.createElement("div", { className: "sk5-status" }, tr("status.loading")) : null, !loaded && shownError !== null ? react.createElement("div", { className: "sk5-err" }, shownError) : null, loaded && groups.length === 0 ? react.createElement("div", { className: "sk5-status" }, tr("status.noModels")) : null, loaded ? groups.map((g) => react.createElement("div", { key: g.id }, react.createElement("div", { className: "sk5-group" }, g.name), g.models.map((m) => {
|
|
383
508
|
const active = current !== null && current.provider === g.id && current.model === m.id;
|
|
384
509
|
return react.createElement("button", {
|
|
385
510
|
key: m.id,
|
|
@@ -392,7 +517,7 @@ window.__ModuleLoader__.load({
|
|
|
392
517
|
}))) : null)) : null, menu === "effort" ? react.createElement("div", {
|
|
393
518
|
className: "sk5-drop sk5-effort",
|
|
394
519
|
role: "menu",
|
|
395
|
-
"aria-label": "
|
|
520
|
+
"aria-label": tr("effort.title")
|
|
396
521
|
}, react.createElement("div", { className: "sk5-titleRow" }, react.createElement("span", { className: "sk5-effortLabel" }, "Effort"), react.createElement("span", {
|
|
397
522
|
key: displayLabel,
|
|
398
523
|
className: "sk5-effortValue" + (choices.length > 1 && inMax ? " sk5-effortValueMax" : ""),
|
|
@@ -400,24 +525,39 @@ window.__ModuleLoader__.load({
|
|
|
400
525
|
}, displayLabel), react.createElement("div", { className: "sk5-help" }, react.createElement("button", {
|
|
401
526
|
type: "button",
|
|
402
527
|
className: "sk5-helpBtn",
|
|
403
|
-
"aria-label": "
|
|
404
|
-
},
|
|
528
|
+
"aria-label": tr("help.aria")
|
|
529
|
+
}, react.createElement("svg", {
|
|
530
|
+
className: "sk5-helpRing",
|
|
531
|
+
viewBox: "0 0 16 16",
|
|
532
|
+
"aria-hidden": "true"
|
|
533
|
+
}, react.createElement("circle", {
|
|
534
|
+
cx: 8,
|
|
535
|
+
cy: 8,
|
|
536
|
+
r: 7.35,
|
|
537
|
+
fill: "none",
|
|
538
|
+
stroke: "currentColor",
|
|
539
|
+
strokeWidth: .75
|
|
540
|
+
})), react.createElement("span", { className: "sk5-helpQ" }, "?")), react.createElement("div", { className: "sk5-helpTip" }, react.createElement("div", { className: "sk5-helpTitle" }, tr("effort.title")), react.createElement("div", { className: "sk5-helpText" }, tr("help.text"))))), react.createElement("div", { className: "sk5-labelsRow" }, react.createElement("span", { className: "sk5-endLabel" }, "Faster"), react.createElement("span", { className: "sk5-endLabel" }, "Smarter")), react.createElement("div", {
|
|
405
541
|
ref: trackRef,
|
|
406
542
|
className: "sk5-track",
|
|
543
|
+
style: grid === null ? void 0 : { height: grid.trackHeight + "px" },
|
|
407
544
|
onPointerDown: onTrackDown
|
|
408
545
|
}, react.createElement("div", {
|
|
409
|
-
className: "sk5-fill",
|
|
546
|
+
className: "sk5-fill" + (posPct > 96 ? " sk5-fillEnd" : ""),
|
|
410
547
|
style: {
|
|
411
548
|
width: fillWidth,
|
|
412
549
|
background: fillSolid,
|
|
413
|
-
opacity: fillHidden ? 0 : 1
|
|
414
|
-
borderRadius: posPct > 96 ? 6 : "6px 0 0 6px"
|
|
550
|
+
opacity: fillHidden ? 0 : 1
|
|
415
551
|
}
|
|
416
|
-
}), matrixVisible && grid
|
|
552
|
+
}), matrixVisible && grid !== null ? react.createElement("div", {
|
|
417
553
|
className: "sk5-matrix",
|
|
418
554
|
style: {
|
|
419
|
-
gridTemplateColumns: "repeat(" + grid.c + ",
|
|
420
|
-
gridTemplateRows: "repeat(" + grid.r + ",
|
|
555
|
+
gridTemplateColumns: "repeat(" + grid.c + "," + grid.sq + "px)",
|
|
556
|
+
gridTemplateRows: "repeat(" + grid.r + "," + grid.sq + "px)",
|
|
557
|
+
gap: grid.gap + "px",
|
|
558
|
+
padding: grid.padTop + "px " + grid.padRight + "px " + grid.padBottom + "px " + grid.padLeft + "px",
|
|
559
|
+
justifyContent: "start",
|
|
560
|
+
alignContent: "start"
|
|
421
561
|
}
|
|
422
562
|
}, matrixCells.map((sq) => react.createElement("div", {
|
|
423
563
|
key: sq.key,
|
|
@@ -429,16 +569,30 @@ window.__ModuleLoader__.load({
|
|
|
429
569
|
background: "var(--dsw-static-deepseek-500)",
|
|
430
570
|
opacity: sq.fade,
|
|
431
571
|
animationDelay: sq.animationDelay,
|
|
572
|
+
animationDuration: sq.flashDuration,
|
|
573
|
+
borderRadius: grid.radius + "px",
|
|
432
574
|
"--flash-light": sq.flashLight
|
|
433
575
|
}
|
|
434
576
|
})))) : null, !inMax ? choices.map((c, i) => react.createElement("span", {
|
|
435
577
|
key: c.key,
|
|
436
578
|
className: "sk5-dotHit",
|
|
437
|
-
style: { left:
|
|
438
|
-
}, react.createElement("span", {
|
|
579
|
+
style: { left: dotLeftSnapped(i) }
|
|
580
|
+
}, react.createElement("span", {
|
|
581
|
+
className: "sk5-dot" + (i === choices.length - 1 ? " sk5-dotMax" : ""),
|
|
582
|
+
style: { marginTop: dotTopSnapped }
|
|
583
|
+
}, react.createElement("svg", {
|
|
584
|
+
className: "sk5-dotShape",
|
|
585
|
+
viewBox: "0 0 4 4",
|
|
586
|
+
"aria-hidden": "true"
|
|
587
|
+
}, react.createElement("circle", {
|
|
588
|
+
cx: 2,
|
|
589
|
+
cy: 2,
|
|
590
|
+
r: 2,
|
|
591
|
+
fill: "currentColor"
|
|
592
|
+
}))))) : null, react.createElement("div", {
|
|
439
593
|
className: "sk5-handle" + (inMax ? " sk5-handleMax" : ""),
|
|
440
594
|
style: { left: handleLeft }
|
|
441
|
-
})),
|
|
595
|
+
})), shownError !== null ? react.createElement("div", { className: "sk5-err" }, shownError) : null) : null);
|
|
442
596
|
}));
|
|
443
597
|
}
|
|
444
598
|
//#endregion
|
|
@@ -447,5 +601,3 @@ window.__ModuleLoader__.load({
|
|
|
447
601
|
return module.exports;
|
|
448
602
|
}
|
|
449
603
|
});
|
|
450
|
-
|
|
451
|
-
//# sourceMappingURL=client.js.map
|
package/lib/index.js
CHANGED
package/lib/invariant.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
//#region
|
|
2
|
-
/**
|
|
3
|
-
* Package-owned invariant companion for `@domitor-syh/dsh-ui-skin-switcher`.
|
|
4
|
-
* @module @domitor-syh/dsh-ui-skin-switcher/invariant
|
|
5
|
-
*/
|
|
1
|
+
//#region src/invariant.ts
|
|
6
2
|
const PACKAGE_NAME = "@domitor-syh/dsh-ui-skin-switcher";
|
|
7
3
|
/** Cordis companion plugin name. */
|
|
8
4
|
const name = "client-ui-skin-switcher-invariant";
|
package/package.json
CHANGED
|
@@ -1,32 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@domitor-syh/dsh-ui-skin-switcher",
|
|
3
3
|
"description": "Model and reasoning-effort switcher: composer seat with an Off/Max effort slider over session.models / session.selectModel",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/
|
|
11
|
-
"directory": "packages/client/ui-skin-switcher"
|
|
10
|
+
"url": "git+https://github.com/domitor-syh/dsh-ui-skin-switcher.git"
|
|
12
11
|
},
|
|
13
12
|
"type": "module",
|
|
14
13
|
"main": "lib/index.js",
|
|
15
|
-
"types": "lib/types/index.d.ts",
|
|
16
14
|
"exports": {
|
|
17
|
-
".":
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
},
|
|
21
|
-
"./invariant": {
|
|
22
|
-
"types": "./lib/types/invariant.d.ts",
|
|
23
|
-
"default": "./lib/invariant.js"
|
|
24
|
-
},
|
|
25
|
-
"./client": {
|
|
26
|
-
"types": "./lib/types/client/index.d.ts",
|
|
27
|
-
"default": "./lib/client.js"
|
|
28
|
-
},
|
|
29
|
-
"./src/*": "./src/*",
|
|
15
|
+
".": "./lib/index.js",
|
|
16
|
+
"./invariant": "./lib/invariant.js",
|
|
17
|
+
"./client": "./lib/client.js",
|
|
30
18
|
"./package.json": "./package.json"
|
|
31
19
|
},
|
|
32
20
|
"dsh": {
|
|
@@ -35,44 +23,29 @@
|
|
|
35
23
|
},
|
|
36
24
|
"client": {
|
|
37
25
|
"inject": [
|
|
38
|
-
"@deepseek-ai/dsh-
|
|
26
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
27
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
28
|
+
"@deepseek-ai/dsh-client-locale",
|
|
39
29
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
40
|
-
"@deepseek-ai/dsh-client-ui-
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-model-selection"
|
|
41
31
|
],
|
|
42
32
|
"platform": "web"
|
|
43
33
|
}
|
|
44
34
|
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsdown"
|
|
37
|
+
},
|
|
45
38
|
"license": "MIT",
|
|
46
39
|
"peerDependencies": {
|
|
47
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2",
|
|
49
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
|
|
50
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
|
|
52
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
53
|
-
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
54
40
|
"react": "^18.2.0"
|
|
55
41
|
},
|
|
56
42
|
"devDependencies": {
|
|
57
|
-
"
|
|
58
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2",
|
|
59
|
-
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
|
|
60
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
63
|
-
"@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
|
|
64
|
-
"@types/react": "~18.3.1",
|
|
65
|
-
"react": "^18.2.0"
|
|
43
|
+
"tsdown": "^0.22.2"
|
|
66
44
|
},
|
|
67
45
|
"files": [
|
|
68
46
|
"lib/index.js",
|
|
69
47
|
"lib/invariant.js",
|
|
70
48
|
"lib/client.js",
|
|
71
|
-
"lib/types/**/*.d.ts",
|
|
72
49
|
"cordis.patch.yml"
|
|
73
|
-
]
|
|
74
|
-
|
|
75
|
-
"bundle": "tsdown",
|
|
76
|
-
"watch": "tsdown --watch"
|
|
77
|
-
}
|
|
78
|
-
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** Required services: the connection RPC face (sessions) and the slot registry. */
|
|
2
|
-
/** Required services: the connection RPC face (sessions models/selectModel), the runtime sessions service (subagentAddress), and the slot registry. */
|
|
3
|
-
export declare const inject: string[];
|
|
4
|
-
/**
|
|
5
|
-
* Client plugin body: inject the stylesheet once, then register the composer
|
|
6
|
-
* model seat whose component reads `sessions.models` and writes
|
|
7
|
-
* `sessions.selectModel`.
|
|
8
|
-
* @param ctx - client root context carrying connection and slots.
|
|
9
|
-
*/
|
|
10
|
-
export declare function apply(ctx: any): void;
|
|
11
|
-
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/index.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skin-switcher plugin, node half. Pure UI plugin: the empty apply exists so
|
|
3
|
-
* the plugin appears in the host cordis.yml / Loader; the browser half ships
|
|
4
|
-
* via exports["./client"], discovered through the package.json dsh.client
|
|
5
|
-
* declaration.
|
|
6
|
-
*/
|
|
7
|
-
/** Host plugin body — no host-side behavior for this surface plugin. */
|
|
8
|
-
export declare function apply(): void;
|
|
9
|
-
//# sourceMappingURL=index.d.ts.map
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package-owned invariant companion for `@domitor-syh/dsh-ui-skin-switcher`.
|
|
3
|
-
* @module @domitor-syh/dsh-ui-skin-switcher/invariant
|
|
4
|
-
*/
|
|
5
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
-
/** Cordis companion plugin name. */
|
|
7
|
-
export declare const name = "client-ui-skin-switcher-invariant";
|
|
8
|
-
/** Service required before the companion can reserve package ownership. */
|
|
9
|
-
export declare const inject: string[];
|
|
10
|
-
/**
|
|
11
|
-
* Register this package's invariant companion.
|
|
12
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
-
*/
|
|
15
|
-
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
-
//# sourceMappingURL=invariant.d.ts.map
|