@jcy2387/dsh-models-input-modalities 0.1.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 +21 -0
- package/README.md +124 -0
- package/README.zh.md +127 -0
- package/cordis.patch.yml +8 -0
- package/lib/client.cjs +409 -0
- package/lib/client.cjs.map +1 -0
- package/lib/index.js +5 -0
- package/lib/types/client/ImageInputCard.d.ts +27 -0
- package/lib/types/client/controller.d.ts +51 -0
- package/lib/types/client/index.d.ts +22 -0
- package/lib/types/client/locales.d.ts +24 -0
- package/lib/types/image-input.d.ts +37 -0
- package/lib/types/index.d.ts +2 -0
- package/package.json +116 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-models-input-modalities contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# DSH Models Input Modalities
|
|
2
|
+
|
|
3
|
+
[](https://github.com/DamonBao/dsh-models-input-modalities/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#development)
|
|
6
|
+
[](#development)
|
|
7
|
+
|
|
8
|
+
English | [简体中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web client plugin that adds an **Input modalities** fold to every third-party (pi-ai) provider card on the **Settings → Models** page, declaring per model which inputs it accepts (whether images are allowed) — exactly the field the page's own forms do not expose. Built against DSH `0.1.5-alpha.1` (peer range `>=0.1.5-alpha.1 <0.2.0`).
|
|
11
|
+
|
|
12
|
+
The providers themselves (provider ID, API base URL, protocol, API key, model list) are still created and edited entirely through the Models page forms; this plugin **pre-writes no provider configuration**.
|
|
13
|
+
|
|
14
|
+
## What it does
|
|
15
|
+
|
|
16
|
+
- **A three-way choice per model** — *Provider default*, *Text only*, or *Text and image*.
|
|
17
|
+
- **Exact adapter semantics** — *Text only* writes `input: [text]`, *Text and image* writes `input: [text, image]`, and *Provider default* removes the field so the row inherits the installed catalog's modalities, then the route's `defaultInput`.
|
|
18
|
+
- **Revision-fenced writes** — the whole `models` array is written back under the revision read when the fold was opened, with the same array semantics and conflict handling as the Models page's own cards: a concurrent edit surfaces a conflict notice and reloads instead of silently clobbering.
|
|
19
|
+
- **Field preservation** — every other field of every row survives verbatim, including fields this card never shows.
|
|
20
|
+
- **Localized UI** — English and Chinese follow the Web UI's locale.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
1. Install (see below) and restart `dsh web`.
|
|
25
|
+
2. **Settings → Models**: create your gateway provider with **Add custom provider** as usual, or open an existing one.
|
|
26
|
+
3. An **Input modalities** fold appears under every third-party provider card; expand it and choose one of the three states per model.
|
|
27
|
+
4. Click **Save**. The claim lands in the user layer of `$DSH_HOME/settings.yaml`; the adapter picks it up on its next request — no restart required.
|
|
28
|
+
|
|
29
|
+
## How it works
|
|
30
|
+
|
|
31
|
+
The plugin registers its component into the `settings.models.provider-card` extension seat exposed by the Models page (key `llm-pi-ai`, i.e. the cards of the whole pi-ai adapter family). On first expansion the fold reads the provider's stored `models` rows through the settings Remote, edits them locally, and writes the whole array back under the revision captured at read time — the same array semantics and conflict handling as the page's own cards (a concurrent edit prompts a conflict notice and a reload). All fields other than `input` are preserved verbatim in every row.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Prerequisites: DeepSeek Harness (`dsh`) `>=0.1.5-alpha.1 <0.2.0` with the `web` profile.
|
|
36
|
+
|
|
37
|
+
**From npm:**
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
dsh plugin --profile web add @jcy2387/dsh-models-input-modalities
|
|
41
|
+
dsh web
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**From a local checkout (development):**
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
git clone https://github.com/DamonBao/dsh-models-input-modalities.git
|
|
48
|
+
cd dsh-models-input-modalities
|
|
49
|
+
pnpm install && pnpm run build
|
|
50
|
+
dsh plugin --profile web add link:$PWD
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- `link:` references this directory directly; after a change run `pnpm run build` and restart dsh. Without the `link:` prefix the install is a copy — update it with `dsh plugin --profile web update`.
|
|
54
|
+
- Uninstall: `dsh plugin --profile web remove @jcy2387/dsh-models-input-modalities`.
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
Requirements: Node.js `^22.19.0 || >=24.0.0` and pnpm `11.7`.
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pnpm install
|
|
62
|
+
pnpm run check # typecheck + test + build + publint, same as CI
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Individual commands:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
pnpm run typecheck # host + client faces
|
|
69
|
+
pnpm test # vitest suites over the pure row helpers
|
|
70
|
+
pnpm run build # tsc d.ts + tsdown (lib/index.js & lib/client.cjs)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The client artifact is a self-contained bundle: React, Cordis, ui-slots, and ui-primitives are supplied by the Web shell, CSS Modules are inlined, and every other `@deepseek-ai/*` package is a type-only import (enforced by a build-time purity check).
|
|
74
|
+
|
|
75
|
+
CI ([`.github/workflows/ci.yml`](.github/workflows/ci.yml)) runs the same gates on Node 22.22.0 and 24.x, verifies on tag pushes that the release tag matches the package version, audits the packed tarball's file list, and runs a consumer smoke test that installs the tarball into a scratch project (resolving the published peer ranges against the real registry) and imports every Node-side entry point.
|
|
76
|
+
|
|
77
|
+
### Release
|
|
78
|
+
|
|
79
|
+
Publishing is automated by the **Release** workflow ([`.github/workflows/release.yml`](.github/workflows/release.yml)), which runs whenever a GitHub Release is published. It requires the release tag to equal the package version (an optional `v` prefix is stripped), re-runs the full quality gates, packs the tarball, and publishes to npm with **provenance** via **OIDC trusted publishing** — no long-lived `NPM_TOKEN` secret is involved.
|
|
80
|
+
|
|
81
|
+
One-time setup: configure [trusted publishing](https://docs.npmjs.com/trusted-publishing) on npmjs.com for `@jcy2387/dsh-models-input-modalities`, authorizing repository `DamonBao/dsh-models-input-modalities` with workflow `release.yml` (no environment).
|
|
82
|
+
|
|
83
|
+
The dist-tag follows the GitHub Release's pre-release flag: a full release (checkbox unchecked) publishes under `latest` — including rc versions — while a pre-release publishes under the channel tag derived from the version (`0.1.1-alpha.2` → `alpha`, `0.1.1-rc.1` → `rc`). The workflow is idempotent — a version that already exists on npm is skipped, so a re-run after a partial failure republishes only what is missing.
|
|
84
|
+
|
|
85
|
+
A typical release:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
# bump the version in package.json, then:
|
|
89
|
+
pnpm run check
|
|
90
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
91
|
+
git commit -am "release: $VERSION"
|
|
92
|
+
git tag "$VERSION"
|
|
93
|
+
git push origin main --tags
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Then create and publish a GitHub Release for that tag.
|
|
97
|
+
|
|
98
|
+
Dependabot checks GitHub Actions dependencies weekly. npm version updates are intentionally not enabled for Dependabot: it regenerates `pnpm-lock.yaml` without the workspace overrides, so its PRs cannot pass `pnpm install --frozen-lockfile` — bump dependencies manually with `pnpm update`.
|
|
99
|
+
|
|
100
|
+
### Repository layout
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
.
|
|
104
|
+
├─ src/
|
|
105
|
+
│ ├─ index.ts # Host half: an intentionally empty apply (browser-only plugin)
|
|
106
|
+
│ ├─ image-input.ts # pure row helpers for the per-model input claim
|
|
107
|
+
│ └─ client/ # Web half: the input-modality fold (controller, card, locales)
|
|
108
|
+
├─ tests/ # vitest suites over the pure row helpers
|
|
109
|
+
├─ build/ # tsdown preset for the self-contained client bundle
|
|
110
|
+
├─ .github/workflows/ci.yml # validate + tarball audit + consumer smoke
|
|
111
|
+
├─ .github/workflows/release.yml # npm publish on GitHub Release
|
|
112
|
+
├─ cordis.patch.yml
|
|
113
|
+
└─ README.md / README.zh.md
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Known limitations
|
|
117
|
+
|
|
118
|
+
- Dormant (not yet configured) provider cards do not render the fold; a freshly created custom provider appears **after** it is saved.
|
|
119
|
+
- Route-level `defaultInput` and `modelOverrides` for built-in provider-catalog models are out of this plugin's scope — set them directly in `$DSH_HOME/settings.yaml`.
|
|
120
|
+
- In read-only settings deployments the fold is visible but cannot save.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
[MIT](LICENSE) © jcy2387
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# DSH Models Input Modalities
|
|
2
|
+
|
|
3
|
+
[](https://github.com/DamonBao/dsh-models-input-modalities/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](#开发)
|
|
6
|
+
[](#开发)
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 简体中文
|
|
9
|
+
|
|
10
|
+
为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)打造的 Web 客户端插件:给**设置 → 模型**页的每个第三方(pi-ai)提供方卡片补上一个**「输入模态」折叠区**,按模型声明它的输入模态列表(是否接受图片)——这正是模型页自带表单没有开放的那个字段。基于 DSH `0.1.5-alpha.1` 构建(peer 范围 `>=0.1.5-alpha.1 <0.2.0`)。
|
|
11
|
+
|
|
12
|
+
提供方本身(Provider ID、API 地址、协议、密钥、模型列表)仍然完全在模型页的表单里创建和编辑;本插件**不预写任何提供方配置**。
|
|
13
|
+
|
|
14
|
+
## 功能
|
|
15
|
+
|
|
16
|
+
- **按模型三选一** —— *提供方默认*、*仅文本*、*文本和图片*。
|
|
17
|
+
- **与适配器语义一致** —— *仅文本*写入 `input: [text]`,*文本和图片*写入 `input: [text, image]`;*提供方默认*删除该字段,让这一行继承已安装目录的模态,再回退路由 `defaultInput`。
|
|
18
|
+
- **revision 围栏写入** —— 整组 `models` 数组按折叠区打开时读到的 revision 写回,与模型页自身卡片相同的数组语义与冲突处理:他人同时改动时提示冲突并重新加载,而不是静默覆盖。
|
|
19
|
+
- **字段保留** —— 行内除 `input` 外的所有字段原样保留,包括本卡片从不展示的字段。
|
|
20
|
+
- **本地化界面** —— 中英文跟随 Web UI 语言。
|
|
21
|
+
|
|
22
|
+
## 使用
|
|
23
|
+
|
|
24
|
+
1. 安装(见下),重启 `dsh web`。
|
|
25
|
+
2. 设置 → 模型:用**添加自定义提供方**照常创建你的网关提供方(或打开已有的)。
|
|
26
|
+
3. 每张第三方提供方卡片下方出现**输入模态**折叠区,展开后按模型三选一:
|
|
27
|
+
- **提供方默认** — 不写字段,继承已安装目录的模态,再回退路由 `defaultInput`
|
|
28
|
+
- **仅文本** — 写入 `input: [text]`
|
|
29
|
+
- **文本和图片** — 写入 `input: [text, image]`
|
|
30
|
+
4. 点**保存**。写入 `$DSH_HOME/settings.yaml` 用户层,适配器在下一次请求时生效,无需重启。
|
|
31
|
+
|
|
32
|
+
## 原理
|
|
33
|
+
|
|
34
|
+
插件把组件注册进模型页对外开放的 `settings.models.provider-card` 扩展位(key 为 `llm-pi-ai`,即整个 pi-ai 适配器家族的卡片)。折叠区首次展开时通过 settings Remote 读取该提供方存储的 `models` 行,本地编辑后按读取时的 revision 围栏整组写回——与模型页自身卡片相同的数组语义与冲突处理(他人同时改动时提示冲突并重新加载)。行内除 `input` 外的所有字段原样保留。
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
前置条件:DeepSeek Harness(`dsh`)`>=0.1.5-alpha.1 <0.2.0`(装有 `web` profile)。
|
|
39
|
+
|
|
40
|
+
**从 npm 安装:**
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
dsh plugin --profile web add @jcy2387/dsh-models-input-modalities
|
|
44
|
+
dsh web
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**从本地代码安装(开发):**
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
git clone https://github.com/DamonBao/dsh-models-input-modalities.git
|
|
51
|
+
cd dsh-models-input-modalities
|
|
52
|
+
pnpm install && pnpm run build
|
|
53
|
+
dsh plugin --profile web add link:$PWD
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `link:` 直接引用本目录,改动后 `pnpm run build` 再重启 dsh 即可;去掉 `link:` 前缀则是复制安装,更新需要 `dsh plugin --profile web update`。
|
|
57
|
+
- 卸载:`dsh plugin --profile web remove @jcy2387/dsh-models-input-modalities`。
|
|
58
|
+
|
|
59
|
+
## 开发
|
|
60
|
+
|
|
61
|
+
环境要求:Node.js `^22.19.0 || >=24.0.0`、pnpm `11.7`。
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
pnpm install
|
|
65
|
+
pnpm run check # typecheck + test + build + publint,与 CI 相同
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
单条命令:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
pnpm run typecheck # host + client 双面
|
|
72
|
+
pnpm test # 纯函数行助手的 vitest 单测
|
|
73
|
+
pnpm run build # tsc d.ts + tsdown(lib/index.js 与 lib/client.cjs)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
客户端产物是自包含 bundle:React、Cordis、ui-slots、ui-primitives 由 Web 壳供给,CSS Modules 内联,其余 `@deepseek-ai/*` 只做类型导入(构建期的 purity 检查强制)。
|
|
77
|
+
|
|
78
|
+
CI([`.github/workflows/ci.yml`](.github/workflows/ci.yml))在 Node 22.22.0 与 24.x 上运行同一组门禁;tag 推送时校验 release tag 与包版本一致,审计 npm tarball 的文件清单,并运行消费者冒烟测试:把打包产物安装进一个临时工程(peer 范围对真实 registry 解析),再导入所有 Node 侧入口。
|
|
79
|
+
|
|
80
|
+
### 发布
|
|
81
|
+
|
|
82
|
+
发布由 **Release** 工作流([`.github/workflows/release.yml`](.github/workflows/release.yml))自动化:GitHub Release 一经发布即触发。它要求 release tag 与包版本一致(可选 `v` 前缀会被去掉),重跑完整质量门禁,打包 tarball,并通过 **OIDC trusted publishing** 带 **provenance** 发布到 npm——不涉及长期有效的 `NPM_TOKEN` secret。
|
|
83
|
+
|
|
84
|
+
一次性配置:在 npmjs.com 为 `@jcy2387/dsh-models-input-modalities` 配置 [trusted publishing](https://docs.npmjs.com/trusted-publishing),授权仓库 `DamonBao/dsh-models-input-modalities` 与工作流 `release.yml`(不填 environment)。
|
|
85
|
+
|
|
86
|
+
dist-tag 跟随 GitHub Release 的 pre-release 勾选:正式 release(不勾选)发布到 `latest`——包括 rc 版本;pre-release 发布到按版本推导的频道 tag(`0.1.1-alpha.2` → `alpha`,`0.1.1-rc.1` → `rc`)。工作流是幂等的——npm 上已存在的版本会被跳过,部分失败后重跑只补发缺失的包。
|
|
87
|
+
|
|
88
|
+
一次典型发布:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
# 先更新 package.json 里的 version,然后:
|
|
92
|
+
pnpm run check
|
|
93
|
+
VERSION="$(node -p "require('./package.json').version")"
|
|
94
|
+
git commit -am "release: $VERSION"
|
|
95
|
+
git tag "$VERSION"
|
|
96
|
+
git push origin main --tags
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
再为该 tag 创建并发布 GitHub Release。
|
|
100
|
+
|
|
101
|
+
Dependabot 每周检查 GitHub Actions 依赖。npm 版本更新有意未启用 Dependabot:它重新生成的 `pnpm-lock.yaml` 不含 workspace overrides,其 PR 无法通过 `pnpm install --frozen-lockfile`——请用 `pnpm update` 手动升级依赖。
|
|
102
|
+
|
|
103
|
+
### 仓库结构
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
.
|
|
107
|
+
├─ src/
|
|
108
|
+
│ ├─ index.ts # Host 半边:刻意留空的 apply(纯浏览器插件)
|
|
109
|
+
│ ├─ image-input.ts # 每模型 input 声明的纯函数行助手
|
|
110
|
+
│ └─ client/ # Web 半边:输入模态折叠区(controller、card、locales)
|
|
111
|
+
├─ tests/ # 纯函数行助手的 vitest 单测
|
|
112
|
+
├─ build/ # 自包含客户端 bundle 的 tsdown 预设
|
|
113
|
+
├─ .github/workflows/ci.yml # 校验 + tarball 审计 + 消费者冒烟
|
|
114
|
+
├─ .github/workflows/release.yml # GitHub Release 触发 npm 发布
|
|
115
|
+
├─ cordis.patch.yml
|
|
116
|
+
└─ README.md / README.zh.md
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 已知边界
|
|
120
|
+
|
|
121
|
+
- 休眠(尚未配置)的提供方卡片不渲染折叠区;新建的自定义提供方在**保存之后**才出现。
|
|
122
|
+
- 路由级 `defaultInput` 与内置提供方目录模型的 `modelOverrides` 不在本插件范围内,仍直接在 `$DSH_HOME/settings.yaml` 中设置。
|
|
123
|
+
- 只读设置部署中折叠区可见但不可保存。
|
|
124
|
+
|
|
125
|
+
## 许可
|
|
126
|
+
|
|
127
|
+
[MIT](LICENSE) © jcy2387
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# One installable row mounts the plugin. The Host half is an empty apply; the
|
|
2
|
+
# Web client half (the package's dsh.client manifest) carries the whole
|
|
3
|
+
# feature: a per-model input-modality selector inside every llm-pi-ai provider
|
|
4
|
+
# card on the Models settings page. Provider routes themselves are NOT
|
|
5
|
+
# pre-written here — create and edit them entirely from the Models page.
|
|
6
|
+
- insert:
|
|
7
|
+
- id: models-input-modalities
|
|
8
|
+
name: '@jcy2387/dsh-models-input-modalities'
|
package/lib/client.cjs
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@jcy2387/dsh-models-input-modalities",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
9
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
10
|
+
//#region src/client/controller.ts
|
|
11
|
+
/** The settings namespace every pi-ai provider card addresses. */
|
|
12
|
+
const NS$1 = "llm-pi-ai";
|
|
13
|
+
/** Read one plain-object path; anything off-path answers undefined. */
|
|
14
|
+
function at(source, path) {
|
|
15
|
+
let current = source;
|
|
16
|
+
for (const key of path) {
|
|
17
|
+
if (typeof current !== "object" || current === null || Array.isArray(current)) return void 0;
|
|
18
|
+
current = current[key];
|
|
19
|
+
}
|
|
20
|
+
return current;
|
|
21
|
+
}
|
|
22
|
+
/** Coerce a stored models value into open row records. */
|
|
23
|
+
function rows(value) {
|
|
24
|
+
return Array.isArray(value) ? value.map((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry) ? entry : {}) : [];
|
|
25
|
+
}
|
|
26
|
+
/** Joins the settings Remote's document view and fenced writes for one card. */
|
|
27
|
+
var ImageInputController = class {
|
|
28
|
+
ctx;
|
|
29
|
+
/**
|
|
30
|
+
* @param ctx - the plugin's client context, which declares `remote.settings`
|
|
31
|
+
* in its own `inject`.
|
|
32
|
+
*/
|
|
33
|
+
constructor(ctx) {
|
|
34
|
+
this.ctx = ctx;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Read one provider's model rows and the revision fence for writing them.
|
|
38
|
+
* @param entry - the card's directory row (its settings address names the profile).
|
|
39
|
+
* @returns the view, or undefined when the settings face or namespace is unavailable.
|
|
40
|
+
*/
|
|
41
|
+
async load(entry) {
|
|
42
|
+
const response = await this.ctx.remote.settings.describe();
|
|
43
|
+
if (!response.ok) return void 0;
|
|
44
|
+
const namespace = response.value.namespaces.find((view) => view.ns === NS$1);
|
|
45
|
+
if (namespace === void 0) return void 0;
|
|
46
|
+
const path = [...entry.settingsPath, "models"];
|
|
47
|
+
const userModel = at(namespace.user, path);
|
|
48
|
+
const fromUser = Array.isArray(userModel);
|
|
49
|
+
return {
|
|
50
|
+
writable: response.value.writable,
|
|
51
|
+
revision: namespace.revision,
|
|
52
|
+
models: rows(fromUser ? userModel : at(namespace.value, path)),
|
|
53
|
+
fromUser
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Write the rows back as the profile's whole `models` array, under the fence
|
|
58
|
+
* the load answered. The array is replaced by value — the adapter's own
|
|
59
|
+
* semantics — so untouched rows ride along exactly as stored.
|
|
60
|
+
* @param entry - the card's directory row.
|
|
61
|
+
* @param models - the edited rows.
|
|
62
|
+
* @param revision - the fence from the load this draft was opened at.
|
|
63
|
+
* @returns the write outcome the card renders from.
|
|
64
|
+
*/
|
|
65
|
+
async save(entry, models, revision) {
|
|
66
|
+
const value = models;
|
|
67
|
+
const ops = [{
|
|
68
|
+
op: "set",
|
|
69
|
+
path: [...entry.settingsPath, "models"],
|
|
70
|
+
value
|
|
71
|
+
}];
|
|
72
|
+
const response = await this.ctx.remote.settings.mutate(NS$1, ops, revision);
|
|
73
|
+
if (response.ok) return {
|
|
74
|
+
kind: "written",
|
|
75
|
+
revision: response.value.revision
|
|
76
|
+
};
|
|
77
|
+
const { code, message } = response.error;
|
|
78
|
+
return code === "settings/conflict" ? {
|
|
79
|
+
kind: "conflict",
|
|
80
|
+
message
|
|
81
|
+
} : {
|
|
82
|
+
kind: "refused",
|
|
83
|
+
message
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/image-input.ts
|
|
89
|
+
/**
|
|
90
|
+
* The choice a row's stored `input` displays. Absent and empty mean the same
|
|
91
|
+
* inheritance — the installed catalog's modalities, then the route's
|
|
92
|
+
* `defaultInput` — and any list naming `image` is the image-capable claim
|
|
93
|
+
* however else it is spelled.
|
|
94
|
+
* @param row - one stored model row.
|
|
95
|
+
* @returns the choice the row's select shows.
|
|
96
|
+
*/
|
|
97
|
+
function imageInputChoice(row) {
|
|
98
|
+
const value = row["input"];
|
|
99
|
+
if (!Array.isArray(value) || value.length === 0) return "inherit";
|
|
100
|
+
return value.includes("image") ? "image" : "text";
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The row with one choice applied: `inherit` removes the field, the others
|
|
104
|
+
* store exactly the modality list the adapter reads. Every other field,
|
|
105
|
+
* including ones this card never shows, survives.
|
|
106
|
+
* @param row - the row to patch.
|
|
107
|
+
* @param choice - the selected state.
|
|
108
|
+
* @returns a new row carrying the choice.
|
|
109
|
+
*/
|
|
110
|
+
function withImageInput(row, choice) {
|
|
111
|
+
const next = { ...row };
|
|
112
|
+
delete next["input"];
|
|
113
|
+
if (choice === "text") next["input"] = ["text"];
|
|
114
|
+
else if (choice === "image") next["input"] = ["text", "image"];
|
|
115
|
+
return next;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Read a select's submitted value. The DOM hands over a bare string, so an
|
|
119
|
+
* unrecognized one is refused rather than cast into the union.
|
|
120
|
+
* @param value - the submitted option value.
|
|
121
|
+
* @returns the choice, or undefined for anything else.
|
|
122
|
+
*/
|
|
123
|
+
function parseImageInputChoice(value) {
|
|
124
|
+
return value === "inherit" || value === "text" || value === "image" ? value : void 0;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The row's model id for labels.
|
|
128
|
+
* @param row - one stored model row.
|
|
129
|
+
* @param index - the row's zero-based position.
|
|
130
|
+
* @returns the id, or a positional name for an id-less row.
|
|
131
|
+
*/
|
|
132
|
+
function rowId(row, index) {
|
|
133
|
+
const id = row["id"];
|
|
134
|
+
return typeof id === "string" && id.length > 0 ? id : `#${String(index + 1)}`;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region \0dsh-css:src/client/styles.module.css.mjs
|
|
138
|
+
const css = ".nU9wWG_fold{border:1px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-alias-bg-layer-3,transparent);border-radius:10px;margin:8px 0 0}.nU9wWG_summary{cursor:pointer;color:var(--dsw-alias-label-primary,inherit);user-select:none;padding:10px 14px;font-size:13px;font-weight:600}.nU9wWG_body{flex-direction:column;gap:10px;padding:2px 14px 14px;display:flex}.nU9wWG_row{align-items:center;gap:10px;display:flex}.nU9wWG_rowId{text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dsw-alias-label-primary,inherit);flex:auto;font-size:13px;overflow:hidden}.nU9wWG_select{min-width:9em;color:var(--dsw-alias-label-primary,inherit);border:1px solid var(--dsw-alias-border-l2,#0000001f);border-radius:8px;flex:none;padding:6px 8px;font-size:13px}.nU9wWG_hint,.nU9wWG_status{color:var(--dsw-alias-label-tertiary,inherit);margin:0;font-size:12px;line-height:1.5}.nU9wWG_error{color:var(--dsw-alias-status-danger,#d64545);margin:0;font-size:12px;line-height:1.5}.nU9wWG_footer{align-items:center;gap:10px;display:flex}";
|
|
139
|
+
const tagId = "@jcy2387/dsh-models-input-modalities/styles.module.css";
|
|
140
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
141
|
+
const tag = document.createElement("style");
|
|
142
|
+
tag.dataset.plugin = "@jcy2387/dsh-models-input-modalities";
|
|
143
|
+
tag.dataset.pluginCss = tagId;
|
|
144
|
+
tag.textContent = css;
|
|
145
|
+
document.head.appendChild(tag);
|
|
146
|
+
}
|
|
147
|
+
var styles_module_css_default = {
|
|
148
|
+
"footer": "nU9wWG_footer",
|
|
149
|
+
"body": "nU9wWG_body",
|
|
150
|
+
"fold": "nU9wWG_fold",
|
|
151
|
+
"rowId": "nU9wWG_rowId",
|
|
152
|
+
"select": "nU9wWG_select",
|
|
153
|
+
"hint": "nU9wWG_hint",
|
|
154
|
+
"summary": "nU9wWG_summary",
|
|
155
|
+
"error": "nU9wWG_error",
|
|
156
|
+
"status": "nU9wWG_status",
|
|
157
|
+
"row": "nU9wWG_row"
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/client/ImageInputCard.tsx
|
|
161
|
+
/**
|
|
162
|
+
* One pi-ai provider card's image-input fold: the per-model modality claim the
|
|
163
|
+
* Models page's own form does not carry. The fold loads the provider's stored
|
|
164
|
+
* rows when first opened, edits them locally, and writes the whole `models`
|
|
165
|
+
* array back under the revision fence the load answered — the same array
|
|
166
|
+
* semantics the page's own cards use.
|
|
167
|
+
*/
|
|
168
|
+
/**
|
|
169
|
+
* Render the image-input fold of one provider card.
|
|
170
|
+
* @param props - the card's directory row plus the bound face and copy.
|
|
171
|
+
* @returns the fold, or nothing while the provider is still a dormant row.
|
|
172
|
+
*/
|
|
173
|
+
function ImageInputCard(props) {
|
|
174
|
+
const { provider, configured, t, loadModels, saveModels } = props;
|
|
175
|
+
const [status, setStatus] = (0, react.useState)("idle");
|
|
176
|
+
const [view, setView] = (0, react.useState)(void 0);
|
|
177
|
+
const [rows, setRows] = (0, react.useState)([]);
|
|
178
|
+
const [failure, setFailure] = (0, react.useState)(void 0);
|
|
179
|
+
const [saved, setSaved] = (0, react.useState)(false);
|
|
180
|
+
const reload = (0, react.useCallback)(async () => {
|
|
181
|
+
setStatus("loading");
|
|
182
|
+
setFailure(void 0);
|
|
183
|
+
setSaved(false);
|
|
184
|
+
const loaded = await loadModels(provider);
|
|
185
|
+
if (loaded === void 0) {
|
|
186
|
+
setView(void 0);
|
|
187
|
+
setFailure(t("loadFailed"));
|
|
188
|
+
setStatus("ready");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
setView(loaded);
|
|
192
|
+
setRows(loaded.models.map((row) => ({ ...row })));
|
|
193
|
+
setStatus("ready");
|
|
194
|
+
}, [
|
|
195
|
+
loadModels,
|
|
196
|
+
provider,
|
|
197
|
+
t
|
|
198
|
+
]);
|
|
199
|
+
if (configured !== true) return null;
|
|
200
|
+
const dirty = view !== void 0 && JSON.stringify(rows) !== JSON.stringify(view.models);
|
|
201
|
+
const choose = (index, choice) => {
|
|
202
|
+
setRows((current) => current.map((row, at) => at === index ? withImageInput(row, choice) : row));
|
|
203
|
+
};
|
|
204
|
+
const submit = async () => {
|
|
205
|
+
if (view === void 0) return;
|
|
206
|
+
setStatus("saving");
|
|
207
|
+
const outcome = await saveModels(provider, rows, view.revision);
|
|
208
|
+
if (outcome.kind === "written") {
|
|
209
|
+
setView({
|
|
210
|
+
...view,
|
|
211
|
+
revision: outcome.revision,
|
|
212
|
+
models: rows.map((row) => ({ ...row })),
|
|
213
|
+
fromUser: true
|
|
214
|
+
});
|
|
215
|
+
setSaved(true);
|
|
216
|
+
setStatus("ready");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
setFailure(outcome.kind === "conflict" ? t("conflict") : outcome.message);
|
|
220
|
+
if (outcome.kind === "conflict") await reload();
|
|
221
|
+
else setStatus("ready");
|
|
222
|
+
};
|
|
223
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
224
|
+
className: styles_module_css_default["fold"],
|
|
225
|
+
onToggle: (event) => {
|
|
226
|
+
if (event.currentTarget.open && status === "idle") reload();
|
|
227
|
+
},
|
|
228
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
229
|
+
className: styles_module_css_default["summary"],
|
|
230
|
+
children: t("title")
|
|
231
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
232
|
+
className: styles_module_css_default["body"],
|
|
233
|
+
children: [
|
|
234
|
+
status === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
235
|
+
className: styles_module_css_default["status"],
|
|
236
|
+
children: t("loading")
|
|
237
|
+
}) : null,
|
|
238
|
+
status !== "loading" && view === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
239
|
+
className: styles_module_css_default["error"],
|
|
240
|
+
children: failure ?? t("loadFailed")
|
|
241
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
242
|
+
className: styles_module_css_default["footer"],
|
|
243
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
244
|
+
variant: "ghost",
|
|
245
|
+
size: "sm",
|
|
246
|
+
onClick: () => {
|
|
247
|
+
reload();
|
|
248
|
+
},
|
|
249
|
+
children: t("retry")
|
|
250
|
+
})
|
|
251
|
+
})] }) : null,
|
|
252
|
+
status !== "loading" && view !== void 0 ? view.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
253
|
+
className: styles_module_css_default["hint"],
|
|
254
|
+
children: t("empty")
|
|
255
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
256
|
+
view.fromUser ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
257
|
+
className: styles_module_css_default["hint"],
|
|
258
|
+
children: t("inheritsHint")
|
|
259
|
+
}),
|
|
260
|
+
rows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
261
|
+
className: styles_module_css_default["row"],
|
|
262
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
263
|
+
className: styles_module_css_default["rowId"],
|
|
264
|
+
children: rowId(row, index)
|
|
265
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
266
|
+
className: styles_module_css_default["select"],
|
|
267
|
+
value: imageInputChoice(row),
|
|
268
|
+
"aria-label": `${t("title")} ${rowId(row, index)}`,
|
|
269
|
+
disabled: !view.writable || status === "saving",
|
|
270
|
+
onChange: (event) => {
|
|
271
|
+
const next = parseImageInputChoice(event.target.value);
|
|
272
|
+
if (next !== void 0) choose(index, next);
|
|
273
|
+
},
|
|
274
|
+
children: [
|
|
275
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
276
|
+
value: "inherit",
|
|
277
|
+
children: t("choiceDefault")
|
|
278
|
+
}),
|
|
279
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
280
|
+
value: "text",
|
|
281
|
+
children: t("choiceText")
|
|
282
|
+
}),
|
|
283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
284
|
+
value: "image",
|
|
285
|
+
children: t("choiceImage")
|
|
286
|
+
})
|
|
287
|
+
]
|
|
288
|
+
})]
|
|
289
|
+
}, index)),
|
|
290
|
+
failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
291
|
+
className: styles_module_css_default["error"],
|
|
292
|
+
children: failure
|
|
293
|
+
}) : null,
|
|
294
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
295
|
+
className: styles_module_css_default["footer"],
|
|
296
|
+
children: [
|
|
297
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
298
|
+
variant: "outline",
|
|
299
|
+
size: "sm",
|
|
300
|
+
disabled: !view.writable || status === "saving" || !dirty,
|
|
301
|
+
onClick: () => {
|
|
302
|
+
submit();
|
|
303
|
+
},
|
|
304
|
+
children: status === "saving" ? t("saving") : t("save")
|
|
305
|
+
}),
|
|
306
|
+
failure !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
307
|
+
variant: "ghost",
|
|
308
|
+
size: "sm",
|
|
309
|
+
disabled: status === "saving",
|
|
310
|
+
onClick: () => {
|
|
311
|
+
reload();
|
|
312
|
+
},
|
|
313
|
+
children: t("retry")
|
|
314
|
+
}) : null,
|
|
315
|
+
saved ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
316
|
+
className: styles_module_css_default["status"],
|
|
317
|
+
children: t("saved")
|
|
318
|
+
}) : null,
|
|
319
|
+
view.writable ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
320
|
+
className: styles_module_css_default["hint"],
|
|
321
|
+
children: t("readOnly")
|
|
322
|
+
})
|
|
323
|
+
]
|
|
324
|
+
})
|
|
325
|
+
] }) : null
|
|
326
|
+
]
|
|
327
|
+
})]
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/client/locales.ts
|
|
332
|
+
/** Copy dictionaries for the image-input card. */
|
|
333
|
+
/** English strings (the key-set source of truth for this pair). */
|
|
334
|
+
const en = {
|
|
335
|
+
title: "Input modalities",
|
|
336
|
+
loading: "Loading the model list…",
|
|
337
|
+
loadFailed: "Loading the model configuration failed.",
|
|
338
|
+
retry: "Retry",
|
|
339
|
+
empty: "No explicit model list yet — add models in the catalog above, then declare their input modalities here.",
|
|
340
|
+
inheritsHint: "Showing the inherited model list; saving copies it into your user settings.",
|
|
341
|
+
readOnly: "The settings document is read-only in this deployment.",
|
|
342
|
+
choiceDefault: "Provider default",
|
|
343
|
+
choiceText: "Text only",
|
|
344
|
+
choiceImage: "Text and image",
|
|
345
|
+
save: "Save",
|
|
346
|
+
saving: "Saving…",
|
|
347
|
+
saved: "Saved. The adapter picks it up on its next request.",
|
|
348
|
+
conflict: "These settings changed elsewhere while this card was open; the latest values were reloaded."
|
|
349
|
+
};
|
|
350
|
+
/** Chinese strings (same keys as {@link en}). */
|
|
351
|
+
const zh = {
|
|
352
|
+
title: "输入模态",
|
|
353
|
+
loading: "正在读取模型列表…",
|
|
354
|
+
loadFailed: "读取模型配置失败。",
|
|
355
|
+
retry: "重试",
|
|
356
|
+
empty: "还没有显式模型列表——请先在上方模型目录中添加模型,再回到这里声明输入模态。",
|
|
357
|
+
inheritsHint: "当前显示的是继承的模型列表;保存会将其复制到你的用户设置层。",
|
|
358
|
+
readOnly: "当前部署的设置文档为只读。",
|
|
359
|
+
choiceDefault: "提供方默认",
|
|
360
|
+
choiceText: "仅文本",
|
|
361
|
+
choiceImage: "文本和图片",
|
|
362
|
+
save: "保存",
|
|
363
|
+
saving: "保存中…",
|
|
364
|
+
saved: "已保存。适配器会在下一次请求时生效。",
|
|
365
|
+
conflict: "这张卡片打开期间,设置已被其他地方改动;已重新加载最新值。"
|
|
366
|
+
};
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/client/index.ts
|
|
369
|
+
/** Dictionary namespace owned by this plugin. */
|
|
370
|
+
const NS = "settings.models.imageInput";
|
|
371
|
+
/** The effect label prefix. */
|
|
372
|
+
const PKG = "@jcy2387/dsh-models-input-modalities";
|
|
373
|
+
/** Required browser services. */
|
|
374
|
+
const inject = [
|
|
375
|
+
"slots",
|
|
376
|
+
"locale",
|
|
377
|
+
"remote",
|
|
378
|
+
"remote.settings"
|
|
379
|
+
];
|
|
380
|
+
/**
|
|
381
|
+
* Register the image-input fold on every llm-pi-ai provider card once the
|
|
382
|
+
* Models section has declared the seat.
|
|
383
|
+
* @param ctx - the plugin's client context.
|
|
384
|
+
*/
|
|
385
|
+
function apply(ctx) {
|
|
386
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
387
|
+
zh,
|
|
388
|
+
en
|
|
389
|
+
}), `${PKG}: dictionaries`);
|
|
390
|
+
const controller = new ImageInputController(ctx);
|
|
391
|
+
const face = {
|
|
392
|
+
loadModels: (entry) => controller.load(entry),
|
|
393
|
+
saveModels: (entry, models, revision) => controller.save(entry, models, revision)
|
|
394
|
+
};
|
|
395
|
+
ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
|
|
396
|
+
name: "settings.models.provider-card",
|
|
397
|
+
key: "llm-pi-ai",
|
|
398
|
+
locale: NS,
|
|
399
|
+
inject: () => face
|
|
400
|
+
}, ImageInputCard));
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
exports.apply = apply;
|
|
404
|
+
exports.inject = inject;
|
|
405
|
+
return module.exports;
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
//# sourceMappingURL=client.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.cjs","names":["NS","useState","useCallback","css","Button"],"sources":["../src/client/controller.ts","../src/image-input.ts","../src/client/ImageInputCard.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/** Settings reads and writes for the image-input card, over the settings Remote. */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type { SettingsPathOpView } from '@deepseek-ai/dsh-api-remotes/client'\nimport type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport type { JsonValue } from '@deepseek-ai/dsh-util-values'\nimport type { ModelRow } from '../image-input.ts'\n\n/** The settings namespace every pi-ai provider card addresses. */\nconst NS = 'llm-pi-ai'\n\n/** What one load answers for a provider card. */\nexport interface ImageInputView {\n /** Whether the deployment accepts settings writes at all. */\n writable: boolean\n /** Revision fence the next save must carry. */\n revision: number\n /** The rows to show: the user layer's when it owns the list, else the effective ones. */\n models: readonly ModelRow[]\n /** Whether the shown rows already live in the user layer. */\n fromUser: boolean\n}\n\n/** What one save answered. */\nexport type ImageInputSaveOutcome =\n | { readonly kind: 'written'; readonly revision: number }\n | { readonly kind: 'conflict'; readonly message: string }\n | { readonly kind: 'refused'; readonly message: string }\n\n/** Read one plain-object path; anything off-path answers undefined. */\nfunction at(source: unknown, path: readonly string[]): unknown {\n let current: unknown = source\n for (const key of path) {\n if (typeof current !== 'object' || current === null || Array.isArray(current)) return undefined\n current = (current as Record<string, unknown>)[key]\n }\n return current\n}\n\n/** Coerce a stored models value into open row records. */\nfunction rows(value: unknown): ModelRow[] {\n return Array.isArray(value)\n ? value.map(entry =>\n typeof entry === 'object' && entry !== null && !Array.isArray(entry) ? entry as ModelRow : {})\n : []\n}\n\n/** Joins the settings Remote's document view and fenced writes for one card. */\nexport class ImageInputController {\n /**\n * @param ctx - the plugin's client context, which declares `remote.settings`\n * in its own `inject`.\n */\n constructor(private readonly ctx: ClientContext) {}\n\n /**\n * Read one provider's model rows and the revision fence for writing them.\n * @param entry - the card's directory row (its settings address names the profile).\n * @returns the view, or undefined when the settings face or namespace is unavailable.\n */\n async load(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined> {\n const response = await this.ctx.remote.settings.describe()\n if (!response.ok) return undefined\n const namespace = response.value.namespaces.find(view => view.ns === NS)\n if (namespace === undefined) return undefined\n const path = [...entry.settingsPath, 'models']\n const userModel = at(namespace.user, path)\n const fromUser = Array.isArray(userModel)\n return {\n writable: response.value.writable,\n revision: namespace.revision,\n models: rows(fromUser ? userModel : at(namespace.value, path)),\n fromUser,\n }\n }\n\n /**\n * Write the rows back as the profile's whole `models` array, under the fence\n * the load answered. The array is replaced by value — the adapter's own\n * semantics — so untouched rows ride along exactly as stored.\n * @param entry - the card's directory row.\n * @param models - the edited rows.\n * @param revision - the fence from the load this draft was opened at.\n * @returns the write outcome the card renders from.\n */\n async save(\n entry: ProviderDirectoryEntry,\n models: readonly ModelRow[],\n revision: number,\n ): Promise<ImageInputSaveOutcome> {\n // The rows came out of a stored JSON document and the edit only ever sets\n // string arrays, so the array is JSON by construction.\n const value = models as unknown as JsonValue\n const ops: SettingsPathOpView[] = [{ op: 'set', path: [...entry.settingsPath, 'models'], value }]\n const response = await this.ctx.remote.settings.mutate(NS, ops, revision)\n if (response.ok) return { kind: 'written', revision: response.value.revision }\n const { code, message } = response.error\n return code === 'settings/conflict' ? { kind: 'conflict', message } : { kind: 'refused', message }\n }\n}\n","/** Pure row helpers for the per-model image-input claim. */\n\n/** One configured model row, structurally open so hidden fields survive an edit. */\nexport type ModelRow = Record<string, unknown>\n\n/** The three image-input states a row's select offers. */\nexport type ImageInputChoice = 'inherit' | 'text' | 'image'\n\n/**\n * The choice a row's stored `input` displays. Absent and empty mean the same\n * inheritance — the installed catalog's modalities, then the route's\n * `defaultInput` — and any list naming `image` is the image-capable claim\n * however else it is spelled.\n * @param row - one stored model row.\n * @returns the choice the row's select shows.\n */\nexport function imageInputChoice(row: ModelRow): ImageInputChoice {\n const value = row['input']\n if (!Array.isArray(value) || value.length === 0) return 'inherit'\n return value.includes('image') ? 'image' : 'text'\n}\n\n/**\n * The row with one choice applied: `inherit` removes the field, the others\n * store exactly the modality list the adapter reads. Every other field,\n * including ones this card never shows, survives.\n * @param row - the row to patch.\n * @param choice - the selected state.\n * @returns a new row carrying the choice.\n */\nexport function withImageInput(row: ModelRow, choice: ImageInputChoice): ModelRow {\n const next = { ...row }\n delete next['input']\n if (choice === 'text') next['input'] = ['text']\n else if (choice === 'image') next['input'] = ['text', 'image']\n return next\n}\n\n/**\n * Read a select's submitted value. The DOM hands over a bare string, so an\n * unrecognized one is refused rather than cast into the union.\n * @param value - the submitted option value.\n * @returns the choice, or undefined for anything else.\n */\nexport function parseImageInputChoice(value: string): ImageInputChoice | undefined {\n return value === 'inherit' || value === 'text' || value === 'image' ? value : undefined\n}\n\n/**\n * The row's model id for labels.\n * @param row - one stored model row.\n * @param index - the row's zero-based position.\n * @returns the id, or a positional name for an id-less row.\n */\nexport function rowId(row: ModelRow, index: number): string {\n const id = row['id']\n return typeof id === 'string' && id.length > 0 ? id : `#${String(index + 1)}`\n}\n","/**\n * One pi-ai provider card's image-input fold: the per-model modality claim the\n * Models page's own form does not carry. The fold loads the provider's stored\n * rows when first opened, edits them locally, and writes the whole `models`\n * array back under the revision fence the load answered — the same array\n * semantics the page's own cards use.\n */\n\nimport { useCallback, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport { Button } from '@deepseek-ai/dsh-client-ui-primitives'\nimport type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport type { ImageInputSaveOutcome, ImageInputView } from './controller.ts'\nimport { imageInputChoice, parseImageInputChoice, rowId, withImageInput } from '../image-input.ts'\nimport type { ImageInputChoice, ModelRow } from '../image-input.ts'\nimport css from './styles.module.css'\n\n/** The registration-side face this card receives. */\nexport interface ImageInputFace {\n /** Read the provider's rows and revision fence; undefined when the settings face is unavailable. */\n loadModels(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined>\n /** Write the rows back under the fence the load answered. */\n saveModels(\n entry: ProviderDirectoryEntry,\n models: readonly ModelRow[],\n revision: number,\n ): Promise<ImageInputSaveOutcome>\n}\n\n/** Props the provider-card slot binds. */\nexport type ImageInputCardProps =\n PropsRuntime<'settings.models.provider-card'>\n & PropsLocale<'settings.models.imageInput'>\n & InjectFace<ImageInputFace>\n\n/** Lifecycle of one fold: closed, loading, editable, or writing. */\ntype Status = 'idle' | 'loading' | 'ready' | 'saving'\n\n/**\n * Render the image-input fold of one provider card.\n * @param props - the card's directory row plus the bound face and copy.\n * @returns the fold, or nothing while the provider is still a dormant row.\n */\nexport function ImageInputCard(props: ImageInputCardProps): ReactNode {\n const { provider, configured, t, loadModels, saveModels } = props\n const [status, setStatus] = useState<Status>('idle')\n const [view, setView] = useState<ImageInputView | undefined>(undefined)\n const [rows, setRows] = useState<readonly ModelRow[]>([])\n const [failure, setFailure] = useState<string | undefined>(undefined)\n const [saved, setSaved] = useState(false)\n\n const reload = useCallback(async (): Promise<void> => {\n setStatus('loading')\n setFailure(undefined)\n setSaved(false)\n const loaded = await loadModels(provider)\n if (loaded === undefined) {\n setView(undefined)\n setFailure(t('loadFailed'))\n setStatus('ready')\n return\n }\n setView(loaded)\n setRows(loaded.models.map(row => ({ ...row })))\n setStatus('ready')\n }, [loadModels, provider, t])\n\n // A dormant directory row has no profile to read models from; the create\n // card dispatches this seat only after the provider is saved anyway.\n if (configured !== true) return null\n\n const dirty = view !== undefined && JSON.stringify(rows) !== JSON.stringify(view.models)\n\n const choose = (index: number, choice: ImageInputChoice): void => {\n setRows(current => current.map((row, at) => at === index ? withImageInput(row, choice) : row))\n }\n\n const submit = async (): Promise<void> => {\n if (view === undefined) return\n setStatus('saving')\n const outcome = await saveModels(provider, rows, view.revision)\n if (outcome.kind === 'written') {\n setView({ ...view, revision: outcome.revision, models: rows.map(row => ({ ...row })), fromUser: true })\n setSaved(true)\n setStatus('ready')\n return\n }\n setFailure(outcome.kind === 'conflict' ? t('conflict') : outcome.message)\n if (outcome.kind === 'conflict') await reload()\n else setStatus('ready')\n }\n\n return (\n <details\n className={css['fold']}\n onToggle={(event) => {\n // Load on first open only: a reopened fold keeps the loaded (possibly\n // just-saved) view, and every write is revision-fenced regardless.\n if (event.currentTarget.open && status === 'idle') void reload()\n }}\n >\n <summary className={css['summary']}>{t('title')}</summary>\n <div className={css['body']}>\n {status === 'loading' ? <p className={css['status']}>{t('loading')}</p> : null}\n {status !== 'loading' && view === undefined\n ? (\n <>\n <p className={css['error']}>{failure ?? t('loadFailed')}</p>\n <div className={css['footer']}>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => { void reload() }}>{t('retry')}</Button>\n </div>\n </>\n )\n : null}\n {status !== 'loading' && view !== undefined\n ? (view.models.length === 0\n ? <p className={css['hint']}>{t('empty')}</p>\n : (\n <>\n {view.fromUser ? null : <p className={css['hint']}>{t('inheritsHint')}</p>}\n {rows.map((row, index) => (\n <label key={index} className={css['row']}>\n <span className={css['rowId']}>{rowId(row, index)}</span>\n <select\n className={css['select']}\n value={imageInputChoice(row)}\n aria-label={`${t('title')} ${rowId(row, index)}`}\n disabled={!view.writable || status === 'saving'}\n onChange={(event) => {\n const next = parseImageInputChoice(event.target.value)\n if (next !== undefined) choose(index, next)\n }}\n >\n <option value=\"inherit\">{t('choiceDefault')}</option>\n <option value=\"text\">{t('choiceText')}</option>\n <option value=\"image\">{t('choiceImage')}</option>\n </select>\n </label>\n ))}\n {failure !== undefined ? <p className={css['error']}>{failure}</p> : null}\n <div className={css['footer']}>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={!view.writable || status === 'saving' || !dirty}\n onClick={() => { void submit() }}\n >\n {status === 'saving' ? t('saving') : t('save')}\n </Button>\n {failure !== undefined\n ? (\n <Button variant=\"ghost\" size=\"sm\" disabled={status === 'saving'} onClick={() => { void reload() }}>\n {t('retry')}\n </Button>\n )\n : null}\n {saved ? <p className={css['status']}>{t('saved')}</p> : null}\n {view.writable ? null : <p className={css['hint']}>{t('readOnly')}</p>}\n </div>\n </>\n ))\n : null}\n </div>\n </details>\n )\n}\n","/** Copy dictionaries for the image-input card. */\n\n/** English strings (the key-set source of truth for this pair). */\nexport const en = {\n title: 'Input modalities',\n loading: 'Loading the model list…',\n loadFailed: 'Loading the model configuration failed.',\n retry: 'Retry',\n empty: 'No explicit model list yet — add models in the catalog above, then declare their input modalities here.',\n inheritsHint: 'Showing the inherited model list; saving copies it into your user settings.',\n readOnly: 'The settings document is read-only in this deployment.',\n choiceDefault: 'Provider default',\n choiceText: 'Text only',\n choiceImage: 'Text and image',\n save: 'Save',\n saving: 'Saving…',\n saved: 'Saved. The adapter picks it up on its next request.',\n conflict: 'These settings changed elsewhere while this card was open; the latest values were reloaded.',\n}\n\n/** The settings.models.imageInput namespace key union. */\nexport type ImageInputKey = keyof typeof en\n\n/** Chinese strings (same keys as {@link en}). */\nexport const zh: { [Key in keyof typeof en]: string } = {\n title: '输入模态',\n loading: '正在读取模型列表…',\n loadFailed: '读取模型配置失败。',\n retry: '重试',\n empty: '还没有显式模型列表——请先在上方模型目录中添加模型,再回到这里声明输入模态。',\n inheritsHint: '当前显示的是继承的模型列表;保存会将其复制到你的用户设置层。',\n readOnly: '当前部署的设置文档为只读。',\n choiceDefault: '提供方默认',\n choiceText: '仅文本',\n choiceImage: '文本和图片',\n save: '保存',\n saving: '保存中…',\n saved: '已保存。适配器会在下一次请求时生效。',\n conflict: '这张卡片打开期间,设置已被其他地方改动;已重新加载最新值。',\n}\n","/**\n * Browser half: the per-model image-input fold inside every llm-pi-ai provider\n * card of the Models settings page. The Host half is empty; provider routes\n * are created and edited through the page's own forms, and this plugin only\n * adds the one field those forms do not carry.\n */\n\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the ctx.slots service merge (SlotRegistry).\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: pulls the ctx.locale merge.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the ctx.remote merge (the settings describe/mutate face).\nimport type {} from '@deepseek-ai/dsh-api-remotes/client'\n// Type-only: pulls the 'settings.models.provider-card' SlotMap entry and the\n// ProviderDirectoryEntry owner data.\nimport type {} from '@deepseek-ai/dsh-client-ui-settings-models/client'\nimport { ImageInputController } from './controller.ts'\nimport { ImageInputCard } from './ImageInputCard.tsx'\nimport type { ImageInputFace } from './ImageInputCard.tsx'\nimport { en, zh } from './locales.ts'\nimport type { ImageInputKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Per-model image-input copy on the Models page. */\n 'settings.models.imageInput': ImageInputKey\n }\n}\n\n/** Dictionary namespace owned by this plugin. */\nconst NS = 'settings.models.imageInput'\n\n/** The effect label prefix. */\nconst PKG = '@jcy2387/dsh-models-input-modalities'\n\n/** Required browser services. */\nexport const inject = ['slots', 'locale', 'remote', 'remote.settings']\n\n/**\n * Register the image-input fold on every llm-pi-ai provider card once the\n * Models section has declared the seat.\n * @param ctx - the plugin's client context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), `${PKG}: dictionaries`)\n const controller = new ImageInputController(ctx)\n const face: ImageInputFace = {\n loadModels: entry => controller.load(entry),\n saveModels: (entry, models, revision) => controller.save(entry, models, revision),\n }\n ctx.slots.inject('settings.models.provider-card', () => ctx.slots.register({\n name: 'settings.models.provider-card',\n key: 'llm-pi-ai',\n locale: NS,\n inject: () => face,\n }, ImageInputCard))\n}\n"],"mappings":"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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One pi-ai provider card's image-input fold: the per-model modality claim the
|
|
3
|
+
* Models page's own form does not carry. The fold loads the provider's stored
|
|
4
|
+
* rows when first opened, edits them locally, and writes the whole `models`
|
|
5
|
+
* array back under the revision fence the load answered — the same array
|
|
6
|
+
* semantics the page's own cards use.
|
|
7
|
+
*/
|
|
8
|
+
import type { ReactNode } from 'react';
|
|
9
|
+
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
10
|
+
import type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client';
|
|
11
|
+
import type { ImageInputSaveOutcome, ImageInputView } from './controller.ts';
|
|
12
|
+
import type { ModelRow } from '../image-input.ts';
|
|
13
|
+
/** The registration-side face this card receives. */
|
|
14
|
+
export interface ImageInputFace {
|
|
15
|
+
/** Read the provider's rows and revision fence; undefined when the settings face is unavailable. */
|
|
16
|
+
loadModels(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined>;
|
|
17
|
+
/** Write the rows back under the fence the load answered. */
|
|
18
|
+
saveModels(entry: ProviderDirectoryEntry, models: readonly ModelRow[], revision: number): Promise<ImageInputSaveOutcome>;
|
|
19
|
+
}
|
|
20
|
+
/** Props the provider-card slot binds. */
|
|
21
|
+
export type ImageInputCardProps = PropsRuntime<'settings.models.provider-card'> & PropsLocale<'settings.models.imageInput'> & InjectFace<ImageInputFace>;
|
|
22
|
+
/**
|
|
23
|
+
* Render the image-input fold of one provider card.
|
|
24
|
+
* @param props - the card's directory row plus the bound face and copy.
|
|
25
|
+
* @returns the fold, or nothing while the provider is still a dormant row.
|
|
26
|
+
*/
|
|
27
|
+
export declare function ImageInputCard(props: ImageInputCardProps): ReactNode;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** Settings reads and writes for the image-input card, over the settings Remote. */
|
|
2
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
3
|
+
import type { ProviderDirectoryEntry } from '@deepseek-ai/dsh-client-ui-settings-models/client';
|
|
4
|
+
import type { ModelRow } from '../image-input.ts';
|
|
5
|
+
/** What one load answers for a provider card. */
|
|
6
|
+
export interface ImageInputView {
|
|
7
|
+
/** Whether the deployment accepts settings writes at all. */
|
|
8
|
+
writable: boolean;
|
|
9
|
+
/** Revision fence the next save must carry. */
|
|
10
|
+
revision: number;
|
|
11
|
+
/** The rows to show: the user layer's when it owns the list, else the effective ones. */
|
|
12
|
+
models: readonly ModelRow[];
|
|
13
|
+
/** Whether the shown rows already live in the user layer. */
|
|
14
|
+
fromUser: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** What one save answered. */
|
|
17
|
+
export type ImageInputSaveOutcome = {
|
|
18
|
+
readonly kind: 'written';
|
|
19
|
+
readonly revision: number;
|
|
20
|
+
} | {
|
|
21
|
+
readonly kind: 'conflict';
|
|
22
|
+
readonly message: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'refused';
|
|
25
|
+
readonly message: string;
|
|
26
|
+
};
|
|
27
|
+
/** Joins the settings Remote's document view and fenced writes for one card. */
|
|
28
|
+
export declare class ImageInputController {
|
|
29
|
+
private readonly ctx;
|
|
30
|
+
/**
|
|
31
|
+
* @param ctx - the plugin's client context, which declares `remote.settings`
|
|
32
|
+
* in its own `inject`.
|
|
33
|
+
*/
|
|
34
|
+
constructor(ctx: ClientContext);
|
|
35
|
+
/**
|
|
36
|
+
* Read one provider's model rows and the revision fence for writing them.
|
|
37
|
+
* @param entry - the card's directory row (its settings address names the profile).
|
|
38
|
+
* @returns the view, or undefined when the settings face or namespace is unavailable.
|
|
39
|
+
*/
|
|
40
|
+
load(entry: ProviderDirectoryEntry): Promise<ImageInputView | undefined>;
|
|
41
|
+
/**
|
|
42
|
+
* Write the rows back as the profile's whole `models` array, under the fence
|
|
43
|
+
* the load answered. The array is replaced by value — the adapter's own
|
|
44
|
+
* semantics — so untouched rows ride along exactly as stored.
|
|
45
|
+
* @param entry - the card's directory row.
|
|
46
|
+
* @param models - the edited rows.
|
|
47
|
+
* @param revision - the fence from the load this draft was opened at.
|
|
48
|
+
* @returns the write outcome the card renders from.
|
|
49
|
+
*/
|
|
50
|
+
save(entry: ProviderDirectoryEntry, models: readonly ModelRow[], revision: number): Promise<ImageInputSaveOutcome>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser half: the per-model image-input fold inside every llm-pi-ai provider
|
|
3
|
+
* card of the Models settings page. The Host half is empty; provider routes
|
|
4
|
+
* are created and edited through the page's own forms, and this plugin only
|
|
5
|
+
* adds the one field those forms do not carry.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
8
|
+
import type { ImageInputKey } from './locales.ts';
|
|
9
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
10
|
+
interface LocaleNamespaceMap {
|
|
11
|
+
/** Per-model image-input copy on the Models page. */
|
|
12
|
+
'settings.models.imageInput': ImageInputKey;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Required browser services. */
|
|
16
|
+
export declare const inject: string[];
|
|
17
|
+
/**
|
|
18
|
+
* Register the image-input fold on every llm-pi-ai provider card once the
|
|
19
|
+
* Models section has declared the seat.
|
|
20
|
+
* @param ctx - the plugin's client context.
|
|
21
|
+
*/
|
|
22
|
+
export declare function apply(ctx: ClientContext): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Copy dictionaries for the image-input card. */
|
|
2
|
+
/** English strings (the key-set source of truth for this pair). */
|
|
3
|
+
export declare const en: {
|
|
4
|
+
title: string;
|
|
5
|
+
loading: string;
|
|
6
|
+
loadFailed: string;
|
|
7
|
+
retry: string;
|
|
8
|
+
empty: string;
|
|
9
|
+
inheritsHint: string;
|
|
10
|
+
readOnly: string;
|
|
11
|
+
choiceDefault: string;
|
|
12
|
+
choiceText: string;
|
|
13
|
+
choiceImage: string;
|
|
14
|
+
save: string;
|
|
15
|
+
saving: string;
|
|
16
|
+
saved: string;
|
|
17
|
+
conflict: string;
|
|
18
|
+
};
|
|
19
|
+
/** The settings.models.imageInput namespace key union. */
|
|
20
|
+
export type ImageInputKey = keyof typeof en;
|
|
21
|
+
/** Chinese strings (same keys as {@link en}). */
|
|
22
|
+
export declare const zh: {
|
|
23
|
+
[Key in keyof typeof en]: string;
|
|
24
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Pure row helpers for the per-model image-input claim. */
|
|
2
|
+
/** One configured model row, structurally open so hidden fields survive an edit. */
|
|
3
|
+
export type ModelRow = Record<string, unknown>;
|
|
4
|
+
/** The three image-input states a row's select offers. */
|
|
5
|
+
export type ImageInputChoice = 'inherit' | 'text' | 'image';
|
|
6
|
+
/**
|
|
7
|
+
* The choice a row's stored `input` displays. Absent and empty mean the same
|
|
8
|
+
* inheritance — the installed catalog's modalities, then the route's
|
|
9
|
+
* `defaultInput` — and any list naming `image` is the image-capable claim
|
|
10
|
+
* however else it is spelled.
|
|
11
|
+
* @param row - one stored model row.
|
|
12
|
+
* @returns the choice the row's select shows.
|
|
13
|
+
*/
|
|
14
|
+
export declare function imageInputChoice(row: ModelRow): ImageInputChoice;
|
|
15
|
+
/**
|
|
16
|
+
* The row with one choice applied: `inherit` removes the field, the others
|
|
17
|
+
* store exactly the modality list the adapter reads. Every other field,
|
|
18
|
+
* including ones this card never shows, survives.
|
|
19
|
+
* @param row - the row to patch.
|
|
20
|
+
* @param choice - the selected state.
|
|
21
|
+
* @returns a new row carrying the choice.
|
|
22
|
+
*/
|
|
23
|
+
export declare function withImageInput(row: ModelRow, choice: ImageInputChoice): ModelRow;
|
|
24
|
+
/**
|
|
25
|
+
* Read a select's submitted value. The DOM hands over a bare string, so an
|
|
26
|
+
* unrecognized one is refused rather than cast into the union.
|
|
27
|
+
* @param value - the submitted option value.
|
|
28
|
+
* @returns the choice, or undefined for anything else.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseImageInputChoice(value: string): ImageInputChoice | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* The row's model id for labels.
|
|
33
|
+
* @param row - one stored model row.
|
|
34
|
+
* @param index - the row's zero-based position.
|
|
35
|
+
* @returns the id, or a positional name for an id-less row.
|
|
36
|
+
*/
|
|
37
|
+
export declare function rowId(row: ModelRow, index: number): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jcy2387/dsh-models-input-modalities",
|
|
3
|
+
"displayName": "Models input modalities",
|
|
4
|
+
"description": "DeepSeek Harness Web plugin: per-model input-modality selector on the Models settings page for third-party (pi-ai) providers",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"author": "jcy2387",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org/"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/DamonBao/dsh-models-input-modalities.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/DamonBao/dsh-models-input-modalities#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/DamonBao/dsh-models-input-modalities/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"deepseek-harness",
|
|
26
|
+
"dsh-plugin",
|
|
27
|
+
"models",
|
|
28
|
+
"input-modalities",
|
|
29
|
+
"multimodal",
|
|
30
|
+
"image-input",
|
|
31
|
+
"settings"
|
|
32
|
+
],
|
|
33
|
+
"main": "lib/index.js",
|
|
34
|
+
"types": "lib/types/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./lib/types/index.d.ts",
|
|
38
|
+
"default": "./lib/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./client": {
|
|
41
|
+
"types": "./lib/types/client/index.d.ts",
|
|
42
|
+
"default": "./lib/client.cjs"
|
|
43
|
+
},
|
|
44
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"lib/index.js",
|
|
49
|
+
"lib/client.cjs",
|
|
50
|
+
"lib/client.cjs.map",
|
|
51
|
+
"lib/types/**/*.d.ts",
|
|
52
|
+
"cordis.patch.yml",
|
|
53
|
+
"README.md",
|
|
54
|
+
"README.zh.md",
|
|
55
|
+
"LICENSE"
|
|
56
|
+
],
|
|
57
|
+
"dsh": {
|
|
58
|
+
"engines": {
|
|
59
|
+
"dsh": ">=0.1.5-alpha.1 <0.2.0"
|
|
60
|
+
},
|
|
61
|
+
"bundle": {
|
|
62
|
+
"patch": "./cordis.patch.yml"
|
|
63
|
+
},
|
|
64
|
+
"client": {
|
|
65
|
+
"platform": "web",
|
|
66
|
+
"inject": [
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-settings-models",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
69
|
+
"@deepseek-ai/dsh-client-locale",
|
|
70
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
76
|
+
"@deepseek-ai/dsh-api-gateway": ">=0.1.5-alpha.1 <0.2.0",
|
|
77
|
+
"@deepseek-ai/dsh-api-remotes": ">=0.1.5-alpha.1 <0.2.0",
|
|
78
|
+
"@deepseek-ai/dsh-api-settings-controller": ">=0.1.5-alpha.1 <0.2.0",
|
|
79
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.5-alpha.1 <0.2.0",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.5-alpha.1 <0.2.0",
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.5-alpha.1 <0.2.0",
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-settings-models": ">=0.1.5-alpha.1 <0.2.0",
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.5-alpha.1 <0.2.0",
|
|
84
|
+
"@deepseek-ai/dsh-settings": ">=0.1.5-alpha.1 <0.2.0",
|
|
85
|
+
"@deepseek-ai/dsh-util-values": ">=0.1.5-alpha.1 <0.2.0",
|
|
86
|
+
"react": "^18.2.0"
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
90
|
+
"@deepseek-ai/dsh-api-gateway": "0.1.5-alpha.1",
|
|
91
|
+
"@deepseek-ai/dsh-api-remotes": "0.1.5-alpha.1",
|
|
92
|
+
"@deepseek-ai/dsh-api-settings-controller": "0.1.5-alpha.1",
|
|
93
|
+
"@deepseek-ai/dsh-client-locale": "0.1.5-alpha.1",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-alpha.1",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.5-alpha.1",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-settings-models": "0.1.5-alpha.1",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.5-alpha.1",
|
|
98
|
+
"@deepseek-ai/dsh-settings": "0.1.5-alpha.1",
|
|
99
|
+
"@deepseek-ai/dsh-util-values": "0.1.5-alpha.1",
|
|
100
|
+
"@types/node": "^22.20.0",
|
|
101
|
+
"@types/react": "~18.3.1",
|
|
102
|
+
"lightningcss": "^1.32.0",
|
|
103
|
+
"publint": "^0.3.21",
|
|
104
|
+
"react": "^18.3.1",
|
|
105
|
+
"tsdown": "^0.22.2",
|
|
106
|
+
"typescript": "^5.9.3",
|
|
107
|
+
"vitest": "^4.1.8"
|
|
108
|
+
},
|
|
109
|
+
"scripts": {
|
|
110
|
+
"build": "tsc -p tsconfig.build.host.json && tsc -p tsconfig.build.client.json && tsdown",
|
|
111
|
+
"check": "pnpm run typecheck && pnpm test && pnpm run build && pnpm run publint",
|
|
112
|
+
"publint": "publint --strict",
|
|
113
|
+
"test": "vitest run",
|
|
114
|
+
"typecheck": "tsc -p tsconfig.host.json --noEmit && tsc -p tsconfig.client.json --noEmit"
|
|
115
|
+
}
|
|
116
|
+
}
|