dsh-plugin-effort-declare 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/CONTRIBUTING.en.md +57 -0
- package/CONTRIBUTING.md +57 -0
- package/INSTALL.en.md +62 -0
- package/INSTALL.md +62 -0
- package/LICENSE +21 -0
- package/README.en.md +64 -0
- package/README.md +64 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +1237 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +6 -0
- package/lib/types/client/EffortDeclareSection.d.ts +24 -0
- package/lib/types/client/EffortDeclareSection.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +15 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/load-drafts.d.ts +20 -0
- package/lib/types/client/load-drafts.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/schema-ops.d.ts +13 -0
- package/lib/types/client/schema-ops.d.ts.map +1 -0
- package/lib/types/core/catalog.d.ts +31 -0
- package/lib/types/core/catalog.d.ts.map +1 -0
- package/lib/types/core/drafts.d.ts +53 -0
- package/lib/types/core/drafts.d.ts.map +1 -0
- package/lib/types/core/efforts.d.ts +32 -0
- package/lib/types/core/efforts.d.ts.map +1 -0
- package/lib/types/core/filter.d.ts +28 -0
- package/lib/types/core/filter.d.ts.map +1 -0
- package/lib/types/core/path-ops.d.ts +40 -0
- package/lib/types/core/path-ops.d.ts.map +1 -0
- package/lib/types/core/paths.d.ts +16 -0
- package/lib/types/core/paths.d.ts.map +1 -0
- package/lib/types/core/presets.d.ts +31 -0
- package/lib/types/core/presets.d.ts.map +1 -0
- package/lib/types/core/validate.d.ts +3 -0
- package/lib/types/core/validate.d.ts.map +1 -0
- package/lib/types/index.d.ts +10 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +87 -0
- package/src/README.en.md +23 -0
- package/src/README.md +23 -0
- package/src/client/EffortDeclareSection.tsx +496 -0
- package/src/client/README.en.md +27 -0
- package/src/client/README.md +27 -0
- package/src/client/effort-declare.module.css +278 -0
- package/src/client/index.ts +97 -0
- package/src/client/load-drafts.ts +83 -0
- package/src/client/locales.ts +133 -0
- package/src/client/schema-ops.ts +20 -0
- package/src/core/README.en.md +27 -0
- package/src/core/README.md +27 -0
- package/src/core/catalog.ts +61 -0
- package/src/core/drafts.ts +137 -0
- package/src/core/efforts.ts +119 -0
- package/src/core/filter.ts +82 -0
- package/src/core/path-ops.ts +65 -0
- package/src/core/paths.ts +45 -0
- package/src/core/presets.ts +104 -0
- package/src/core/validate.ts +7 -0
- package/src/css-modules.d.ts +6 -0
- package/src/index.ts +11 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-effort-declare — host half.
|
|
3
|
+
* v1 contributes no host services, adapters, or events. The cordis row still
|
|
4
|
+
* has to exist so a later `dsh plugin add` can load the bundle.
|
|
5
|
+
*/
|
|
6
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
7
|
+
export declare const name = "dsh-plugin-effort-declare";
|
|
8
|
+
/** Host apply is intentionally empty in v1. */
|
|
9
|
+
export declare function apply(_ctx: Context): void;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAElD,eAAO,MAAM,IAAI,8BAA8B,CAAA;AAE/C,+CAA+C;AAC/C,wBAAgB,KAAK,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAAG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-effort-declare",
|
|
3
|
+
"description": "Settings page to declare per-model reasoningEfforts and openai-completions compat on hand-declared llm-pi-ai routes. Writes the official namespace; does not intercept llm/stream or replace the Models page.",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"private": false,
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/zimodzh/dsh-plugin-effort-declare.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/zimodzh/dsh-plugin-effort-declare/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/zimodzh/dsh-plugin-effort-declare#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "lib/index.js",
|
|
17
|
+
"types": "lib/types/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./lib/types/index.d.ts",
|
|
21
|
+
"default": "./lib/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./client": {
|
|
24
|
+
"types": "./lib/types/client/index.d.ts",
|
|
25
|
+
"default": "./lib/client.js"
|
|
26
|
+
},
|
|
27
|
+
"./src/*": "./src/*",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"dsh": {
|
|
31
|
+
"bundle": {
|
|
32
|
+
"patch": "./cordis.patch.yml"
|
|
33
|
+
},
|
|
34
|
+
"client": {
|
|
35
|
+
"inject": [
|
|
36
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
37
|
+
"@deepseek-ai/dsh-client-locale",
|
|
38
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
39
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
40
|
+
],
|
|
41
|
+
"platform": "web"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8",
|
|
46
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
|
|
47
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
|
|
49
|
+
"react": "^18.2.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8",
|
|
54
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
|
|
55
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
|
|
58
|
+
"@types/node": "^22.20.0",
|
|
59
|
+
"@types/react": "~18.3.1",
|
|
60
|
+
"lightningcss": "^1.32.0",
|
|
61
|
+
"react": "^18.3.1",
|
|
62
|
+
"tsdown": "0.22.2",
|
|
63
|
+
"typescript": "~5.7.2",
|
|
64
|
+
"vitest": "^4.1.8"
|
|
65
|
+
},
|
|
66
|
+
"files": [
|
|
67
|
+
"lib/*.js",
|
|
68
|
+
"lib/*.js.map",
|
|
69
|
+
"lib/types/**/*.d.ts",
|
|
70
|
+
"lib/types/**/*.d.ts.map",
|
|
71
|
+
"src",
|
|
72
|
+
"cordis.patch.yml",
|
|
73
|
+
"README.md",
|
|
74
|
+
"README.en.md",
|
|
75
|
+
"INSTALL.md",
|
|
76
|
+
"INSTALL.en.md",
|
|
77
|
+
"CONTRIBUTING.md",
|
|
78
|
+
"CONTRIBUTING.en.md",
|
|
79
|
+
"LICENSE"
|
|
80
|
+
],
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "tsc -b && tsdown",
|
|
83
|
+
"watch": "tsdown --watch",
|
|
84
|
+
"test": "vitest run",
|
|
85
|
+
"typecheck": "tsc -b --pretty false"
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/README.en.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# src/
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<samp>
|
|
5
|
+
<a href="./README.md">中文</a> ·
|
|
6
|
+
<strong>English</strong>
|
|
7
|
+
</samp>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
TypeScript sources for the plugin. The host (Node) and the browser must be separate TypeScript programs: their `Context` merges cannot share one `ts.Program`.
|
|
11
|
+
|
|
12
|
+
Install: [INSTALL.en.md](../INSTALL.en.md). Usage: root [README.en.md](../README.en.md). Contributing: [CONTRIBUTING.en.md](../CONTRIBUTING.en.md).
|
|
13
|
+
|
|
14
|
+
## What's in this directory
|
|
15
|
+
|
|
16
|
+
| Path | Role |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| [`index.ts`](./index.ts) | Host entry. Exports `name` and `apply`. v1 registers no host services or adapters; the bundle still needs this entry so `dsh plugin add` can load the package. |
|
|
19
|
+
| [`core/`](./core/) | UI-free pure functions: efforts, presets, save ops, draft merge, route filtering. Shared by the client and tests; the v1 host entry does not import them (`tsconfig.host` still compiles `core` for types). |
|
|
20
|
+
| [`client/`](./client/) | Browser half: locale, settings page, `settings.mutate` against `llm-pi-ai`. |
|
|
21
|
+
| [`css-modules.d.ts`](./css-modules.d.ts) | Types for `*.module.css` (class map plus `cssText` / `cssTagId`). |
|
|
22
|
+
|
|
23
|
+
`tsconfig.host.json` compiles host + `core`; `tsconfig.client.json` compiles `client` + `core`. Build output lives in [`lib/`](../lib/).
|
package/src/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# src/
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<samp>
|
|
5
|
+
<strong>中文</strong> ·
|
|
6
|
+
<a href="./README.en.md">English</a>
|
|
7
|
+
</samp>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
插件 TypeScript 源码。Host(Node)与浏览器必须分成两个 TypeScript Program:两边对 `Context` 的 merge 不能共处同一份 `ts.Program`。
|
|
11
|
+
|
|
12
|
+
安装见 [INSTALL.md](../INSTALL.md),使用见仓库根 [README.md](../README.md),贡献见 [CONTRIBUTING.md](../CONTRIBUTING.md)。
|
|
13
|
+
|
|
14
|
+
## 这个目录里有什么
|
|
15
|
+
|
|
16
|
+
| 路径 | 说明 |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| [`index.ts`](./index.ts) | Host 入口。导出 `name` 与 `apply`。v1 不注册 host 服务、不挂适配器;组合包仍需要这一入口才能被 `dsh plugin add` 加载。 |
|
|
19
|
+
| [`core/`](./core/) | 与 UI 无关的纯函数:档位、预设、保存 ops、草稿合并、路由过滤。client 与单测共用;Host 入口 v1 不引用这些函数(`tsconfig.host` 仍编译 `core` 以产出类型)。 |
|
|
20
|
+
| [`client/`](./client/) | 浏览器半区:locale、设置页、对 `llm-pi-ai` 的 `settings.mutate`。 |
|
|
21
|
+
| [`css-modules.d.ts`](./css-modules.d.ts) | `*.module.css` 模块类型(class map + `cssText` / `cssTagId`)。 |
|
|
22
|
+
|
|
23
|
+
`tsconfig.host.json` 编译 host + `core`;`tsconfig.client.json` 编译 `client` + `core`。构建产物在 [`lib/`](../lib/)。
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings section: per-model reasoningEfforts + openai-completions compat
|
|
3
|
+
* for hand-declared llm-pi-ai routes.
|
|
4
|
+
*/
|
|
5
|
+
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
6
|
+
import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client'
|
|
7
|
+
import type { SettingsDescribeFace } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
8
|
+
import { LLM_PI_AI_NS, THINKING_LEVELS_WITHOUT_OFF } from '../core/catalog.ts'
|
|
9
|
+
import {
|
|
10
|
+
alignDraft,
|
|
11
|
+
applySaveSuccess,
|
|
12
|
+
draftDirty,
|
|
13
|
+
generationIsCurrent,
|
|
14
|
+
mergeLoadedDrafts,
|
|
15
|
+
nextGeneration,
|
|
16
|
+
thinkingFormatChoices,
|
|
17
|
+
type RouteDraft,
|
|
18
|
+
} from '../core/drafts.ts'
|
|
19
|
+
import {
|
|
20
|
+
hasLevel,
|
|
21
|
+
readEfforts,
|
|
22
|
+
readOff,
|
|
23
|
+
setWireSpelling,
|
|
24
|
+
toggleLevel,
|
|
25
|
+
writeEfforts,
|
|
26
|
+
writeOff,
|
|
27
|
+
clearReasoningEfforts,
|
|
28
|
+
} from '../core/efforts.ts'
|
|
29
|
+
import { applyPresetCompat, applyPresetEfforts, PRESETS, type PresetId } from '../core/presets.ts'
|
|
30
|
+
import { buildSaveOps } from '../core/path-ops.ts'
|
|
31
|
+
import { cloneModels, cloneObject } from '../core/paths.ts'
|
|
32
|
+
import { modelEffortError } from '../core/validate.ts'
|
|
33
|
+
import { loadDrafts } from './load-drafts.ts'
|
|
34
|
+
import type { SchemaOps } from './schema-ops.ts'
|
|
35
|
+
import type { EffortDeclareKey } from './locales.ts'
|
|
36
|
+
import css from './effort-declare.module.css'
|
|
37
|
+
|
|
38
|
+
export type InvalidationSource = 'settings' | 'directory'
|
|
39
|
+
|
|
40
|
+
export interface EffortDeclareSectionInjected {
|
|
41
|
+
api: Pick<IApiClient, 'settings' | 'llm'>
|
|
42
|
+
describe: SettingsDescribeFace
|
|
43
|
+
schema: SchemaOps
|
|
44
|
+
t: (key: EffortDeclareKey) => string
|
|
45
|
+
subscribeInvalidate: (listener: (source: InvalidationSource) => void) => () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EffortDeclareSectionProps extends Partial<EffortDeclareSectionInjected> {
|
|
49
|
+
t: (key: EffortDeclareKey) => string
|
|
50
|
+
close: () => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { loadDrafts } from './load-drafts.ts'
|
|
54
|
+
|
|
55
|
+
type CardNotice = { kind: 'saved' | 'conflict' | 'error'; text: string }
|
|
56
|
+
|
|
57
|
+
function compatSummary(compat: Record<string, unknown>): string {
|
|
58
|
+
const parts: string[] = []
|
|
59
|
+
if (typeof compat.thinkingFormat === 'string') parts.push(`thinkingFormat=${compat.thinkingFormat}`)
|
|
60
|
+
if (compat.supportsDeveloperRole === false) parts.push('supportsDeveloperRole=false')
|
|
61
|
+
if (compat.supportsReasoningEffort === false) parts.push('supportsReasoningEffort=false')
|
|
62
|
+
return parts.join(' · ')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function errorText(code: ReturnType<typeof modelEffortError>, t: (key: EffortDeclareKey) => string): string | undefined {
|
|
66
|
+
if (code === 'empty') return t('errorEmpty')
|
|
67
|
+
if (code === 'off-only') return t('errorOffOnly')
|
|
68
|
+
if (code === 'bad-wire') return t('errorBadWire')
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ModelRowEditor(props: {
|
|
73
|
+
row: Record<string, unknown>
|
|
74
|
+
index: number
|
|
75
|
+
radioName: string
|
|
76
|
+
disabled: boolean
|
|
77
|
+
t: (key: EffortDeclareKey) => string
|
|
78
|
+
onChange: (row: Record<string, unknown>) => void
|
|
79
|
+
}): ReactNode {
|
|
80
|
+
const { row, disabled, t, onChange } = props
|
|
81
|
+
const efforts = readEfforts(row) ?? {}
|
|
82
|
+
const off = readOff(readEfforts(row))
|
|
83
|
+
const id = typeof row.id === 'string' ? row.id : ''
|
|
84
|
+
const name = typeof row.name === 'string' ? row.name : ''
|
|
85
|
+
|
|
86
|
+
const patchEfforts = (next: typeof efforts): void => {
|
|
87
|
+
onChange(writeEfforts(row, Object.keys(next).length === 0 ? undefined : next))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className={css.modelEntry}>
|
|
92
|
+
<div className={css.modelHead}>
|
|
93
|
+
<span className={css.modelId}>{id || t('model')}</span>
|
|
94
|
+
{name !== '' && name !== id ? <span className={css.modelName}>{name}</span> : null}
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
className={css.linkButton}
|
|
98
|
+
disabled={disabled || !('reasoningEfforts' in row)}
|
|
99
|
+
onClick={() => { onChange(clearReasoningEfforts(row)) }}
|
|
100
|
+
>
|
|
101
|
+
{t('clear')}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
<span className={css.fieldLabel}>{t('levels')}</span>
|
|
105
|
+
<div className={css.levels}>
|
|
106
|
+
{THINKING_LEVELS_WITHOUT_OFF.map((level) => (
|
|
107
|
+
<label key={level} className={css.level}>
|
|
108
|
+
<input
|
|
109
|
+
type="checkbox"
|
|
110
|
+
checked={hasLevel(efforts, level)}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
onChange={(event) => { patchEfforts(toggleLevel(efforts, level, event.target.checked)) }}
|
|
113
|
+
/>
|
|
114
|
+
{level}
|
|
115
|
+
</label>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
<div className={css.wireRow}>
|
|
119
|
+
<span className={css.fieldLabel}>{t('wire')}</span>
|
|
120
|
+
{THINKING_LEVELS_WITHOUT_OFF.filter(level => hasLevel(efforts, level)).map((level) => (
|
|
121
|
+
<label key={level} className={css.level}>
|
|
122
|
+
{level}
|
|
123
|
+
<input
|
|
124
|
+
className={`${css.input} ${css.wireInput}`}
|
|
125
|
+
type="text"
|
|
126
|
+
value={efforts[level] ?? level}
|
|
127
|
+
disabled={disabled}
|
|
128
|
+
aria-label={`${t('wire')} ${level}`}
|
|
129
|
+
onChange={(event) => { patchEfforts(setWireSpelling(efforts, level, event.target.value)) }}
|
|
130
|
+
/>
|
|
131
|
+
</label>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
<div className={css.offGroup}>
|
|
135
|
+
<span className={css.fieldLabel}>{t('offMode')}</span>
|
|
136
|
+
{(['absent', 'empty', 'value'] as const).map((mode) => (
|
|
137
|
+
<label key={mode} className={css.level}>
|
|
138
|
+
<input
|
|
139
|
+
type="radio"
|
|
140
|
+
name={props.radioName}
|
|
141
|
+
checked={off.mode === mode}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
onChange={() => { patchEfforts(writeOff(efforts, mode, off.value)) }}
|
|
144
|
+
/>
|
|
145
|
+
{mode === 'absent' ? t('offAbsent') : mode === 'empty' ? t('offEmpty') : t('offValue')}
|
|
146
|
+
</label>
|
|
147
|
+
))}
|
|
148
|
+
{off.mode === 'value'
|
|
149
|
+
? (
|
|
150
|
+
<input
|
|
151
|
+
className={`${css.input} ${css.wireInput}`}
|
|
152
|
+
type="text"
|
|
153
|
+
value={off.value}
|
|
154
|
+
placeholder={t('offValuePlaceholder')}
|
|
155
|
+
disabled={disabled}
|
|
156
|
+
onChange={(event) => { patchEfforts(writeOff(efforts, 'value', event.target.value)) }}
|
|
157
|
+
/>
|
|
158
|
+
)
|
|
159
|
+
: null}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function RouteCard(props: {
|
|
166
|
+
draft: RouteDraft
|
|
167
|
+
formats: readonly string[]
|
|
168
|
+
writable: boolean
|
|
169
|
+
busy: boolean
|
|
170
|
+
reloading: boolean
|
|
171
|
+
notice: CardNotice | undefined
|
|
172
|
+
t: (key: EffortDeclareKey) => string
|
|
173
|
+
onChange: (draft: RouteDraft) => void
|
|
174
|
+
onSave: (draft: RouteDraft) => void
|
|
175
|
+
onCancel: (draft: RouteDraft) => void
|
|
176
|
+
}): ReactNode {
|
|
177
|
+
const { draft, writable, busy, reloading, t, onChange } = props
|
|
178
|
+
const noModels = draft.models.length === 0
|
|
179
|
+
const editDisabled = !writable || busy || noModels
|
|
180
|
+
const saveLocked = editDisabled || reloading
|
|
181
|
+
const formats = thinkingFormatChoices(props.formats, draft.compat.thinkingFormat)
|
|
182
|
+
const summary = compatSummary(draft.compat)
|
|
183
|
+
const sameWire = draft.compat.supportsReasoningEffort === false
|
|
184
|
+
const clientError = draft.models
|
|
185
|
+
.map(row => errorText(modelEffortError(row), t))
|
|
186
|
+
.find(text => text !== undefined)
|
|
187
|
+
const dirty = draftDirty(draft)
|
|
188
|
+
|
|
189
|
+
const applyPreset = (id: PresetId): void => {
|
|
190
|
+
const preset = PRESETS[id]
|
|
191
|
+
onChange({
|
|
192
|
+
...draft,
|
|
193
|
+
models: applyPresetEfforts(draft.models, preset),
|
|
194
|
+
compat: applyPresetCompat(draft.compat, preset),
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<li className={css.rowCard}>
|
|
200
|
+
<div className={css.rowHead}>
|
|
201
|
+
<span className={css.rowName}>{draft.displayName}</span>
|
|
202
|
+
<span className={css.rowTag}>{draft.provider}</span>
|
|
203
|
+
</div>
|
|
204
|
+
<p className={css.compatSummary}>
|
|
205
|
+
{t('compatSummary')}
|
|
206
|
+
{summary === '' ? '' : `: ${summary}`}
|
|
207
|
+
</p>
|
|
208
|
+
<div className={css.presetRow}>
|
|
209
|
+
<span className={css.fieldLabel}>{t('presets')}</span>
|
|
210
|
+
<button type="button" className={css.secondaryButton} disabled={editDisabled} onClick={() => { applyPreset('deepseek') }}>
|
|
211
|
+
{t('presetDeepSeek')}
|
|
212
|
+
</button>
|
|
213
|
+
<button type="button" className={css.secondaryButton} disabled={editDisabled} onClick={() => { applyPreset('openai') }}>
|
|
214
|
+
{t('presetOpenAI')}
|
|
215
|
+
</button>
|
|
216
|
+
<button type="button" className={css.secondaryButton} disabled={editDisabled} onClick={() => { applyPreset('toggle') }}>
|
|
217
|
+
{t('presetToggle')}
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
{sameWire ? <p className={css.notice}>{t('presetToggleWarn')}</p> : null}
|
|
221
|
+
{noModels ? <p className={css.notice}>{t('noModels')}</p> : null}
|
|
222
|
+
{draft.models.map((row, index) => (
|
|
223
|
+
<ModelRowEditor
|
|
224
|
+
key={`${String(row.id)}-${String(index)}`}
|
|
225
|
+
row={row}
|
|
226
|
+
index={index}
|
|
227
|
+
radioName={`off-${draft.provider}-${String(index)}`}
|
|
228
|
+
disabled={editDisabled}
|
|
229
|
+
t={t}
|
|
230
|
+
onChange={(next) => {
|
|
231
|
+
const models = draft.models.map((candidate, at) => at === index ? next : candidate)
|
|
232
|
+
onChange({ ...draft, models })
|
|
233
|
+
}}
|
|
234
|
+
/>
|
|
235
|
+
))}
|
|
236
|
+
<details className={css.advanced}>
|
|
237
|
+
<summary>{t('advanced')}</summary>
|
|
238
|
+
<div className={css.advancedBody}>
|
|
239
|
+
<label className={css.fieldLabel}>
|
|
240
|
+
{t('thinkingFormat')}
|
|
241
|
+
<select
|
|
242
|
+
className={`${css.input} ${css.selectInput}`}
|
|
243
|
+
value={typeof draft.compat.thinkingFormat === 'string' ? draft.compat.thinkingFormat : ''}
|
|
244
|
+
disabled={editDisabled}
|
|
245
|
+
onChange={(event) => {
|
|
246
|
+
const compat = { ...draft.compat }
|
|
247
|
+
if (event.target.value === '') delete compat.thinkingFormat
|
|
248
|
+
else compat.thinkingFormat = event.target.value
|
|
249
|
+
onChange({ ...draft, compat })
|
|
250
|
+
}}
|
|
251
|
+
>
|
|
252
|
+
<option value="">{t('thinkingFormatDefault')}</option>
|
|
253
|
+
{formats.map(format => <option key={format} value={format}>{format}</option>)}
|
|
254
|
+
</select>
|
|
255
|
+
</label>
|
|
256
|
+
{draft.compat.supportsDeveloperRole === true
|
|
257
|
+
? <p className={css.notice}>{t('developerTrueHint')}</p>
|
|
258
|
+
: null}
|
|
259
|
+
<label className={css.check}>
|
|
260
|
+
<input
|
|
261
|
+
type="checkbox"
|
|
262
|
+
checked={draft.compat.supportsDeveloperRole === false}
|
|
263
|
+
disabled={editDisabled}
|
|
264
|
+
onChange={(event) => {
|
|
265
|
+
const compat = { ...draft.compat }
|
|
266
|
+
if (event.target.checked) compat.supportsDeveloperRole = false
|
|
267
|
+
else delete compat.supportsDeveloperRole
|
|
268
|
+
onChange({ ...draft, compat })
|
|
269
|
+
}}
|
|
270
|
+
/>
|
|
271
|
+
{t('supportsDeveloperRole')}
|
|
272
|
+
</label>
|
|
273
|
+
<label className={css.check}>
|
|
274
|
+
<input
|
|
275
|
+
type="checkbox"
|
|
276
|
+
checked={draft.compat.supportsReasoningEffort === false}
|
|
277
|
+
disabled={editDisabled}
|
|
278
|
+
onChange={(event) => {
|
|
279
|
+
const compat = { ...draft.compat }
|
|
280
|
+
if (event.target.checked) compat.supportsReasoningEffort = false
|
|
281
|
+
else delete compat.supportsReasoningEffort
|
|
282
|
+
onChange({ ...draft, compat })
|
|
283
|
+
}}
|
|
284
|
+
/>
|
|
285
|
+
{t('supportsReasoningEffort')}
|
|
286
|
+
</label>
|
|
287
|
+
</div>
|
|
288
|
+
</details>
|
|
289
|
+
{clientError !== undefined ? <p className={css.error}>{clientError}</p> : null}
|
|
290
|
+
{props.notice?.kind === 'saved' ? <p className={css.savedNotice}>{props.notice.text}</p> : null}
|
|
291
|
+
{props.notice?.kind === 'conflict' || props.notice?.kind === 'error'
|
|
292
|
+
? <p className={css.error}>{props.notice.text}</p>
|
|
293
|
+
: null}
|
|
294
|
+
<div className={css.actions}>
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
className={css.secondaryButton}
|
|
298
|
+
disabled={busy || !dirty}
|
|
299
|
+
onClick={() => { props.onCancel(draft) }}
|
|
300
|
+
>
|
|
301
|
+
{t('cancel')}
|
|
302
|
+
</button>
|
|
303
|
+
<button
|
|
304
|
+
type="button"
|
|
305
|
+
className={css.primaryButton}
|
|
306
|
+
disabled={saveLocked || !dirty || clientError !== undefined}
|
|
307
|
+
onClick={() => { props.onSave(draft) }}
|
|
308
|
+
>
|
|
309
|
+
{busy ? t('saving') : t('save')}
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
</li>
|
|
313
|
+
)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function EffortDeclareSection(props: EffortDeclareSectionProps): ReactNode {
|
|
317
|
+
const t = props.t
|
|
318
|
+
const api = props.api
|
|
319
|
+
const describe = props.describe
|
|
320
|
+
const schema = props.schema
|
|
321
|
+
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
|
322
|
+
const [error, setError] = useState('')
|
|
323
|
+
const [writable, setWritable] = useState(false)
|
|
324
|
+
const [formats, setFormats] = useState<string[]>([])
|
|
325
|
+
const [drafts, setDrafts] = useState<RouteDraft[]>([])
|
|
326
|
+
const [busyRoute, setBusyRoute] = useState<string | null>(null)
|
|
327
|
+
const [notices, setNotices] = useState<Record<string, CardNotice>>({})
|
|
328
|
+
const generationRef = useRef(0)
|
|
329
|
+
const draftsRef = useRef(drafts)
|
|
330
|
+
draftsRef.current = drafts
|
|
331
|
+
|
|
332
|
+
const reload = useCallback((preserveDirty: boolean): void => {
|
|
333
|
+
if (api === undefined || describe === undefined || schema === undefined) {
|
|
334
|
+
setStatus('error')
|
|
335
|
+
setError(t('loadError'))
|
|
336
|
+
return
|
|
337
|
+
}
|
|
338
|
+
const generation = nextGeneration(generationRef)
|
|
339
|
+
setStatus('loading')
|
|
340
|
+
setError('')
|
|
341
|
+
void loadDrafts(api, describe, schema).then((result) => {
|
|
342
|
+
if (!generationIsCurrent(generationRef, generation)) return
|
|
343
|
+
setWritable(result.writable)
|
|
344
|
+
if (result.formats.length > 0) setFormats(result.formats)
|
|
345
|
+
if (result.error !== undefined) {
|
|
346
|
+
setStatus('error')
|
|
347
|
+
setError(result.error)
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
const merged = mergeLoadedDrafts(draftsRef.current, result.drafts, { preserveDirty })
|
|
351
|
+
setDrafts(merged.drafts)
|
|
352
|
+
if (merged.conflicted.length > 0) {
|
|
353
|
+
setNotices(Object.fromEntries(
|
|
354
|
+
merged.conflicted.map(provider => [provider, { kind: 'conflict' as const, text: t('dirtyConflict') }]),
|
|
355
|
+
))
|
|
356
|
+
}
|
|
357
|
+
setStatus('ready')
|
|
358
|
+
}, (failure: unknown) => {
|
|
359
|
+
if (!generationIsCurrent(generationRef, generation)) return
|
|
360
|
+
setStatus('error')
|
|
361
|
+
setError(failure instanceof Error ? failure.message : t('loadError'))
|
|
362
|
+
})
|
|
363
|
+
}, [api, describe, schema, t])
|
|
364
|
+
|
|
365
|
+
useEffect(() => { reload(false) }, [reload])
|
|
366
|
+
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (props.subscribeInvalidate === undefined) return undefined
|
|
369
|
+
return props.subscribeInvalidate(() => { reload(true) })
|
|
370
|
+
}, [props.subscribeInvalidate, reload])
|
|
371
|
+
|
|
372
|
+
const save = async (draft: RouteDraft): Promise<void> => {
|
|
373
|
+
if (api === undefined || describe === undefined) return
|
|
374
|
+
if (status === 'loading' || busyRoute !== null) return
|
|
375
|
+
const blocking = draft.models
|
|
376
|
+
.map(row => errorText(modelEffortError(row), t))
|
|
377
|
+
.find(text => text !== undefined)
|
|
378
|
+
if (blocking !== undefined) {
|
|
379
|
+
setNotices({ [draft.provider]: { kind: 'error', text: blocking } })
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
setBusyRoute(draft.provider)
|
|
383
|
+
setNotices({})
|
|
384
|
+
try {
|
|
385
|
+
const ops = buildSaveOps({
|
|
386
|
+
settingsPath: draft.settingsPath,
|
|
387
|
+
beforeModels: draft.originalModels,
|
|
388
|
+
afterModels: draft.models,
|
|
389
|
+
beforeCompat: draft.compatPresent ? draft.originalCompat : undefined,
|
|
390
|
+
afterCompat: draft.compat,
|
|
391
|
+
})
|
|
392
|
+
if (ops.length === 0) {
|
|
393
|
+
setDrafts(current => current.map(row => row.provider === draft.provider ? alignDraft(row) : row))
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
const response = await api.settings.mutate({
|
|
397
|
+
ns: LLM_PI_AI_NS,
|
|
398
|
+
ops,
|
|
399
|
+
expectedRevision: draft.revision,
|
|
400
|
+
})
|
|
401
|
+
if (!response.result.ok) {
|
|
402
|
+
setNotices({
|
|
403
|
+
[draft.provider]: {
|
|
404
|
+
kind: 'error',
|
|
405
|
+
text: response.result.error.code === 'settings-conflict' ? t('conflict') : response.result.error.message,
|
|
406
|
+
},
|
|
407
|
+
})
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
const view = response.result.value
|
|
411
|
+
describe.acceptView(view)
|
|
412
|
+
setDrafts(current => applySaveSuccess(current, draft.provider, {
|
|
413
|
+
user: view.user ?? {},
|
|
414
|
+
revision: view.revision,
|
|
415
|
+
}))
|
|
416
|
+
setNotices({ [draft.provider]: { kind: 'saved', text: t('saved') } })
|
|
417
|
+
} catch (failure) {
|
|
418
|
+
setNotices({
|
|
419
|
+
[draft.provider]: {
|
|
420
|
+
kind: 'error',
|
|
421
|
+
text: failure instanceof Error ? failure.message : t('loadError'),
|
|
422
|
+
},
|
|
423
|
+
})
|
|
424
|
+
} finally {
|
|
425
|
+
setBusyRoute(null)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const showLoading = status === 'loading' && drafts.length === 0
|
|
430
|
+
const showEmpty = status === 'ready' && drafts.length === 0
|
|
431
|
+
const showList = drafts.length > 0
|
|
432
|
+
|
|
433
|
+
return (
|
|
434
|
+
<div className={css.section}>
|
|
435
|
+
<h2 className={css.title}>{t('title')}</h2>
|
|
436
|
+
<p className={css.intro}>{t('intro')}</p>
|
|
437
|
+
{!writable && status === 'ready' ? <p className={css.notice}>{t('readOnly')}</p> : null}
|
|
438
|
+
{showLoading ? <p className={css.intro}>{t('loading')}</p> : null}
|
|
439
|
+
{status === 'error' ? <p className={css.error}>{error}</p> : null}
|
|
440
|
+
{status === 'error'
|
|
441
|
+
? (
|
|
442
|
+
<button type="button" className={css.secondaryButton} onClick={() => { reload(true) }}>{t('reload')}</button>
|
|
443
|
+
)
|
|
444
|
+
: null}
|
|
445
|
+
{showEmpty
|
|
446
|
+
? (
|
|
447
|
+
<>
|
|
448
|
+
<p className={css.notice}>{t('empty')}</p>
|
|
449
|
+
<p className={css.intro}>{t('emptyHint')}</p>
|
|
450
|
+
</>
|
|
451
|
+
)
|
|
452
|
+
: null}
|
|
453
|
+
{showList
|
|
454
|
+
? (
|
|
455
|
+
<ul className={css.rows}>
|
|
456
|
+
{drafts.map(draft => (
|
|
457
|
+
<RouteCard
|
|
458
|
+
key={draft.provider}
|
|
459
|
+
draft={draft}
|
|
460
|
+
formats={formats.length > 0 ? formats : []}
|
|
461
|
+
writable={writable}
|
|
462
|
+
busy={busyRoute === draft.provider}
|
|
463
|
+
reloading={status === 'loading'}
|
|
464
|
+
notice={notices[draft.provider]}
|
|
465
|
+
t={t}
|
|
466
|
+
onChange={(next) => {
|
|
467
|
+
setNotices(current => {
|
|
468
|
+
const copy = { ...current }
|
|
469
|
+
delete copy[next.provider]
|
|
470
|
+
return copy
|
|
471
|
+
})
|
|
472
|
+
setDrafts(current => current.map(row => row.provider === next.provider ? next : row))
|
|
473
|
+
}}
|
|
474
|
+
onSave={(next) => { void save(next) }}
|
|
475
|
+
onCancel={(next) => {
|
|
476
|
+
setNotices(current => {
|
|
477
|
+
const copy = { ...current }
|
|
478
|
+
delete copy[next.provider]
|
|
479
|
+
return copy
|
|
480
|
+
})
|
|
481
|
+
setDrafts(current => current.map(row => row.provider === next.provider
|
|
482
|
+
? {
|
|
483
|
+
...row,
|
|
484
|
+
models: cloneModels(row.originalModels),
|
|
485
|
+
compat: cloneObject(row.originalCompat),
|
|
486
|
+
}
|
|
487
|
+
: row))
|
|
488
|
+
}}
|
|
489
|
+
/>
|
|
490
|
+
))}
|
|
491
|
+
</ul>
|
|
492
|
+
)
|
|
493
|
+
: null}
|
|
494
|
+
</div>
|
|
495
|
+
)
|
|
496
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# src/client/
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<samp>
|
|
5
|
+
<a href="./README.md">中文</a> ·
|
|
6
|
+
<strong>English</strong>
|
|
7
|
+
</samp>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
Browser half: registers **Reasoning efforts** in the Web UI settings panel and writes `llm-pi-ai` through the official settings RPC.
|
|
11
|
+
|
|
12
|
+
Wiring failures in `apply` throw, same as the official Models page. Do not swallow `apply` with a blanket `try/catch`. Locale, CSS, and event subscriptions are cleaned up through `ctx.effect` on unload.
|
|
13
|
+
|
|
14
|
+
Cross-plugin work uses Cordis services only (`connection`, `settingsScope`, `settingsSchema`, `slots`, `locale`, `remote`). Do not value-import `@deepseek-ai/dsh-client-ui-settings-models`: the client bundle purity rule forbids inlining other `@deepseek-ai/*` packages except platform modules in the shell’s frozen table.
|
|
15
|
+
|
|
16
|
+
## What's in this directory
|
|
17
|
+
|
|
18
|
+
| File | Role |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| [`index.ts`](./index.ts) | Registers zh/en copy, CSS (`ctx.effect` insert/remove), and `settings.section` (id `effort-declare`, order 15). Subscribes in `apply` to `settings/document-updated` (`llm-pi-ai` only), `llm/adapters-updated`, and `connection/reset`. |
|
|
21
|
+
| [`EffortDeclareSection.tsx`](./EffortDeclareSection.tsx) | Route cards, presets, Off tri-state, advanced protocol switches, save and cancel. |
|
|
22
|
+
| [`load-drafts.ts`](./load-drafts.ts) | Builds drafts from `llm.providers` + the settings mirror; drafts come from `user`, protocol classification may use `value`. |
|
|
23
|
+
| [`locales.ts`](./locales.ts) | Copy namespace `plugin-effort-declare`. |
|
|
24
|
+
| [`effort-declare.module.css`](./effort-declare.module.css) | `--dsw-alias-*` tokens only, so dark theme stays correct. |
|
|
25
|
+
| [`schema-ops.ts`](./schema-ops.ts) | Binds `settingsSchema` as plain callbacks so the service identity is not passed into React. |
|
|
26
|
+
|
|
27
|
+
Visual language follows the official Models page, not `--ds-*` variables with light-mode fallbacks.
|