@fonlan/dsh-model-parameters 0.1.0 → 0.1.3

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.
@@ -0,0 +1,395 @@
1
+ /**
2
+ * The Model Parameters Settings Card: master + per-field toggles, catalog TTL,
3
+ * provider→models.dev mapping overrides, a manual refresh/fill action, the
4
+ * catalog freshness line, and the last fill report (filled / touched /
5
+ * unmatched).
6
+ *
7
+ * The card registers into the `settings.plugin.item` slot keyed by the
8
+ * `model-parameters` settings namespace, so it renders inside
9
+ * 设置 → 插件 → 插件配置, stacked with the built-in plugin cards. The chrome is
10
+ * self-drawn (external plugins cannot import the built-in PluginCard) with
11
+ * styles aligned to it: an expandable header (collapsed by default) and a body
12
+ * that discloses the controls in place.
13
+ *
14
+ * All reads/mutations go through the plugin's fenced API; config persists into
15
+ * the `model-parameters` settings namespace (settings.yaml).
16
+ */
17
+ import { useCallback, useEffect, useState, useSyncExternalStore } from 'react'
18
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
19
+ import type { Translate } from '@deepseek-ai/dsh-client-ui-slots'
20
+ import { LOCALE_NS } from './locales'
21
+ import { api, type PluginState } from './api'
22
+ import type { ModelParametersConfig } from '../shared/config.js'
23
+ import './settings-card.css'
24
+
25
+ function useLocaleRevision(ctx: ClientContext): number {
26
+ const subscribe = useCallback(
27
+ (onChange: () => void) => {
28
+ try {
29
+ return ctx.locale.subscribe(onChange)
30
+ } catch {
31
+ return () => {}
32
+ }
33
+ },
34
+ [ctx],
35
+ )
36
+ const getSnapshot = useCallback(() => {
37
+ try {
38
+ return ctx.locale.getLocale().revision
39
+ } catch {
40
+ return 0
41
+ }
42
+ }, [ctx])
43
+ return useSyncExternalStore(subscribe, getSnapshot)
44
+ }
45
+
46
+ function formatTime(epochMs: number): string {
47
+ try {
48
+ return new Date(epochMs).toLocaleString()
49
+ } catch {
50
+ return String(epochMs)
51
+ }
52
+ }
53
+
54
+ export function makeSettingsCard(ctx: ClientContext): () => JSX.Element {
55
+ const t: Translate = (() => {
56
+ try {
57
+ return ctx.locale.bind(LOCALE_NS) as unknown as Translate
58
+ } catch {
59
+ return (key: string) => key
60
+ }
61
+ })()
62
+
63
+ return function ModelParametersCard(): JSX.Element {
64
+ useLocaleRevision(ctx)
65
+ // Card-local disclosure: collapsed by default, like the built-in plugin cards.
66
+ const [open, setOpen] = useState(false)
67
+ const [state, setState] = useState<PluginState | null>(null)
68
+ const [error, setError] = useState<string | null>(null)
69
+ const [loading, setLoading] = useState(true)
70
+ const [busy, setBusy] = useState(false)
71
+ const [saving, setSaving] = useState(false)
72
+ const [mappingRows, setMappingRows] = useState<Array<{ from: string; to: string }>>([])
73
+ const [newFrom, setNewFrom] = useState('')
74
+ const [newTo, setNewTo] = useState('')
75
+
76
+ const load = useCallback(async () => {
77
+ setLoading(true)
78
+ try {
79
+ const next = await api.state()
80
+ setState(next)
81
+ setMappingRows(Object.entries(next.config.providerMap).map(([from, to]) => ({ from, to })))
82
+ setError(null)
83
+ } catch (cause) {
84
+ setError(cause instanceof Error ? cause.message : String(cause))
85
+ } finally {
86
+ setLoading(false)
87
+ }
88
+ }, [])
89
+
90
+ useEffect(() => {
91
+ void load()
92
+ }, [load])
93
+
94
+ const patchConfig = useCallback(async (patch: Partial<ModelParametersConfig>) => {
95
+ setSaving(true)
96
+ try {
97
+ const next = await api.setConfig(patch)
98
+ setState(next)
99
+ setMappingRows(Object.entries(next.config.providerMap).map(([from, to]) => ({ from, to })))
100
+ setError(null)
101
+ } catch (cause) {
102
+ setError(cause instanceof Error ? cause.message : String(cause))
103
+ } finally {
104
+ setSaving(false)
105
+ }
106
+ }, [])
107
+
108
+ const refresh = useCallback(async () => {
109
+ setBusy(true)
110
+ try {
111
+ setState(await api.refresh())
112
+ setError(null)
113
+ } catch (cause) {
114
+ setError(cause instanceof Error ? cause.message : String(cause))
115
+ } finally {
116
+ setBusy(false)
117
+ }
118
+ }, [])
119
+
120
+ const reconcileNow = useCallback(async () => {
121
+ setBusy(true)
122
+ try {
123
+ setState(await api.reconcile())
124
+ setError(null)
125
+ } catch (cause) {
126
+ setError(cause instanceof Error ? cause.message : String(cause))
127
+ } finally {
128
+ setBusy(false)
129
+ }
130
+ }, [])
131
+
132
+ const applyMappings = useCallback(async () => {
133
+ const providerMap: Record<string, string> = {}
134
+ for (const row of mappingRows) {
135
+ const from = row.from.trim()
136
+ const to = row.to.trim()
137
+ if (from.length > 0 && to.length > 0) providerMap[from] = to
138
+ }
139
+ await patchConfig({ providerMap })
140
+ }, [mappingRows, patchConfig])
141
+
142
+ const title = t('settingsTitle')
143
+
144
+ return (
145
+ <li className="mp-card" data-open={open ? '' : undefined}>
146
+ <button type="button" className="mp-card-head" aria-expanded={open}
147
+ aria-label={(open ? t('collapse') : t('expand')) + ':' + title}
148
+ onClick={() => setOpen(!open)}>
149
+ <span className="mp-card-headText">
150
+ <span className="mp-card-title">{title}</span>
151
+ <span className="mp-card-sub">{t('cardSub')}</span>
152
+ </span>
153
+ <span className="mp-card-chevron" data-open={open ? '' : undefined} aria-hidden="true">
154
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
155
+ <path d="M3.5 5.25L7 8.75L10.5 5.25" stroke="currentColor" strokeWidth="1.5"
156
+ strokeLinecap="round" strokeLinejoin="round" />
157
+ </svg>
158
+ </span>
159
+ </button>
160
+ {open && (
161
+ <div className="mp-card-body">
162
+ {error !== null && state === null ? (
163
+ <p className="mp-error">{t('error')}: {error}</p>
164
+ ) : loading || state === null ? (
165
+ <p className="mp-muted">{t('saving')}</p>
166
+ ) : (
167
+ <ModelParametersCardBody
168
+ t={t}
169
+ state={state}
170
+ error={error}
171
+ saving={saving}
172
+ busy={busy}
173
+ mappingRows={mappingRows}
174
+ newFrom={newFrom}
175
+ newTo={newTo}
176
+ setNewFrom={setNewFrom}
177
+ setNewTo={setNewTo}
178
+ setMappingRows={setMappingRows}
179
+ onPatchConfig={patchConfig}
180
+ onRefresh={refresh}
181
+ onReconcile={reconcileNow}
182
+ onApplyMappings={applyMappings}
183
+ />
184
+ )}
185
+ </div>
186
+ )}
187
+ </li>
188
+ )
189
+ }
190
+ }
191
+
192
+ /** The card body: divider-separated control groups, styled like the built-in cards. */
193
+ function ModelParametersCardBody(props: {
194
+ t: Translate
195
+ state: PluginState
196
+ error: string | null
197
+ saving: boolean
198
+ busy: boolean
199
+ mappingRows: Array<{ from: string; to: string }>
200
+ newFrom: string
201
+ newTo: string
202
+ setNewFrom: (value: string) => void
203
+ setNewTo: (value: string) => void
204
+ setMappingRows: (rows: Array<{ from: string; to: string }>) => void
205
+ onPatchConfig: (patch: Partial<ModelParametersConfig>) => Promise<void>
206
+ onRefresh: () => Promise<void>
207
+ onReconcile: () => Promise<void>
208
+ onApplyMappings: () => Promise<void>
209
+ }): JSX.Element {
210
+ const {
211
+ t, state, error, saving, busy,
212
+ mappingRows, newFrom, newTo,
213
+ setNewFrom, setNewTo, setMappingRows,
214
+ onPatchConfig, onRefresh, onReconcile, onApplyMappings,
215
+ } = props
216
+
217
+ const config = state.config
218
+ const catalog = state.catalog
219
+ const report = state.report
220
+
221
+ return (
222
+ <div className="mp-body">
223
+ <section className="mp-group">
224
+ <h4 className="mp-group-title">{t('fields')}</h4>
225
+ <label className="mp-toggle">
226
+ <input
227
+ type="checkbox"
228
+ checked={config.enabled}
229
+ disabled={saving}
230
+ onChange={(event) => void onPatchConfig({ enabled: event.target.checked })}
231
+ />
232
+ <span className="mp-toggle-body">
233
+ <span className="mp-toggle-label">{t('master')}</span>
234
+ <span className="mp-toggle-desc">{t('masterDesc')}</span>
235
+ </span>
236
+ </label>
237
+ {([
238
+ ['fillName', t('fillName'), t('fillNameDesc')],
239
+ ['fillContext', t('fillContext'), t('fillContextDesc')],
240
+ ['fillMaxTokens', t('fillMaxTokens'), t('fillMaxTokensDesc')],
241
+ ['fillReasoning', t('fillReasoning'), t('fillReasoningDesc')],
242
+ ['fillInput', t('fillInput'), t('fillInputDesc')],
243
+ ] as const).map(([key, label, desc]) => (
244
+ <label className="mp-toggle mp-toggle-sub" key={key}>
245
+ <input
246
+ type="checkbox"
247
+ checked={config[key]}
248
+ disabled={saving || !config.enabled}
249
+ onChange={(event) => void onPatchConfig({ [key]: event.target.checked })}
250
+ />
251
+ <span className="mp-toggle-body">
252
+ <span className="mp-toggle-label">{label}</span>
253
+ <span className="mp-toggle-desc">{desc}</span>
254
+ </span>
255
+ </label>
256
+ ))}
257
+ </section>
258
+
259
+ <hr className="mp-divider" />
260
+
261
+ <section className="mp-group">
262
+ <h4 className="mp-group-title">{t('catalog')}</h4>
263
+ <div className="mp-meta">
264
+ <span>{t('catalogEntries')}: <strong>{catalog.entries.toLocaleString()}</strong></span>
265
+ <span>{t('catalogProviders')}: <strong>{catalog.providers.toLocaleString()}</strong></span>
266
+ <span>
267
+ {t('catalogFetched')}:{' '}
268
+ <strong>{catalog.fetchedAt > 0 ? formatTime(catalog.fetchedAt) : t('catalogNever')}</strong>{' '}
269
+ {catalog.fetchedAt > 0 && (
270
+ <span className={catalog.fresh ? 'mp-badge mp-badge-ok' : 'mp-badge mp-badge-warn'}>
271
+ {catalog.fresh ? t('catalogFresh') : t('catalogStale')}
272
+ </span>
273
+ )}
274
+ </span>
275
+ <label className="mp-ttl">
276
+ {t('ttl')}
277
+ <input
278
+ className="mp-input mp-input-narrow"
279
+ type="number"
280
+ min={1}
281
+ max={90}
282
+ value={config.ttlDays}
283
+ disabled={saving}
284
+ onChange={(event) => {
285
+ const days = Number(event.target.value)
286
+ if (Number.isFinite(days) && days >= 1 && days <= 90) void onPatchConfig({ ttlDays: Math.round(days) })
287
+ }}
288
+ />
289
+ </label>
290
+ </div>
291
+ <div className="mp-actions">
292
+ <button className="mp-button" disabled={busy} onClick={() => void onRefresh()}>
293
+ {busy ? t('refreshBusy') : t('refresh')}
294
+ </button>
295
+ <button className="mp-button mp-button-ghost" disabled={busy} onClick={() => void onReconcile()}>
296
+ {t('reconcileNow')}
297
+ </button>
298
+ </div>
299
+ </section>
300
+
301
+ <hr className="mp-divider" />
302
+
303
+ <section className="mp-group">
304
+ <h4 className="mp-group-title">{t('providerMap')}</h4>
305
+ <p className="mp-desc">{t('providerMapDesc')}</p>
306
+ {mappingRows.map((row, index) => (
307
+ <div className="mp-map-row" key={index}>
308
+ <input
309
+ className="mp-input"
310
+ value={row.from}
311
+ disabled={saving}
312
+ onChange={(event) => {
313
+ const next = [...mappingRows]
314
+ next[index] = { ...row, from: event.target.value }
315
+ setMappingRows(next)
316
+ }}
317
+ />
318
+ <span className="mp-map-arrow">→</span>
319
+ <input
320
+ className="mp-input"
321
+ value={row.to}
322
+ disabled={saving}
323
+ onChange={(event) => {
324
+ const next = [...mappingRows]
325
+ next[index] = { ...row, to: event.target.value }
326
+ setMappingRows(next)
327
+ }}
328
+ />
329
+ <button
330
+ className="mp-button mp-button-ghost"
331
+ disabled={saving}
332
+ onClick={() => setMappingRows(mappingRows.filter((_, i) => i !== index))}
333
+ >
334
+ {t('removeMapping')}
335
+ </button>
336
+ </div>
337
+ ))}
338
+ <div className="mp-map-row">
339
+ <input
340
+ className="mp-input"
341
+ placeholder="dsh provider"
342
+ value={newFrom}
343
+ onChange={(event) => setNewFrom(event.target.value)}
344
+ />
345
+ <span className="mp-map-arrow">→</span>
346
+ <input
347
+ className="mp-input"
348
+ placeholder="models.dev provider"
349
+ value={newTo}
350
+ onChange={(event) => setNewTo(event.target.value)}
351
+ />
352
+ <button
353
+ className="mp-button mp-button-ghost"
354
+ disabled={newFrom.trim().length === 0 || newTo.trim().length === 0}
355
+ onClick={() => {
356
+ setMappingRows([...mappingRows, { from: newFrom.trim(), to: newTo.trim() }])
357
+ setNewFrom('')
358
+ setNewTo('')
359
+ }}
360
+ >
361
+ {t('addMapping')}
362
+ </button>
363
+ </div>
364
+ <div className="mp-actions">
365
+ <button className="mp-button" disabled={saving} onClick={() => void onApplyMappings()}>
366
+ {saving ? t('saving') : '✓ ' + t('providerMap')}
367
+ </button>
368
+ </div>
369
+ </section>
370
+
371
+ <hr className="mp-divider" />
372
+
373
+ <section className="mp-group">
374
+ <h4 className="mp-group-title">{t('report')}</h4>
375
+ {report === null ? (
376
+ <p className="mp-muted">{t('reportNever')}</p>
377
+ ) : (
378
+ <ul className="mp-report">
379
+ <li>{t('reportAt')}: {formatTime(report.at)}</li>
380
+ <li>{t('reportFilled')}: <strong>{report.filled}</strong></li>
381
+ <li>{t('reportTouched')}: <strong>{report.touched}</strong></li>
382
+ <li>
383
+ {t('reportUnmatched')}:{' '}
384
+ {report.unmatched.length === 0
385
+ ? t('reportEmpty')
386
+ : <code className="mp-code">{report.unmatched.join(', ')}</code>}
387
+ </li>
388
+ </ul>
389
+ )}
390
+ </section>
391
+
392
+ {error !== null && <p className="mp-error">{error}</p>}
393
+ </div>
394
+ )
395
+ }
@@ -1,172 +0,0 @@
1
- .mp-root {
2
- display: flex;
3
- flex-direction: column;
4
- gap: 14px;
5
- max-width: 720px;
6
- }
7
-
8
- .mp-desc {
9
- margin: 0;
10
- opacity: 0.85;
11
- font-size: 13px;
12
- line-height: 1.5;
13
- }
14
-
15
- .mp-card {
16
- border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.25));
17
- border-radius: 10px;
18
- padding: 12px 14px;
19
- display: flex;
20
- flex-direction: column;
21
- gap: 10px;
22
- background: var(--dsh-surface-color, transparent);
23
- }
24
-
25
- .mp-card-title {
26
- margin: 0;
27
- font-size: 13px;
28
- font-weight: 600;
29
- opacity: 0.9;
30
- }
31
-
32
- .mp-toggle {
33
- display: flex;
34
- align-items: flex-start;
35
- gap: 10px;
36
- cursor: pointer;
37
- }
38
-
39
- .mp-toggle input[type='checkbox'] {
40
- margin-top: 3px;
41
- accent-color: var(--dsh-accent-color, #4f8cff);
42
- }
43
-
44
- .mp-toggle-sub {
45
- padding-left: 22px;
46
- }
47
-
48
- .mp-toggle-body {
49
- display: flex;
50
- flex-direction: column;
51
- gap: 2px;
52
- }
53
-
54
- .mp-toggle-label {
55
- font-size: 13px;
56
- }
57
-
58
- .mp-toggle-desc {
59
- font-size: 12px;
60
- opacity: 0.65;
61
- }
62
-
63
- .mp-meta {
64
- display: flex;
65
- flex-wrap: wrap;
66
- gap: 6px 16px;
67
- font-size: 13px;
68
- }
69
-
70
- .mp-ttl {
71
- display: inline-flex;
72
- align-items: center;
73
- gap: 6px;
74
- font-size: 13px;
75
- }
76
-
77
- .mp-ttl input {
78
- width: 64px;
79
- padding: 3px 6px;
80
- border-radius: 6px;
81
- border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.35));
82
- background: var(--dsh-input-bg, transparent);
83
- color: inherit;
84
- }
85
-
86
- .mp-actions {
87
- display: flex;
88
- gap: 8px;
89
- flex-wrap: wrap;
90
- }
91
-
92
- .mp-button {
93
- padding: 6px 12px;
94
- border-radius: 8px;
95
- border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.35));
96
- background: var(--dsh-accent-color, #4f8cff);
97
- color: #fff;
98
- font-size: 13px;
99
- cursor: pointer;
100
- }
101
-
102
- .mp-button:disabled {
103
- opacity: 0.5;
104
- cursor: default;
105
- }
106
-
107
- .mp-button-ghost {
108
- background: transparent;
109
- color: inherit;
110
- }
111
-
112
- .mp-map-row {
113
- display: flex;
114
- align-items: center;
115
- gap: 6px;
116
- }
117
-
118
- .mp-map-row input {
119
- flex: 1;
120
- padding: 4px 8px;
121
- border-radius: 6px;
122
- border: 1px solid var(--dsh-border-color, rgba(128, 128, 128, 0.35));
123
- background: var(--dsh-input-bg, transparent);
124
- color: inherit;
125
- font-size: 13px;
126
- }
127
-
128
- .mp-map-arrow {
129
- opacity: 0.6;
130
- }
131
-
132
- .mp-report {
133
- margin: 0;
134
- padding-left: 18px;
135
- font-size: 13px;
136
- line-height: 1.7;
137
- }
138
-
139
- .mp-code {
140
- background: var(--dsh-code-bg, rgba(128, 128, 128, 0.15));
141
- border-radius: 4px;
142
- padding: 1px 5px;
143
- font-size: 12px;
144
- word-break: break-all;
145
- }
146
-
147
- .mp-badge {
148
- font-size: 11px;
149
- padding: 1px 7px;
150
- border-radius: 999px;
151
- margin-left: 4px;
152
- }
153
-
154
- .mp-badge-ok {
155
- background: rgba(52, 199, 89, 0.18);
156
- color: #34c759;
157
- }
158
-
159
- .mp-badge-warn {
160
- background: rgba(255, 149, 0, 0.18);
161
- color: #ff9500;
162
- }
163
-
164
- .mp-muted {
165
- font-size: 13px;
166
- opacity: 0.6;
167
- }
168
-
169
- .mp-error {
170
- font-size: 13px;
171
- color: #ff3b30;
172
- }