@dsh-plus/llm-pi 0.1.42 → 0.1.44
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/lib/client.js +1489 -636
- package/lib/index.d.ts +4 -8
- package/lib/index.js +943 -391
- package/package.json +35 -36
- package/src/catalog/browse.ts +297 -0
- package/src/catalog/builtin.ts +183 -35
- package/src/client/api.ts +96 -33
- package/src/client/card.tsx +180 -120
- package/src/client/client.ts +2 -2
- package/src/client/constants.ts +25 -5
- package/src/client/draft.ts +34 -13
- package/src/client/fields.tsx +18 -3
- package/src/client/i18n.ts +153 -49
- package/src/client/root-fields.tsx +64 -52
- package/src/client/route-fit.ts +117 -0
- package/src/client/styles.ts +40 -10
- package/src/client/views/catalog-browser.tsx +376 -0
- package/src/client/views/catalog-row.tsx +147 -0
- package/src/client/views/compat.tsx +31 -3
- package/src/client/views/models.tsx +189 -193
- package/src/client/views/provider-fields.tsx +2 -2
- package/src/client/views/providers.tsx +51 -11
- package/src/compat.ts +43 -19
- package/src/config-api.ts +65 -37
- package/src/config.ts +20 -28
- package/src/inherit.ts +15 -31
- package/src/kit-meta.ts +156 -0
- package/src/{compat-gates.ts → official-surface.ts} +60 -16
- package/src/profiles.ts +52 -13
- package/src/resolve-dsh.ts +291 -72
- package/src/service.ts +209 -90
- package/src/catalog/models-dev.ts +0 -261
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 目录浏览器的单行:路径 id(可复制)+ 名称 + 协议徽标 + 参数行 + 折叠的参数明细
|
|
3
|
+
* + 「添加到 route」(不可添加时按钮文案即原因)。与浏览器主体拆开,
|
|
4
|
+
* 便于各自演进(行内只依赖服务端给的模型事实)。
|
|
5
|
+
* @module llm-pi/client/views/catalog-row
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
ChevronDownIcon,
|
|
10
|
+
copyText,
|
|
11
|
+
IconCheckOutlineRegular,
|
|
12
|
+
IconCopyOutlineRegular,
|
|
13
|
+
} from '@dsh-plus/shared/client'
|
|
14
|
+
import { type ReactElement, useEffect, useRef, useState } from 'react'
|
|
15
|
+
|
|
16
|
+
import type { WireModelInfo } from '../api.ts'
|
|
17
|
+
import type { Translate } from '../i18n.ts'
|
|
18
|
+
import type { AddBlockReason, AddEligibility } from '../route-fit.ts'
|
|
19
|
+
|
|
20
|
+
/** 计数展示(千分位;不引本地化依赖)。 */
|
|
21
|
+
function count(value: number): string {
|
|
22
|
+
return value.toLocaleString()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 参数行的键值短展示:字符串化,长值截断。 */
|
|
26
|
+
function shortValue(value: unknown): string {
|
|
27
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value)
|
|
28
|
+
if (text === undefined) return 'undefined'
|
|
29
|
+
return text.length > 120 ? `${text.slice(0, 117)}…` : text
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 禁止添加的原因文案。 */
|
|
33
|
+
export function blockText(t: Translate, reason: AddBlockReason, detail?: string): string {
|
|
34
|
+
if (reason === 'unsupported') {
|
|
35
|
+
return `${t('blockUnsupported')}${detail === undefined ? '' : `(${detail})`}`
|
|
36
|
+
}
|
|
37
|
+
if (reason === 'duplicate') return t('blockDuplicate')
|
|
38
|
+
if (reason === 'protocol-conflict') {
|
|
39
|
+
return `${t('blockConflict')}${detail === undefined || detail === '' ? '' : `(${detail})`}`
|
|
40
|
+
}
|
|
41
|
+
if (reason === 'route-mixed') return t('blockMixed')
|
|
42
|
+
return t('blockNoRoute')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CatalogRowProps {
|
|
46
|
+
t: Translate
|
|
47
|
+
model: WireModelInfo
|
|
48
|
+
disabled: boolean
|
|
49
|
+
eligibility: AddEligibility
|
|
50
|
+
onAdd(): void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 单行:路径 id(可复制)+ 名称 + 参数行 + 折叠的参数明细 + 添加按钮。 */
|
|
54
|
+
export function CatalogRow(props: CatalogRowProps): ReactElement {
|
|
55
|
+
const { t, model } = props
|
|
56
|
+
const [details, setDetails] = useState(false)
|
|
57
|
+
const [copied, setCopied] = useState(false)
|
|
58
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
59
|
+
useEffect(
|
|
60
|
+
() => () => {
|
|
61
|
+
if (timer.current !== null) clearTimeout(timer.current)
|
|
62
|
+
},
|
|
63
|
+
[],
|
|
64
|
+
)
|
|
65
|
+
const blocked = props.eligibility.ok ? undefined : props.eligibility
|
|
66
|
+
const compatKeys = Object.keys(model.compat ?? {})
|
|
67
|
+
const restKeys = Object.keys(model.rest)
|
|
68
|
+
return (
|
|
69
|
+
<div className="lpc-catRow">
|
|
70
|
+
<div className="lpc-catHead">
|
|
71
|
+
<span className="lpc-catPath">{model.path}</span>
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
className="lpc-iconBtn"
|
|
75
|
+
title={copied ? t('browserCopied') : t('browserCopy')}
|
|
76
|
+
aria-label={t('browserCopy')}
|
|
77
|
+
onClick={() => {
|
|
78
|
+
void copyText(model.path).then((ok) => {
|
|
79
|
+
if (!ok) return
|
|
80
|
+
setCopied(true)
|
|
81
|
+
if (timer.current !== null) clearTimeout(timer.current)
|
|
82
|
+
timer.current = setTimeout(() => setCopied(false), 1500)
|
|
83
|
+
})
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{copied ? <IconCheckOutlineRegular size={14} /> : <IconCopyOutlineRegular size={14} />}
|
|
87
|
+
</button>
|
|
88
|
+
<span className="lpc-catName">{model.name}</span>
|
|
89
|
+
<span className={model.servable ? 'lpc-badge lpc-badgeSet' : 'lpc-badge lpc-badgeUnset'}>
|
|
90
|
+
{model.api}
|
|
91
|
+
</span>
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
95
|
+
disabled={props.disabled || blocked !== undefined}
|
|
96
|
+
title={blocked === undefined ? undefined : blockText(t, blocked.reason, blocked.detail)}
|
|
97
|
+
onClick={props.onAdd}
|
|
98
|
+
>
|
|
99
|
+
{blocked === undefined ? t('browserAdd') : blockText(t, blocked.reason, blocked.detail)}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
<p className="lpc-catParams">
|
|
103
|
+
{`${t('browserContextShort')} ${count(model.contextWindow)} · ${t('browserMaxShort')} ${count(model.maxTokens)} · `}
|
|
104
|
+
{model.input
|
|
105
|
+
.map((m) => (m === 'image' ? t('browserInputImage') : t('browserInputText')))
|
|
106
|
+
.join('/')}
|
|
107
|
+
{model.reasoning ? ` · ${t('browserReasoning')}` : ''}
|
|
108
|
+
{compatKeys.length > 0 ? ` · compat ${compatKeys.length}` : ''}
|
|
109
|
+
</p>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
className="lpc-catDetailsToggle"
|
|
113
|
+
aria-expanded={details}
|
|
114
|
+
onClick={() => setDetails(!details)}
|
|
115
|
+
>
|
|
116
|
+
<ChevronDownIcon className={`lpc-chevron${details ? ' lpc-chevronOpen' : ''}`} />
|
|
117
|
+
<span>{t('browserDetails')}</span>
|
|
118
|
+
</button>
|
|
119
|
+
{details ? (
|
|
120
|
+
<div className="lpc-catDetails">
|
|
121
|
+
{model.thinkingLevelMap !== undefined ? (
|
|
122
|
+
<p className="lpc-catParams">
|
|
123
|
+
{`${t('browserThinkMap')}:${Object.entries(model.thinkingLevelMap)
|
|
124
|
+
.map(([level, wire]) => `${level}=${wire ?? '-'}`)
|
|
125
|
+
.join(' ')}`}
|
|
126
|
+
</p>
|
|
127
|
+
) : null}
|
|
128
|
+
{compatKeys.length > 0 ? (
|
|
129
|
+
<>
|
|
130
|
+
<p className="lpc-catDetailLabel">{t('browserCompat')}</p>
|
|
131
|
+
<pre className="lpc-catJson">{JSON.stringify(model.compat, null, 2)}</pre>
|
|
132
|
+
</>
|
|
133
|
+
) : null}
|
|
134
|
+
{restKeys.length > 0 ? (
|
|
135
|
+
<>
|
|
136
|
+
<p className="lpc-catDetailLabel">{t('browserExtra')}</p>
|
|
137
|
+
<pre className="lpc-catJson">
|
|
138
|
+
{restKeys.map((key) => `${key}: ${shortValue(model.rest[key])}`).join('\n')}
|
|
139
|
+
</pre>
|
|
140
|
+
</>
|
|
141
|
+
) : null}
|
|
142
|
+
<p className="lpc-catParams">{`baseUrl:${model.baseUrl}`}</p>
|
|
143
|
+
</div>
|
|
144
|
+
) : null}
|
|
145
|
+
</div>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* compat 覆盖编辑器:按当前 api 渲染字段组(与服务端 compat.ts 字段表一致)。
|
|
3
3
|
* boolean → 三态下拉(未设置/true/false),枚举 → 下拉(含未设置),
|
|
4
4
|
* object → JSON 文本框(本地文本状态,合法时写入草稿,非法仅提示)。
|
|
5
|
-
*
|
|
5
|
+
* 未知/非法值由后端校验兜底(保存失败会返回校验明细)。
|
|
6
|
+
* 字段多(如 completions 19 项)时提供字段名过滤,折叠标题显示"已设 M/K"。
|
|
6
7
|
* @module llm-pi/client/views/compat
|
|
7
8
|
*/
|
|
8
9
|
import type { ReactElement } from 'react'
|
|
10
|
+
import { useState } from 'react'
|
|
9
11
|
|
|
10
12
|
import { COMPAT_FALLBACK_API, compatFieldSpec, compatFieldsOf } from '../constants.ts'
|
|
11
13
|
import { CollapseSection, IntegerField, JsonField, SelectField } from '../fields.tsx'
|
|
@@ -25,6 +27,9 @@ export function pruneCompatForApi(
|
|
|
25
27
|
return next
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** 字段数超过该值时提供过滤框(19 项一屏放不下)。 */
|
|
31
|
+
const FILTER_THRESHOLD = 10
|
|
32
|
+
|
|
28
33
|
export interface CompatEditorProps {
|
|
29
34
|
idPrefix: string
|
|
30
35
|
api: string
|
|
@@ -37,9 +42,14 @@ export interface CompatEditorProps {
|
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
export function CompatEditor(props: CompatEditorProps): ReactElement {
|
|
45
|
+
const [filter, setFilter] = useState('')
|
|
40
46
|
const effective =
|
|
41
47
|
props.api !== '' && compatFieldsOf(props.api).length > 0 ? props.api : COMPAT_FALLBACK_API
|
|
42
|
-
const
|
|
48
|
+
const allFields = compatFieldsOf(effective)
|
|
49
|
+
const needle = filter.trim().toLowerCase()
|
|
50
|
+
const fields =
|
|
51
|
+
needle === '' ? allFields : allFields.filter((f) => f.toLowerCase().includes(needle))
|
|
52
|
+
const setCount = allFields.filter((field) => props.compat[field] !== undefined).length
|
|
43
53
|
const setField = (field: string, value: unknown): void => {
|
|
44
54
|
const next = { ...props.compat }
|
|
45
55
|
if (value === undefined) delete next[field]
|
|
@@ -47,13 +57,31 @@ export function CompatEditor(props: CompatEditorProps): ReactElement {
|
|
|
47
57
|
props.onEdit(next)
|
|
48
58
|
}
|
|
49
59
|
return (
|
|
50
|
-
<div className={
|
|
60
|
+
<div className={'lpc-field lpc-wide'}>
|
|
51
61
|
<CollapseSection
|
|
52
62
|
id={`${props.idPrefix}-collapse`}
|
|
53
63
|
title={props.t('compatGroup')}
|
|
64
|
+
meta={
|
|
65
|
+
allFields.length === 0
|
|
66
|
+
? ''
|
|
67
|
+
: `${props.t('compatSetCount')} ${setCount}/${allFields.length}`
|
|
68
|
+
}
|
|
54
69
|
defaultOpen={false}
|
|
55
70
|
>
|
|
56
71
|
{props.api === '' ? <p className="lpc-hint">{props.t('compatApiHint')}</p> : null}
|
|
72
|
+
{allFields.length > FILTER_THRESHOLD ? (
|
|
73
|
+
<div className="lpc-catBar">
|
|
74
|
+
<input
|
|
75
|
+
className="lpc-input lpc-catSearch"
|
|
76
|
+
type="search"
|
|
77
|
+
value={filter}
|
|
78
|
+
placeholder={props.t('compatFilter')}
|
|
79
|
+
aria-label={props.t('compatFilter')}
|
|
80
|
+
disabled={props.disabled === true}
|
|
81
|
+
onChange={(event) => setFilter(event.target.value)}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
) : null}
|
|
57
85
|
<div className="lpc-grid">
|
|
58
86
|
{fields.map((field) => {
|
|
59
87
|
const spec = compatFieldSpec(effective, field)
|
|
@@ -1,147 +1,125 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 模型目录编辑:每行一个模型(id/extends/name/容量/模态/reasoningEfforts/compat
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* 模型目录编辑:每行一个模型(id/extends/name/容量/模态/reasoningEfforts/compat/
|
|
3
|
+
* 其余字段),**行默认折叠**——route 内模型一多,展开态会把配置页拉得极长。
|
|
4
|
+
* 工具条提供"全部展开/收起"与 id/继承源过滤;继承源候选不再内联 datalist,
|
|
5
|
+
* 改由卡片顶部「内置模型目录」浏览器承担(搜索 + 一键添加)。
|
|
5
6
|
* @module llm-pi/client/views/models
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import { CheckRow } from '@dsh-plus/shared/client'
|
|
9
|
-
import { type ReactElement,
|
|
10
|
-
|
|
9
|
+
import { CheckRow, ChevronDownIcon } from '@dsh-plus/shared/client'
|
|
10
|
+
import { type ReactElement, useState } from 'react'
|
|
11
|
+
|
|
11
12
|
import { MODALITIES, THINKING_LEVELS } from '../constants.ts'
|
|
12
13
|
import { emptyModelDraft, extraOfJson, type ModelDraft, type ReasoningDraft } from '../draft.ts'
|
|
13
14
|
import { JsonField, TextField } from '../fields.tsx'
|
|
14
15
|
import type { Translate } from '../i18n.ts'
|
|
15
16
|
import { CompatEditor } from './compat.tsx'
|
|
16
17
|
|
|
17
|
-
type CatalogSource = 'builtin' | 'models-dev'
|
|
18
|
-
|
|
19
|
-
function catalogNote(status: WireModelsDevStatus | undefined, t: Translate): string {
|
|
20
|
-
if (status === undefined) return ''
|
|
21
|
-
if (status.error !== null) return `${t('modelsDevError')}${status.error}`
|
|
22
|
-
return `${t('modelsDevStatusLine')}:${status.providers} 个 provider,快照 ${status.fetchedAt ?? '-'}`
|
|
23
|
-
}
|
|
24
|
-
|
|
25
18
|
export interface ModelsTableProps {
|
|
26
19
|
route: string
|
|
27
20
|
api: string
|
|
28
|
-
/** 默认候选 provider(route 级 extends)。 */
|
|
29
|
-
defaultProvider: string
|
|
30
21
|
models: ModelDraft[]
|
|
31
22
|
epoch: number
|
|
32
23
|
disabled?: boolean
|
|
33
24
|
t: Translate
|
|
34
25
|
onModels(next: ModelDraft[]): void
|
|
26
|
+
/** 打开卡片顶部「内置模型目录」并把本 route 设为添加目标。 */
|
|
27
|
+
onAddFromCatalog(): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 行标题摘要:继承源与显示名(空则回退 id)。 */
|
|
31
|
+
function rowSummary(model: ModelDraft): string {
|
|
32
|
+
const parts: string[] = []
|
|
33
|
+
if (model.extends.trim() !== '') parts.push(`extends ${model.extends.trim()}`)
|
|
34
|
+
if (model.name.trim() !== '' && model.name.trim() !== model.id.trim()) {
|
|
35
|
+
parts.push(model.name.trim())
|
|
36
|
+
}
|
|
37
|
+
return parts.join(' · ')
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export function ModelsTable(props: ModelsTableProps): ReactElement {
|
|
38
41
|
const { t } = props
|
|
39
|
-
const [
|
|
40
|
-
const [
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
const [openRows, setOpenRows] = useState<Record<number, boolean>>({})
|
|
43
|
+
const [filter, setFilter] = useState('')
|
|
44
|
+
const needle = filter.trim().toLowerCase()
|
|
45
|
+
const visible = props.models
|
|
46
|
+
.map((model, index) => ({ model, index }))
|
|
47
|
+
.filter(({ model }) =>
|
|
48
|
+
needle === ''
|
|
49
|
+
? true
|
|
50
|
+
: model.id.toLowerCase().includes(needle) ||
|
|
51
|
+
model.extends.toLowerCase().includes(needle) ||
|
|
52
|
+
model.name.toLowerCase().includes(needle),
|
|
53
|
+
)
|
|
54
|
+
const hidden = props.models.length - visible.length
|
|
55
|
+
const allOpen =
|
|
56
|
+
props.models.length > 0 && props.models.every((_, index) => openRows[index] === true)
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
setNote(t('catalogLoading'))
|
|
50
|
-
void (async () => {
|
|
51
|
-
try {
|
|
52
|
-
const list = await fetchCatalog('', source)
|
|
53
|
-
if (!alive) return
|
|
54
|
-
setProviderIds(list.providers)
|
|
55
|
-
const preferred = list.providers.includes(props.defaultProvider)
|
|
56
|
-
? props.defaultProvider
|
|
57
|
-
: (list.providers[0] ?? '')
|
|
58
|
-
setProvider(preferred)
|
|
59
|
-
setNote(catalogNote(list.status, t))
|
|
60
|
-
if (preferred === '') return
|
|
61
|
-
const result = await fetchCatalog(preferred, source)
|
|
62
|
-
if (alive) setCandidateModels(result.models)
|
|
63
|
-
} catch {
|
|
64
|
-
if (alive) setNote(t('catalogFailed'))
|
|
65
|
-
}
|
|
66
|
-
})()
|
|
67
|
-
return () => {
|
|
68
|
-
alive = false
|
|
69
|
-
}
|
|
70
|
-
}, [source])
|
|
71
|
-
|
|
72
|
-
const onProviderChange = (value: string): void => {
|
|
73
|
-
setProvider(value)
|
|
74
|
-
if (value === '') {
|
|
75
|
-
setCandidateModels([])
|
|
58
|
+
const toggleAll = (): void => {
|
|
59
|
+
if (allOpen) {
|
|
60
|
+
setOpenRows({})
|
|
76
61
|
return
|
|
77
62
|
}
|
|
78
|
-
|
|
79
|
-
.then((result) => setCandidateModels(result.models))
|
|
80
|
-
.catch(() => setNote(t('catalogFailed')))
|
|
63
|
+
setOpenRows(Object.fromEntries(props.models.map((_, index) => [index, true])))
|
|
81
64
|
}
|
|
82
65
|
|
|
83
66
|
return (
|
|
84
67
|
<div className="lpc-models">
|
|
85
68
|
<div className="lpc-modelHead">
|
|
86
|
-
<span className="lpc-modelTitle">{t('modelsGroup')}</span>
|
|
69
|
+
<span className="lpc-modelTitle">{`${t('modelsGroup')}(${props.models.length})`}</span>
|
|
87
70
|
<button
|
|
88
71
|
type="button"
|
|
89
72
|
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
90
|
-
disabled={props.disabled === true}
|
|
91
|
-
onClick={
|
|
73
|
+
disabled={props.disabled === true || props.models.length === 0}
|
|
74
|
+
onClick={toggleAll}
|
|
92
75
|
>
|
|
93
|
-
{t('
|
|
76
|
+
{allOpen ? t('modelsCollapseAll') : t('modelsExpandAll')}
|
|
94
77
|
</button>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{t('catalogSource')}
|
|
99
|
-
</label>
|
|
100
|
-
<select
|
|
101
|
-
id={`${listId}-source`}
|
|
102
|
-
className="lpc-input lpc-select lpc-catalogSelect"
|
|
103
|
-
value={source}
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
104
81
|
disabled={props.disabled === true}
|
|
105
|
-
|
|
82
|
+
onClick={props.onAddFromCatalog}
|
|
106
83
|
>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
id={`${listId}-provider`}
|
|
115
|
-
className="lpc-input lpc-select lpc-catalogSelect"
|
|
116
|
-
value={provider}
|
|
117
|
-
disabled={props.disabled === true || providerIds.length === 0}
|
|
118
|
-
onChange={(event) => onProviderChange(event.target.value)}
|
|
84
|
+
{t('addFromCatalog')}
|
|
85
|
+
</button>
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
89
|
+
disabled={props.disabled === true}
|
|
90
|
+
onClick={() => props.onModels([...props.models, emptyModelDraft()])}
|
|
119
91
|
>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
<option key={id} value={id}>
|
|
123
|
-
{id}
|
|
124
|
-
</option>
|
|
125
|
-
))}
|
|
126
|
-
</select>
|
|
92
|
+
{t('addModel')}
|
|
93
|
+
</button>
|
|
127
94
|
</div>
|
|
128
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
95
|
+
{props.models.length > 6 ? (
|
|
96
|
+
<div className="lpc-catBar">
|
|
97
|
+
<input
|
|
98
|
+
className="lpc-input lpc-catSearch"
|
|
99
|
+
type="search"
|
|
100
|
+
value={filter}
|
|
101
|
+
placeholder={t('modelsFilterPlaceholder')}
|
|
102
|
+
aria-label={t('modelsFilter')}
|
|
103
|
+
disabled={props.disabled === true}
|
|
104
|
+
onChange={(event) => setFilter(event.target.value)}
|
|
105
|
+
/>
|
|
106
|
+
{hidden > 0 ? (
|
|
107
|
+
<span className="lpc-catNote">{`${t('modelsHidden')} ${hidden}`}</span>
|
|
108
|
+
) : null}
|
|
109
|
+
</div>
|
|
110
|
+
) : null}
|
|
111
|
+
{visible.map(({ model, index }) => (
|
|
135
112
|
<ModelRow
|
|
136
|
-
// biome-ignore lint/suspicious/noArrayIndexKey: model.id 可重复(手填),index 前缀保证 key 唯一且随行序稳定
|
|
137
113
|
key={`${index}:${model.id}`}
|
|
138
114
|
index={index}
|
|
139
115
|
model={model}
|
|
140
116
|
api={props.api}
|
|
141
|
-
|
|
117
|
+
uid={props.route.replace(/[^a-zA-Z0-9_-]/g, '_')}
|
|
118
|
+
open={openRows[index] === true}
|
|
142
119
|
epoch={props.epoch}
|
|
143
120
|
disabled={props.disabled === true}
|
|
144
121
|
t={t}
|
|
122
|
+
onToggle={() => setOpenRows((prev) => ({ ...prev, [index]: prev[index] !== true }))}
|
|
145
123
|
onPatch={(patch) =>
|
|
146
124
|
props.onModels(props.models.map((m, i) => (i === index ? { ...m, ...patch } : m)))
|
|
147
125
|
}
|
|
@@ -156,23 +134,38 @@ export interface ModelRowProps {
|
|
|
156
134
|
index: number
|
|
157
135
|
model: ModelDraft
|
|
158
136
|
api: string
|
|
159
|
-
|
|
137
|
+
/** route 派生的 id 前缀(DOM id 唯一性)。 */
|
|
138
|
+
uid: string
|
|
139
|
+
open: boolean
|
|
160
140
|
epoch: number
|
|
161
141
|
disabled?: boolean
|
|
162
142
|
t: Translate
|
|
143
|
+
onToggle(): void
|
|
163
144
|
onPatch(patch: Partial<ModelDraft>): void
|
|
164
145
|
onRemove(): void
|
|
165
146
|
}
|
|
166
147
|
|
|
148
|
+
/** 单行:折叠头(序号/id/摘要) + 展开体(全部字段)。 */
|
|
167
149
|
export function ModelRow(props: ModelRowProps): ReactElement {
|
|
168
150
|
const { model, t } = props
|
|
169
|
-
const id =
|
|
151
|
+
const id = `lpc-m-${props.uid}-${props.index}`
|
|
152
|
+
const summary = rowSummary(model)
|
|
170
153
|
return (
|
|
171
154
|
<div className="lpc-modelRow">
|
|
172
155
|
<div className="lpc-modelHead">
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
className="lpc-routeToggle"
|
|
159
|
+
aria-expanded={props.open}
|
|
160
|
+
onClick={props.onToggle}
|
|
161
|
+
>
|
|
162
|
+
<ChevronDownIcon className={`lpc-chevron${props.open ? ' lpc-chevronOpen' : ''}`} />
|
|
163
|
+
<span className="lpc-modelTitle">{`#${props.index + 1}`}</span>
|
|
164
|
+
<span className={model.id.trim() === '' ? 'lpc-invalid' : 'lpc-routeKey'}>
|
|
165
|
+
{model.id.trim() === '' ? t('modelUntitled') : model.id}
|
|
166
|
+
</span>
|
|
167
|
+
{summary !== '' ? <span className="lpc-routeApi">{summary}</span> : null}
|
|
168
|
+
</button>
|
|
176
169
|
<button
|
|
177
170
|
type="button"
|
|
178
171
|
className="lpc-btn lpc-btnGhost lpc-btnSmall"
|
|
@@ -182,100 +175,103 @@ export function ModelRow(props: ModelRowProps): ReactElement {
|
|
|
182
175
|
{t('deleteModel')}
|
|
183
176
|
</button>
|
|
184
177
|
</div>
|
|
185
|
-
|
|
186
|
-
<
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
invalidLabel={t('modelIdRequired')}
|
|
194
|
-
onEdit={(value) => props.onPatch({ id: value })}
|
|
195
|
-
/>
|
|
196
|
-
<TextField
|
|
197
|
-
id={`${id}-extends`}
|
|
198
|
-
label={t('modelExtends')}
|
|
199
|
-
hint={t('modelExtendsHint')}
|
|
200
|
-
value={model.extends}
|
|
201
|
-
list={props.listId}
|
|
202
|
-
disabled={props.disabled === true}
|
|
203
|
-
onEdit={(value) => props.onPatch({ extends: value })}
|
|
204
|
-
/>
|
|
205
|
-
<TextField
|
|
206
|
-
id={`${id}-name`}
|
|
207
|
-
label={t('modelName')}
|
|
208
|
-
value={model.name}
|
|
209
|
-
disabled={props.disabled === true}
|
|
210
|
-
onEdit={(value) => props.onPatch({ name: value })}
|
|
211
|
-
/>
|
|
212
|
-
<TextField
|
|
213
|
-
id={`${id}-ctx`}
|
|
214
|
-
label={t('contextWindow')}
|
|
215
|
-
numeric
|
|
216
|
-
value={model.contextWindow}
|
|
217
|
-
disabled={props.disabled === true}
|
|
218
|
-
onEdit={(value) => props.onPatch({ contextWindow: value })}
|
|
219
|
-
/>
|
|
220
|
-
<TextField
|
|
221
|
-
id={`${id}-max`}
|
|
222
|
-
label={t('maxTokens')}
|
|
223
|
-
numeric
|
|
224
|
-
value={model.maxTokens}
|
|
225
|
-
disabled={props.disabled === true}
|
|
226
|
-
onEdit={(value) => props.onPatch({ maxTokens: value })}
|
|
227
|
-
/>
|
|
228
|
-
<div className="lpc-field">
|
|
229
|
-
<span className="lpc-label">{t('input')}</span>
|
|
230
|
-
{MODALITIES.map((modality) => (
|
|
231
|
-
<CheckRow
|
|
232
|
-
prefix="lpc"
|
|
233
|
-
key={modality}
|
|
234
|
-
id={`${id}-input-${modality}`}
|
|
235
|
-
label={modality}
|
|
236
|
-
checked={model.input[modality]}
|
|
178
|
+
{props.open ? (
|
|
179
|
+
<div className="lpc-modelBody">
|
|
180
|
+
<div className="lpc-grid">
|
|
181
|
+
<TextField
|
|
182
|
+
id={`${id}-id`}
|
|
183
|
+
label={t('modelId')}
|
|
184
|
+
hint={t('modelIdHint')}
|
|
185
|
+
value={model.id}
|
|
237
186
|
disabled={props.disabled === true}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
})
|
|
242
|
-
}
|
|
187
|
+
invalid={model.id.trim() === ''}
|
|
188
|
+
invalidLabel={t('modelIdRequired')}
|
|
189
|
+
onEdit={(value) => props.onPatch({ id: value })}
|
|
243
190
|
/>
|
|
244
|
-
|
|
191
|
+
<TextField
|
|
192
|
+
id={`${id}-extends`}
|
|
193
|
+
label={t('modelExtends')}
|
|
194
|
+
hint={t('modelExtendsHint')}
|
|
195
|
+
value={model.extends}
|
|
196
|
+
disabled={props.disabled === true}
|
|
197
|
+
onEdit={(value) => props.onPatch({ extends: value })}
|
|
198
|
+
/>
|
|
199
|
+
<TextField
|
|
200
|
+
id={`${id}-name`}
|
|
201
|
+
label={t('modelName')}
|
|
202
|
+
value={model.name}
|
|
203
|
+
disabled={props.disabled === true}
|
|
204
|
+
onEdit={(value) => props.onPatch({ name: value })}
|
|
205
|
+
/>
|
|
206
|
+
<TextField
|
|
207
|
+
id={`${id}-ctx`}
|
|
208
|
+
label={t('contextWindow')}
|
|
209
|
+
numeric
|
|
210
|
+
value={model.contextWindow}
|
|
211
|
+
disabled={props.disabled === true}
|
|
212
|
+
onEdit={(value) => props.onPatch({ contextWindow: value })}
|
|
213
|
+
/>
|
|
214
|
+
<TextField
|
|
215
|
+
id={`${id}-max`}
|
|
216
|
+
label={t('maxTokens')}
|
|
217
|
+
numeric
|
|
218
|
+
value={model.maxTokens}
|
|
219
|
+
disabled={props.disabled === true}
|
|
220
|
+
onEdit={(value) => props.onPatch({ maxTokens: value })}
|
|
221
|
+
/>
|
|
222
|
+
<div className="lpc-field">
|
|
223
|
+
<span className="lpc-label">{t('input')}</span>
|
|
224
|
+
{MODALITIES.map((modality) => (
|
|
225
|
+
<CheckRow
|
|
226
|
+
prefix="lpc"
|
|
227
|
+
key={modality}
|
|
228
|
+
id={`${id}-input-${modality}`}
|
|
229
|
+
label={modality}
|
|
230
|
+
checked={model.input[modality]}
|
|
231
|
+
disabled={props.disabled === true}
|
|
232
|
+
onEdit={(checked) =>
|
|
233
|
+
props.onPatch({
|
|
234
|
+
input: { ...model.input, [modality]: checked },
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
/>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
<ReasoningEditor
|
|
242
|
+
idPrefix={`${id}-re`}
|
|
243
|
+
value={model.reasoningEfforts}
|
|
244
|
+
disabled={props.disabled === true}
|
|
245
|
+
t={t}
|
|
246
|
+
onEdit={(reasoningEfforts) => props.onPatch({ reasoningEfforts })}
|
|
247
|
+
/>
|
|
248
|
+
<CompatEditor
|
|
249
|
+
idPrefix={`${id}-compat`}
|
|
250
|
+
api={props.api}
|
|
251
|
+
compat={model.compat}
|
|
252
|
+
epoch={props.epoch}
|
|
253
|
+
disabled={props.disabled === true}
|
|
254
|
+
wide
|
|
255
|
+
t={t}
|
|
256
|
+
onEdit={(compat) => props.onPatch({ compat })}
|
|
257
|
+
/>
|
|
258
|
+
<JsonField
|
|
259
|
+
id={`${id}-extra`}
|
|
260
|
+
label={t('extraFields')}
|
|
261
|
+
hint={t('extraFieldsHint')}
|
|
262
|
+
invalidText={t('invalidJson')}
|
|
263
|
+
value={Object.keys(model.extra).length === 0 ? undefined : model.extra}
|
|
264
|
+
epoch={props.epoch}
|
|
265
|
+
disabled={props.disabled === true}
|
|
266
|
+
wide
|
|
267
|
+
onEdit={(extra) => {
|
|
268
|
+
const next = extraOfJson(extra)
|
|
269
|
+
// 合法但非对象的 JSON(数组/标量)忽略这次编辑,不静默清空已配置字段。
|
|
270
|
+
if (next !== undefined) props.onPatch({ extra: next })
|
|
271
|
+
}}
|
|
272
|
+
/>
|
|
245
273
|
</div>
|
|
246
|
-
|
|
247
|
-
<ReasoningEditor
|
|
248
|
-
idPrefix={`${id}-re`}
|
|
249
|
-
value={model.reasoningEfforts}
|
|
250
|
-
disabled={props.disabled === true}
|
|
251
|
-
t={t}
|
|
252
|
-
onEdit={(reasoningEfforts) => props.onPatch({ reasoningEfforts })}
|
|
253
|
-
/>
|
|
254
|
-
<CompatEditor
|
|
255
|
-
idPrefix={`${id}-compat`}
|
|
256
|
-
api={props.api}
|
|
257
|
-
compat={model.compat}
|
|
258
|
-
epoch={props.epoch}
|
|
259
|
-
disabled={props.disabled === true}
|
|
260
|
-
wide
|
|
261
|
-
t={t}
|
|
262
|
-
onEdit={(compat) => props.onPatch({ compat })}
|
|
263
|
-
/>
|
|
264
|
-
<JsonField
|
|
265
|
-
id={`${id}-extra`}
|
|
266
|
-
label={t('extraFields')}
|
|
267
|
-
hint={t('extraFieldsHint')}
|
|
268
|
-
invalidText={t('invalidJson')}
|
|
269
|
-
value={Object.keys(model.extra).length === 0 ? undefined : model.extra}
|
|
270
|
-
epoch={props.epoch}
|
|
271
|
-
disabled={props.disabled === true}
|
|
272
|
-
wide
|
|
273
|
-
onEdit={(extra) => {
|
|
274
|
-
const next = extraOfJson(extra)
|
|
275
|
-
// 合法但非对象的 JSON(数组/标量)忽略这次编辑,不静默清空已配置字段。
|
|
276
|
-
if (next !== undefined) props.onPatch({ extra: next })
|
|
277
|
-
}}
|
|
278
|
-
/>
|
|
274
|
+
) : null}
|
|
279
275
|
</div>
|
|
280
276
|
)
|
|
281
277
|
}
|