@dsh-plus/image-studio 0.1.0 → 0.1.1
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 +3569 -60
- package/lib/index.d.ts +2 -1
- package/lib/index.js +3 -2
- package/package.json +5 -3
- package/src/client/card.tsx +579 -9
- package/src/client/client.ts +69 -36
- package/src/client/i18n.ts +230 -9
- package/src/client/panel/controller.ts +75 -0
- package/src/client/panel/entry.tsx +35 -0
- package/src/client/panel/gallery.tsx +217 -0
- package/src/client/panel/generator.tsx +521 -0
- package/src/client/panel/panel.tsx +220 -0
- package/src/client/panel/param-state.ts +144 -0
- package/src/client/panel/params-form.tsx +136 -0
- package/src/client/panel/picker.tsx +117 -0
- package/src/client/panel/tasks.tsx +72 -0
- package/src/client/styles.ts +554 -17
- package/src/service.ts +7 -5
- package/src/client/section.tsx +0 -14
package/lib/index.d.ts
CHANGED
|
@@ -161,6 +161,7 @@ interface PresetsWire {
|
|
|
161
161
|
//#endregion
|
|
162
162
|
//#region src/service.d.ts
|
|
163
163
|
declare class ImageStudioService extends Service {
|
|
164
|
+
static [Context.inject]: string[];
|
|
164
165
|
private current;
|
|
165
166
|
private readonly runner;
|
|
166
167
|
private readonly log;
|
|
@@ -184,7 +185,7 @@ declare class ImageStudioService extends Service {
|
|
|
184
185
|
/** set/unset 凭据(端点写通道)。 */
|
|
185
186
|
setCredential(presetId: string, value: string): Promise<void>;
|
|
186
187
|
unsetCredential(presetId: string): Promise<void>;
|
|
187
|
-
/** credentials seam
|
|
188
|
+
/** credentials seam(inject 硬声明保证时序,见类声明;缺席即组合缺服务)。 */
|
|
188
189
|
private credentialsSeam;
|
|
189
190
|
private requireCredentials;
|
|
190
191
|
/** 提交生图任务:校验 → 入队 → 返回 taskId。 */
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
4
4
|
import { request } from "node:http";
|
|
5
5
|
import { request as request$1 } from "node:https";
|
|
@@ -1007,6 +1007,7 @@ function defaultIdCounter() {
|
|
|
1007
1007
|
*/
|
|
1008
1008
|
/** 静态 schema 引用(installSection 用;避免循环 import config → service)。 */
|
|
1009
1009
|
var ImageStudioService = class extends Service {
|
|
1010
|
+
static [Context.inject] = ["credentials"];
|
|
1010
1011
|
current;
|
|
1011
1012
|
runner;
|
|
1012
1013
|
log;
|
|
@@ -1070,7 +1071,7 @@ var ImageStudioService = class extends Service {
|
|
|
1070
1071
|
async unsetCredential(presetId) {
|
|
1071
1072
|
await (await this.requireCredentials()).unset(credentialRef(credentialRefNameOf(presetId)));
|
|
1072
1073
|
}
|
|
1073
|
-
/** credentials seam
|
|
1074
|
+
/** credentials seam(inject 硬声明保证时序,见类声明;缺席即组合缺服务)。 */
|
|
1074
1075
|
async credentialsSeam() {
|
|
1075
1076
|
const found = this.ctx.get?.("credentials");
|
|
1076
1077
|
return found === void 0 || found === null ? null : found;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/image-studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "dsh-plus service+ui plugin: 图像工作室——OpenAI Images 协议文生图/图生图,全参数开关化,双预设体系,并发生图任务与可二次编辑画廊",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
"src"
|
|
20
20
|
],
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"@dsh-plus/shared": "0.1.12",
|
|
23
|
+
"https-proxy-agent": "^9.1.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
27
28
|
"@deepseek-ai/dsh-credentials": "*",
|
|
28
29
|
"@deepseek-ai/dsh-host-webserver": "*",
|
|
29
30
|
"@deepseek-ai/dsh-settings": "*",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"@deepseek-ai/cordis": "4.0.2",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.1.5-alpha.1",
|
|
34
36
|
"@deepseek-ai/dsh-credentials": "0.1.5-alpha.1",
|
|
35
37
|
"@deepseek-ai/dsh-host-webserver": "0.1.5-alpha.1",
|
|
36
38
|
"@deepseek-ai/dsh-session": "0.1.5-alpha.1",
|
package/src/client/card.tsx
CHANGED
|
@@ -1,14 +1,584 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* 配置卡片(settings.plugin.item,key = settings 命名空间):
|
|
3
|
+
* 三组预设(提供商/参数/提示词)+ 高级项(并发/超时/代理/画廊上限)的
|
|
4
|
+
* staged draft 编辑;提供商预设逐条附 API Key 管理(凭据端点,值不回显)。
|
|
5
|
+
* JSON 字段(extraHeaders/paramSpecs)以文本镜像编辑,保存时解析 +
|
|
6
|
+
* validateParamSpecs 本地校验,非法即拦在保存前。
|
|
4
7
|
* @module image-studio/client/card
|
|
5
8
|
*/
|
|
9
|
+
import {
|
|
10
|
+
CardChrome,
|
|
11
|
+
type CardStatusState,
|
|
12
|
+
IDLE_STATUS,
|
|
13
|
+
type NamespaceSettingsApi,
|
|
14
|
+
type Scope,
|
|
15
|
+
TextField,
|
|
16
|
+
} from '@dsh-plus/shared/client'
|
|
17
|
+
import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
import type { ImageStudioConfig } from '../config.ts'
|
|
20
|
+
import { credentialRefNameOf, isValidPresetId } from '../credentials.ts'
|
|
21
|
+
import { validateParamSpecs } from '../params/spec.ts'
|
|
22
|
+
import { fetchCredentialStatus, fetchProviders, setCredential, unsetCredential } from './api.ts'
|
|
23
|
+
import type { Translate } from './i18n.ts'
|
|
24
|
+
|
|
25
|
+
export interface CardProps {
|
|
26
|
+
t: Translate
|
|
27
|
+
scope: Scope
|
|
28
|
+
api: NamespaceSettingsApi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 编辑态草稿:JSON 字段与数值以文本承载(保存时解析校验)。 */
|
|
32
|
+
interface ProviderDraft {
|
|
33
|
+
id: string
|
|
34
|
+
name: string
|
|
35
|
+
protocol: string
|
|
36
|
+
baseUrl: string
|
|
37
|
+
model: string
|
|
38
|
+
extraHeadersText: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ParamDraft {
|
|
42
|
+
id: string
|
|
43
|
+
name: string
|
|
44
|
+
endpoint: string
|
|
45
|
+
specsText: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface PromptDraft {
|
|
49
|
+
id: string
|
|
50
|
+
name: string
|
|
51
|
+
text: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Draft {
|
|
55
|
+
providers: ProviderDraft[]
|
|
56
|
+
params: ParamDraft[]
|
|
57
|
+
prompts: PromptDraft[]
|
|
58
|
+
maxConcurrent: string
|
|
59
|
+
requestTimeoutMs: string
|
|
60
|
+
proxy: string
|
|
61
|
+
galleryMaxItems: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 配置值 → 编辑草稿。 */
|
|
65
|
+
function draftOf(value: ImageStudioConfig): Draft {
|
|
66
|
+
return {
|
|
67
|
+
providers: value.providerPresets.map((p) => ({
|
|
68
|
+
id: p.id,
|
|
69
|
+
name: p.name,
|
|
70
|
+
protocol: p.protocol,
|
|
71
|
+
baseUrl: p.baseUrl,
|
|
72
|
+
model: p.model,
|
|
73
|
+
extraHeadersText: JSON.stringify(p.extraHeaders ?? {}, null, 2),
|
|
74
|
+
})),
|
|
75
|
+
params: value.paramPresets.map((p) => ({
|
|
76
|
+
id: p.id,
|
|
77
|
+
name: p.name,
|
|
78
|
+
endpoint: p.endpoint,
|
|
79
|
+
specsText: JSON.stringify(p.paramSpecs ?? {}, null, 2),
|
|
80
|
+
})),
|
|
81
|
+
prompts: value.promptPresets.map((p) => ({ id: p.id, name: p.name, text: p.text })),
|
|
82
|
+
maxConcurrent: String(value.maxConcurrent),
|
|
83
|
+
requestTimeoutMs: String(value.requestTimeoutMs),
|
|
84
|
+
proxy: value.proxy,
|
|
85
|
+
galleryMaxItems: String(value.galleryMaxItems),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseJson(text: string, label: string): unknown {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(text)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw new Error(`${label} ${error instanceof Error ? error.message : String(error)}`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 草稿 → settings 载荷(边界校验:JSON 解析、参数表校验、预设 id 形态)。 */
|
|
98
|
+
function payloadOf(draft: Draft): Record<string, unknown> {
|
|
99
|
+
const providers = draft.providers.map((p) => {
|
|
100
|
+
if (!isValidPresetId(p.id)) throw new Error(p.id)
|
|
101
|
+
return {
|
|
102
|
+
id: p.id,
|
|
103
|
+
name: p.name,
|
|
104
|
+
protocol: p.protocol,
|
|
105
|
+
baseUrl: p.baseUrl,
|
|
106
|
+
model: p.model,
|
|
107
|
+
credentialRef: credentialRefNameOf(p.id),
|
|
108
|
+
extraHeaders: parseJson(p.extraHeadersText, p.id) as Record<string, string>,
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
const params = draft.params.map((p) => ({
|
|
112
|
+
id: p.id,
|
|
113
|
+
name: p.name,
|
|
114
|
+
endpoint: p.endpoint,
|
|
115
|
+
paramSpecs: validateParamSpecs(parseJson(p.specsText, p.id)),
|
|
116
|
+
}))
|
|
117
|
+
return {
|
|
118
|
+
promptPresets: draft.prompts.map((p) => ({ id: p.id, name: p.name, text: p.text })),
|
|
119
|
+
paramPresets: params,
|
|
120
|
+
providerPresets: providers,
|
|
121
|
+
maxConcurrent: Math.max(0, Math.floor(Number(draft.maxConcurrent) || 0)),
|
|
122
|
+
requestTimeoutMs: Math.max(10_000, Math.floor(Number(draft.requestTimeoutMs) || 300_000)),
|
|
123
|
+
proxy: draft.proxy,
|
|
124
|
+
galleryMaxItems: Math.max(0, Math.floor(Number(draft.galleryMaxItems) || 0)),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 单个提供商预设的 API Key 行(describe 徽标 + 设置/删除,值不回显)。 */
|
|
129
|
+
function CredentialRow(props: { t: Translate; presetId: string; disabled: boolean }): ReactElement {
|
|
130
|
+
const { t, presetId, disabled } = props
|
|
131
|
+
const [configured, setConfigured] = useState<boolean | null>(null)
|
|
132
|
+
const [value, setValue] = useState('')
|
|
133
|
+
const [busy, setBusy] = useState(false)
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!isValidPresetId(presetId)) return
|
|
137
|
+
let alive = true
|
|
138
|
+
fetchCredentialStatus(presetId)
|
|
139
|
+
.then((res) => {
|
|
140
|
+
if (alive) setConfigured(res.configured)
|
|
141
|
+
})
|
|
142
|
+
.catch(() => {})
|
|
143
|
+
return () => {
|
|
144
|
+
alive = false
|
|
145
|
+
}
|
|
146
|
+
}, [presetId])
|
|
147
|
+
|
|
148
|
+
const act = (fn: () => Promise<unknown>): void => {
|
|
149
|
+
setBusy(true)
|
|
150
|
+
fn()
|
|
151
|
+
.then(() => fetchCredentialStatus(presetId))
|
|
152
|
+
.then((res) => {
|
|
153
|
+
setConfigured(res.configured)
|
|
154
|
+
setValue('')
|
|
155
|
+
})
|
|
156
|
+
.catch(() => {})
|
|
157
|
+
.finally(() => setBusy(false))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<div className="imsc-credRow">
|
|
162
|
+
<input
|
|
163
|
+
className="imsc-input"
|
|
164
|
+
type="password"
|
|
165
|
+
autoComplete="off"
|
|
166
|
+
placeholder={t('card.credential.placeholder')}
|
|
167
|
+
aria-label={t('card.credential.label')}
|
|
168
|
+
value={value}
|
|
169
|
+
disabled={disabled || busy}
|
|
170
|
+
onChange={(event) => setValue(event.target.value)}
|
|
171
|
+
/>
|
|
172
|
+
{configured !== null ? (
|
|
173
|
+
<span className={`imsc-badge ${configured ? 'imsc-badgeSet' : 'imsc-badgeUnset'}`}>
|
|
174
|
+
{t(configured ? 'card.credential.configured' : 'card.credential.unconfigured')}
|
|
175
|
+
</span>
|
|
176
|
+
) : null}
|
|
177
|
+
<button
|
|
178
|
+
type="button"
|
|
179
|
+
className="imsc-btn imsc-btnGhost"
|
|
180
|
+
disabled={disabled || busy || value === '' || !isValidPresetId(presetId)}
|
|
181
|
+
onClick={() => act(() => setCredential(presetId, value))}
|
|
182
|
+
>
|
|
183
|
+
{t('card.credential.set')}
|
|
184
|
+
</button>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
className="imsc-btn imsc-btnGhost"
|
|
188
|
+
disabled={disabled || busy || configured !== true || !isValidPresetId(presetId)}
|
|
189
|
+
onClick={() => act(() => unsetCredential(presetId))}
|
|
190
|
+
>
|
|
191
|
+
{t('card.credential.unset')}
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function StudioConfigCard(props: CardProps): ReactElement | null {
|
|
198
|
+
const { t, scope, api } = props
|
|
199
|
+
const snapshot = useSyncExternalStore(
|
|
200
|
+
(listener: () => void) => scope.subscribe(listener),
|
|
201
|
+
() => scope.getSnapshot(),
|
|
202
|
+
)
|
|
203
|
+
const value = snapshot.value as ImageStudioConfig | undefined
|
|
204
|
+
const [open, setOpen] = useState(false)
|
|
205
|
+
const [draft, setDraft] = useState<Draft | null>(null)
|
|
206
|
+
const [baseline, setBaseline] = useState('')
|
|
207
|
+
const [saving, setSaving] = useState(false)
|
|
208
|
+
const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
|
|
209
|
+
const [protocols, setProtocols] = useState<Array<{ id: string; label: string }>>([])
|
|
210
|
+
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
fetchProviders()
|
|
213
|
+
.then((res) => setProtocols(res.protocols.map((p) => ({ id: p.id, label: p.label }))))
|
|
214
|
+
.catch(() => {})
|
|
215
|
+
}, [])
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (value === undefined || draft !== null) return
|
|
219
|
+
const initial = draftOf(value)
|
|
220
|
+
setDraft(initial)
|
|
221
|
+
setBaseline(JSON.stringify(initial))
|
|
222
|
+
}, [value, draft])
|
|
223
|
+
|
|
224
|
+
const dirty = useMemo(
|
|
225
|
+
() => draft !== null && JSON.stringify(draft) !== baseline,
|
|
226
|
+
[draft, baseline],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if (value === undefined || draft === null) {
|
|
230
|
+
return (
|
|
231
|
+
<li className="imsc-card">
|
|
232
|
+
<p className="imsc-loading">{t('common.loading')}</p>
|
|
233
|
+
</li>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const disabled = !snapshot.writable
|
|
238
|
+
|
|
239
|
+
const onSave = (): void => {
|
|
240
|
+
let payload: Record<string, unknown>
|
|
241
|
+
try {
|
|
242
|
+
payload = payloadOf(draft)
|
|
243
|
+
} catch (error) {
|
|
244
|
+
setStatus({
|
|
245
|
+
kind: 'error',
|
|
246
|
+
text: `${t('card.specsInvalid')}${error instanceof Error ? error.message : String(error)}`,
|
|
247
|
+
})
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
setSaving(true)
|
|
251
|
+
api
|
|
252
|
+
.update(payload, snapshot.revision)
|
|
253
|
+
.then(() => scope.load())
|
|
254
|
+
.then(() => {
|
|
255
|
+
setBaseline(JSON.stringify(draft))
|
|
256
|
+
setStatus({ kind: 'ok', text: t('card.saved') })
|
|
257
|
+
})
|
|
258
|
+
.catch((error: unknown) => {
|
|
259
|
+
setStatus({
|
|
260
|
+
kind: 'error',
|
|
261
|
+
text: `${t('card.saveFailed')}${error instanceof Error ? error.message : String(error)}`,
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
.finally(() => setSaving(false))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const patch = (next: Partial<Draft>): void => {
|
|
268
|
+
setDraft({ ...draft, ...next })
|
|
269
|
+
setStatus(IDLE_STATUS)
|
|
270
|
+
}
|
|
271
|
+
const patchProvider = (index: number, next: Partial<ProviderDraft>): void =>
|
|
272
|
+
patch({ providers: draft.providers.map((p, i) => (i === index ? { ...p, ...next } : p)) })
|
|
273
|
+
const patchParam = (index: number, next: Partial<ParamDraft>): void =>
|
|
274
|
+
patch({ params: draft.params.map((p, i) => (i === index ? { ...p, ...next } : p)) })
|
|
275
|
+
const patchPrompt = (index: number, next: Partial<PromptDraft>): void =>
|
|
276
|
+
patch({ prompts: draft.prompts.map((p, i) => (i === index ? { ...p, ...next } : p)) })
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<CardChrome
|
|
280
|
+
prefix="imsc"
|
|
281
|
+
title={t('card.title')}
|
|
282
|
+
description={t('card.description')}
|
|
283
|
+
open={open}
|
|
284
|
+
onToggle={setOpen}
|
|
285
|
+
statusBadge={{
|
|
286
|
+
text:
|
|
287
|
+
draft.providers.length > 0
|
|
288
|
+
? t('card.badgeOn').replace('{n}', String(draft.providers.length))
|
|
289
|
+
: t('card.badgeOff'),
|
|
290
|
+
on: draft.providers.length > 0,
|
|
291
|
+
}}
|
|
292
|
+
dirty={dirty}
|
|
293
|
+
dirtyLabel={t('common.unsaved')}
|
|
294
|
+
readOnlyNotice={disabled ? t('card.readOnly') : undefined}
|
|
295
|
+
status={status}
|
|
296
|
+
actions={[
|
|
297
|
+
{
|
|
298
|
+
key: 'discard',
|
|
299
|
+
label: t('card.discard'),
|
|
300
|
+
disabled: !dirty || saving,
|
|
301
|
+
onClick: () => {
|
|
302
|
+
const restored = draftOf(value)
|
|
303
|
+
setDraft(restored)
|
|
304
|
+
setBaseline(JSON.stringify(restored))
|
|
305
|
+
setStatus(IDLE_STATUS)
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
key: 'save',
|
|
310
|
+
label: t(saving ? 'common.saving' : 'card.save'),
|
|
311
|
+
variant: 'primary',
|
|
312
|
+
disabled: !dirty || saving || disabled,
|
|
313
|
+
onClick: onSave,
|
|
314
|
+
},
|
|
315
|
+
]}
|
|
316
|
+
>
|
|
317
|
+
<h4 className="imsc-groupTitle">{t('card.providers')}</h4>
|
|
318
|
+
{draft.providers.map((provider, index) => (
|
|
319
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id(id 字段本身可编辑),行序即身份
|
|
320
|
+
<div className="imsc-presetBox" key={`provider-${index}`}>
|
|
321
|
+
<div className="imsc-presetHead">
|
|
322
|
+
<span className="imsc-presetName">{provider.name || provider.id || '—'}</span>
|
|
323
|
+
<button
|
|
324
|
+
type="button"
|
|
325
|
+
className="imsc-btn imsc-btnGhost"
|
|
326
|
+
disabled={disabled}
|
|
327
|
+
onClick={() => patch({ providers: draft.providers.filter((_, i) => i !== index) })}
|
|
328
|
+
>
|
|
329
|
+
{t('common.delete')}
|
|
330
|
+
</button>
|
|
331
|
+
</div>
|
|
332
|
+
<div className="imsc-grid2">
|
|
333
|
+
<TextField
|
|
334
|
+
prefix="imsc"
|
|
335
|
+
id={`imsc-pid-${index}`}
|
|
336
|
+
label={t('card.field.id')}
|
|
337
|
+
hint={t('card.idHint')}
|
|
338
|
+
value={provider.id}
|
|
339
|
+
invalid={provider.id !== '' && !isValidPresetId(provider.id)}
|
|
340
|
+
invalidLabel={t('card.idInvalid')}
|
|
341
|
+
disabled={disabled}
|
|
342
|
+
onEdit={(v) => patchProvider(index, { id: v })}
|
|
343
|
+
/>
|
|
344
|
+
<TextField
|
|
345
|
+
prefix="imsc"
|
|
346
|
+
id={`imsc-pname-${index}`}
|
|
347
|
+
label={t('card.field.name')}
|
|
348
|
+
value={provider.name}
|
|
349
|
+
disabled={disabled}
|
|
350
|
+
onEdit={(v) => patchProvider(index, { name: v })}
|
|
351
|
+
/>
|
|
352
|
+
</div>
|
|
353
|
+
<div className="imsc-grid2">
|
|
354
|
+
<div className="imsc-field">
|
|
355
|
+
<div className="imsc-head">
|
|
356
|
+
<label className="imsc-label" htmlFor={`imsc-pproto-${index}`}>
|
|
357
|
+
{t('card.field.protocol')}
|
|
358
|
+
</label>
|
|
359
|
+
</div>
|
|
360
|
+
<select
|
|
361
|
+
id={`imsc-pproto-${index}`}
|
|
362
|
+
className="imsc-select"
|
|
363
|
+
value={provider.protocol}
|
|
364
|
+
disabled={disabled}
|
|
365
|
+
onChange={(event) => patchProvider(index, { protocol: event.target.value })}
|
|
366
|
+
>
|
|
367
|
+
{(protocols.length > 0
|
|
368
|
+
? protocols
|
|
369
|
+
: [{ id: provider.protocol, label: provider.protocol }]
|
|
370
|
+
).map((protocol) => (
|
|
371
|
+
<option key={protocol.id} value={protocol.id}>
|
|
372
|
+
{protocol.label}
|
|
373
|
+
</option>
|
|
374
|
+
))}
|
|
375
|
+
</select>
|
|
376
|
+
</div>
|
|
377
|
+
<TextField
|
|
378
|
+
prefix="imsc"
|
|
379
|
+
id={`imsc-pmodel-${index}`}
|
|
380
|
+
label={t('card.field.model')}
|
|
381
|
+
value={provider.model}
|
|
382
|
+
disabled={disabled}
|
|
383
|
+
onEdit={(v) => patchProvider(index, { model: v })}
|
|
384
|
+
/>
|
|
385
|
+
</div>
|
|
386
|
+
<TextField
|
|
387
|
+
prefix="imsc"
|
|
388
|
+
id={`imsc-purl-${index}`}
|
|
389
|
+
label={t('card.field.baseUrl')}
|
|
390
|
+
value={provider.baseUrl}
|
|
391
|
+
disabled={disabled}
|
|
392
|
+
onEdit={(v) => patchProvider(index, { baseUrl: v })}
|
|
393
|
+
/>
|
|
394
|
+
<div className="imsc-field">
|
|
395
|
+
<div className="imsc-head">
|
|
396
|
+
<label className="imsc-label" htmlFor={`imsc-pheaders-${index}`}>
|
|
397
|
+
{t('card.field.extraHeaders')}
|
|
398
|
+
</label>
|
|
399
|
+
</div>
|
|
400
|
+
<textarea
|
|
401
|
+
id={`imsc-pheaders-${index}`}
|
|
402
|
+
className="imsc-textarea"
|
|
403
|
+
value={provider.extraHeadersText}
|
|
404
|
+
disabled={disabled}
|
|
405
|
+
onChange={(event) => patchProvider(index, { extraHeadersText: event.target.value })}
|
|
406
|
+
/>
|
|
407
|
+
</div>
|
|
408
|
+
<CredentialRow t={t} presetId={provider.id} disabled={disabled} />
|
|
409
|
+
</div>
|
|
410
|
+
))}
|
|
411
|
+
<button
|
|
412
|
+
type="button"
|
|
413
|
+
className="imsc-btn imsc-btnGhost"
|
|
414
|
+
disabled={disabled}
|
|
415
|
+
onClick={() =>
|
|
416
|
+
patch({
|
|
417
|
+
providers: [
|
|
418
|
+
...draft.providers,
|
|
419
|
+
{
|
|
420
|
+
id: '',
|
|
421
|
+
name: '',
|
|
422
|
+
protocol: protocols[0]?.id ?? 'openai-images',
|
|
423
|
+
baseUrl: '',
|
|
424
|
+
model: '',
|
|
425
|
+
extraHeadersText: '{}',
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
>
|
|
431
|
+
{t('card.add')} · {t('card.providers')}
|
|
432
|
+
</button>
|
|
433
|
+
|
|
434
|
+
<h4 className="imsc-groupTitle">{t('card.prompts')}</h4>
|
|
435
|
+
{draft.prompts.map((prompt, index) => (
|
|
436
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id,行序即身份
|
|
437
|
+
<div className="imsc-presetBox" key={`prompt-${index}`}>
|
|
438
|
+
<div className="imsc-presetHead">
|
|
439
|
+
<input
|
|
440
|
+
className="imsc-input"
|
|
441
|
+
aria-label={t('card.field.name')}
|
|
442
|
+
value={prompt.name}
|
|
443
|
+
disabled={disabled}
|
|
444
|
+
onChange={(event) => patchPrompt(index, { name: event.target.value })}
|
|
445
|
+
/>
|
|
446
|
+
<button
|
|
447
|
+
type="button"
|
|
448
|
+
className="imsc-btn imsc-btnGhost"
|
|
449
|
+
disabled={disabled}
|
|
450
|
+
onClick={() => patch({ prompts: draft.prompts.filter((_, i) => i !== index) })}
|
|
451
|
+
>
|
|
452
|
+
{t('common.delete')}
|
|
453
|
+
</button>
|
|
454
|
+
</div>
|
|
455
|
+
<textarea
|
|
456
|
+
className="imsc-textarea"
|
|
457
|
+
aria-label={t('card.field.text')}
|
|
458
|
+
value={prompt.text}
|
|
459
|
+
disabled={disabled}
|
|
460
|
+
onChange={(event) => patchPrompt(index, { text: event.target.value })}
|
|
461
|
+
/>
|
|
462
|
+
</div>
|
|
463
|
+
))}
|
|
464
|
+
<button
|
|
465
|
+
type="button"
|
|
466
|
+
className="imsc-btn imsc-btnGhost"
|
|
467
|
+
disabled={disabled}
|
|
468
|
+
onClick={() =>
|
|
469
|
+
patch({
|
|
470
|
+
prompts: [
|
|
471
|
+
...draft.prompts,
|
|
472
|
+
{ id: `prompt-${Date.now().toString(36)}`, name: '', text: '' },
|
|
473
|
+
],
|
|
474
|
+
})
|
|
475
|
+
}
|
|
476
|
+
>
|
|
477
|
+
{t('card.add')} · {t('card.prompts')}
|
|
478
|
+
</button>
|
|
479
|
+
|
|
480
|
+
<h4 className="imsc-groupTitle">{t('card.params')}</h4>
|
|
481
|
+
{draft.params.map((param, index) => (
|
|
482
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: 草稿行无稳定 id,行序即身份
|
|
483
|
+
<div className="imsc-presetBox" key={`param-${index}`}>
|
|
484
|
+
<div className="imsc-presetHead">
|
|
485
|
+
<input
|
|
486
|
+
className="imsc-input"
|
|
487
|
+
aria-label={t('card.field.name')}
|
|
488
|
+
value={param.name}
|
|
489
|
+
disabled={disabled}
|
|
490
|
+
onChange={(event) => patchParam(index, { name: event.target.value })}
|
|
491
|
+
/>
|
|
492
|
+
<select
|
|
493
|
+
className="imsc-select"
|
|
494
|
+
aria-label={t('card.field.endpoint')}
|
|
495
|
+
value={param.endpoint}
|
|
496
|
+
disabled={disabled}
|
|
497
|
+
onChange={(event) => patchParam(index, { endpoint: event.target.value })}
|
|
498
|
+
>
|
|
499
|
+
<option value="generation">{t('tab.generation')}</option>
|
|
500
|
+
<option value="edit">{t('tab.edit')}</option>
|
|
501
|
+
</select>
|
|
502
|
+
<button
|
|
503
|
+
type="button"
|
|
504
|
+
className="imsc-btn imsc-btnGhost"
|
|
505
|
+
disabled={disabled}
|
|
506
|
+
onClick={() => patch({ params: draft.params.filter((_, i) => i !== index) })}
|
|
507
|
+
>
|
|
508
|
+
{t('common.delete')}
|
|
509
|
+
</button>
|
|
510
|
+
</div>
|
|
511
|
+
<textarea
|
|
512
|
+
className="imsc-textarea"
|
|
513
|
+
aria-label={t('card.field.paramSpecs')}
|
|
514
|
+
placeholder='{ "size": { "enabled": true, "value": "1024x1024" } }'
|
|
515
|
+
value={param.specsText}
|
|
516
|
+
disabled={disabled}
|
|
517
|
+
onChange={(event) => patchParam(index, { specsText: event.target.value })}
|
|
518
|
+
/>
|
|
519
|
+
</div>
|
|
520
|
+
))}
|
|
521
|
+
<button
|
|
522
|
+
type="button"
|
|
523
|
+
className="imsc-btn imsc-btnGhost"
|
|
524
|
+
disabled={disabled}
|
|
525
|
+
onClick={() =>
|
|
526
|
+
patch({
|
|
527
|
+
params: [
|
|
528
|
+
...draft.params,
|
|
529
|
+
{
|
|
530
|
+
id: `param-${Date.now().toString(36)}`,
|
|
531
|
+
name: '',
|
|
532
|
+
endpoint: 'generation',
|
|
533
|
+
specsText: '{}',
|
|
534
|
+
},
|
|
535
|
+
],
|
|
536
|
+
})
|
|
537
|
+
}
|
|
538
|
+
>
|
|
539
|
+
{t('card.add')} · {t('card.params')}
|
|
540
|
+
</button>
|
|
541
|
+
|
|
542
|
+
<h4 className="imsc-groupTitle">{t('card.advanced')}</h4>
|
|
543
|
+
<div className="imsc-grid2">
|
|
544
|
+
<TextField
|
|
545
|
+
prefix="imsc"
|
|
546
|
+
id="imsc-conc"
|
|
547
|
+
label={t('card.maxConcurrent')}
|
|
548
|
+
numeric
|
|
549
|
+
value={draft.maxConcurrent}
|
|
550
|
+
disabled={disabled}
|
|
551
|
+
onEdit={(v) => patch({ maxConcurrent: v })}
|
|
552
|
+
/>
|
|
553
|
+
<TextField
|
|
554
|
+
prefix="imsc"
|
|
555
|
+
id="imsc-timeout"
|
|
556
|
+
label={t('card.requestTimeoutMs')}
|
|
557
|
+
numeric
|
|
558
|
+
value={draft.requestTimeoutMs}
|
|
559
|
+
disabled={disabled}
|
|
560
|
+
onEdit={(v) => patch({ requestTimeoutMs: v })}
|
|
561
|
+
/>
|
|
562
|
+
</div>
|
|
563
|
+
<div className="imsc-grid2">
|
|
564
|
+
<TextField
|
|
565
|
+
prefix="imsc"
|
|
566
|
+
id="imsc-proxy"
|
|
567
|
+
label={t('card.proxy')}
|
|
568
|
+
value={draft.proxy}
|
|
569
|
+
disabled={disabled}
|
|
570
|
+
onEdit={(v) => patch({ proxy: v })}
|
|
571
|
+
/>
|
|
572
|
+
<TextField
|
|
573
|
+
prefix="imsc"
|
|
574
|
+
id="imsc-gmax"
|
|
575
|
+
label={t('card.galleryMaxItems')}
|
|
576
|
+
numeric
|
|
577
|
+
value={draft.galleryMaxItems}
|
|
578
|
+
disabled={disabled}
|
|
579
|
+
onEdit={(v) => patch({ galleryMaxItems: v })}
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
</CardChrome>
|
|
583
|
+
)
|
|
14
584
|
}
|