@goodandready/dsh-moa 0.2.40 → 0.2.42
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/README.md +7 -0
- package/README.ru.md +7 -0
- package/README.zh.md +11 -0
- package/lib/client.js +407 -98
- package/lib/history.js +169 -35
- package/lib/index.js +21 -3
- package/lib/live-canvas.js +39 -6
- package/lib/moa-multi-judge.js +151 -59
- package/lib/moa-report.js +41 -20
- package/lib/moa-runner.js +11 -6
- package/lib/moa-stream.js +57 -1
- package/lib/pricing.js +10 -6
- package/lib/routes.js +3 -2
- package/lib/updater.js +23 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -142,6 +142,64 @@ window.__ModuleLoader__.load({
|
|
|
142
142
|
'diff.curator': 'Curator Synthesis',
|
|
143
143
|
'diff.candidate': 'Candidate {num}',
|
|
144
144
|
'diff.empty': 'Selected files are identical (zero diff).',
|
|
145
|
+
// Multi-Judge
|
|
146
|
+
'aggregator.multi_judge_title': 'Multi-Judge Panel & Consensus Voting',
|
|
147
|
+
'aggregator.multi_judge_desc': 'Deploy multiple judge models to score candidates and determine winner via consensus voting.',
|
|
148
|
+
'aggregator.voting_strategy_label': 'Voting Strategy:',
|
|
149
|
+
'aggregator.voting_majority': 'Majority Vote',
|
|
150
|
+
'aggregator.voting_highest_score': 'Highest Score',
|
|
151
|
+
'aggregator.voting_unanimous': 'Unanimous',
|
|
152
|
+
'aggregator.judge_models_title': 'Judge Panel Models',
|
|
153
|
+
'aggregator.judge_models_desc': 'Specify 2 or more judge models for panel consensus.',
|
|
154
|
+
'aggregator.add_judge_btn': '+ Add Judge Model',
|
|
155
|
+
'aggregator.judge_badge': 'Judge #{num}',
|
|
156
|
+
'aggregator.judge_models_empty': 'At least 2 judge models are required for consensus voting.',
|
|
157
|
+
|
|
158
|
+
// Composite
|
|
159
|
+
'aggregator.composite_title': 'Composite Hybrid Synthesis (AST / Block Merge)',
|
|
160
|
+
'aggregator.composite_desc': 'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.',
|
|
161
|
+
|
|
162
|
+
// Budget
|
|
163
|
+
'aggregator.budget_title': 'Cost Budget Guardrails & Auto-Fallback',
|
|
164
|
+
'aggregator.budget_desc': 'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.',
|
|
165
|
+
'aggregator.budget_max_usd': 'Max USD ($):',
|
|
166
|
+
'aggregator.budget_action': 'Action:',
|
|
167
|
+
'aggregator.budget_action_trim': 'Trim Pool',
|
|
168
|
+
'aggregator.budget_action_abort': 'Abort',
|
|
169
|
+
|
|
170
|
+
// Reports
|
|
171
|
+
'aggregator.report_title': 'Automated Benchmark & Post-Mortem Report',
|
|
172
|
+
'aggregator.report_desc': 'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.',
|
|
173
|
+
|
|
174
|
+
// Local Fallback
|
|
175
|
+
'aggregator.local_fallback_title': 'Graceful Degradation & Local Fallback',
|
|
176
|
+
'aggregator.local_fallback_desc': 'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.',
|
|
177
|
+
'aggregator.local_models_title': 'Local Fallback Models',
|
|
178
|
+
'aggregator.local_models_desc': 'Local offline models used when cloud candidate APIs fail.',
|
|
179
|
+
'aggregator.add_local_btn': '+ Add Local Model',
|
|
180
|
+
'aggregator.local_badge': 'Local #{num}',
|
|
181
|
+
'aggregator.local_models_empty': 'Configure at least one local fallback model (e.g. Ollama).',
|
|
182
|
+
|
|
183
|
+
// Smart Routing Model
|
|
184
|
+
'config.smart_routing_title': 'Smart Prompt Routing',
|
|
185
|
+
'config.smart_routing_desc': 'Analyze task complexity and automatically select the optimal preset.',
|
|
186
|
+
'config.smart_routing_model': 'Routing Model:',
|
|
187
|
+
|
|
188
|
+
// Multi-Turn Context
|
|
189
|
+
'aggregator.multi_turn_title': 'Multi-Turn Context & Diff Pruning',
|
|
190
|
+
'aggregator.multi_turn_desc': 'Retain conversation context across turns while pruning duplicate code blocks.',
|
|
191
|
+
|
|
192
|
+
// UI States & Errors
|
|
193
|
+
'status.loading': 'Loading MoA settings…',
|
|
194
|
+
'status.unavailable': 'MoA service temporarily unavailable',
|
|
195
|
+
'status.retry': 'Retry',
|
|
196
|
+
'status.error_saving': 'Error saving: ',
|
|
197
|
+
'error_boundary.title': '⚠️ MoA UI Error:',
|
|
198
|
+
'error_boundary.retry': 'Retry',
|
|
199
|
+
|
|
200
|
+
// Diff Viewer
|
|
201
|
+
'diff.select_run': 'Run:',
|
|
202
|
+
'diff.latest_run': 'Latest / Active Workspace',
|
|
145
203
|
'updater.success': 'Successfully updated to v{version}. Restart DSH to apply.',
|
|
146
204
|
}
|
|
147
205
|
|
|
@@ -260,6 +318,64 @@ window.__ModuleLoader__.load({
|
|
|
260
318
|
'diff.curator': '裁判合成 (Curator Synthesis)',
|
|
261
319
|
'diff.candidate': '候选方案 {num}',
|
|
262
320
|
'diff.empty': '选中的文件完全一致(无差异)。',
|
|
321
|
+
// Multi-Judge
|
|
322
|
+
'aggregator.multi_judge_title': '多裁判委员会与共识投票',
|
|
323
|
+
'aggregator.multi_judge_desc': '部署多个裁判模型对候选方案打分并通过共识投票评定获胜者。',
|
|
324
|
+
'aggregator.voting_strategy_label': '投票策略:',
|
|
325
|
+
'aggregator.voting_majority': '多数决',
|
|
326
|
+
'aggregator.voting_highest_score': '最高分',
|
|
327
|
+
'aggregator.voting_unanimous': '全票通过',
|
|
328
|
+
'aggregator.judge_models_title': '裁判模型列表',
|
|
329
|
+
'aggregator.judge_models_desc': '指定 2 个或更多裁判模型以实现委员会共识。',
|
|
330
|
+
'aggregator.add_judge_btn': '+ 添加裁判模型',
|
|
331
|
+
'aggregator.judge_badge': '裁判 #{num}',
|
|
332
|
+
'aggregator.judge_models_empty': '共识投票至少需要配置 2 个裁判模型。',
|
|
333
|
+
|
|
334
|
+
// Composite
|
|
335
|
+
'aggregator.composite_title': '混合复合综合 (AST / 代码块合并)',
|
|
336
|
+
'aggregator.composite_desc': '从多个候选方案中提取模块化代码块、类型定义与算法并综合为统一方案。',
|
|
337
|
+
|
|
338
|
+
// Budget
|
|
339
|
+
'aggregator.budget_title': '成本预算护栏与自动分流',
|
|
340
|
+
'aggregator.budget_desc': '限制单次最高花费;自动削减候选模型或中止以防止超额消耗。',
|
|
341
|
+
'aggregator.budget_max_usd': '最高金额 ($):',
|
|
342
|
+
'aggregator.budget_action': '处置方式:',
|
|
343
|
+
'aggregator.budget_action_trim': '削减候选',
|
|
344
|
+
'aggregator.budget_action_abort': '中止运行',
|
|
345
|
+
|
|
346
|
+
// Reports
|
|
347
|
+
'aggregator.report_title': '自动化基准与复盘报告',
|
|
348
|
+
'aggregator.report_desc': '生成可下载的 Markdown 与 JSON 报告,详细记录候选方案耗时、一致性、测试门禁与成本分布。',
|
|
349
|
+
|
|
350
|
+
// Local Fallback
|
|
351
|
+
'aggregator.local_fallback_title': '优雅降级与本地备用模型',
|
|
352
|
+
'aggregator.local_fallback_desc': '在线候选模型 API 故障时自动降级到本地 Ollama / MiniPC 模型。',
|
|
353
|
+
'aggregator.local_models_title': '本地备用模型列表',
|
|
354
|
+
'aggregator.local_models_desc': '云端候选 API 异常时调用的本地离线模型。',
|
|
355
|
+
'aggregator.add_local_btn': '+ 添加本地模型',
|
|
356
|
+
'aggregator.local_badge': '本地 #{num}',
|
|
357
|
+
'aggregator.local_models_empty': '请至少配置一个本地备用模型(例如 Ollama)。',
|
|
358
|
+
|
|
359
|
+
// Smart Routing Model
|
|
360
|
+
'config.smart_routing_title': '智能提示词分流',
|
|
361
|
+
'config.smart_routing_desc': '分析任务复杂度并自动匹配最佳预设。',
|
|
362
|
+
'config.smart_routing_model': '分流决策模型:',
|
|
363
|
+
|
|
364
|
+
// Multi-Turn Context
|
|
365
|
+
'aggregator.multi_turn_title': '多轮对话上下文与差异剪枝',
|
|
366
|
+
'aggregator.multi_turn_desc': '跨轮次保留对话上下文并剪枝重复代码块。',
|
|
367
|
+
|
|
368
|
+
// UI States & Errors
|
|
369
|
+
'status.loading': '正在加载 MoA 设置…',
|
|
370
|
+
'status.unavailable': 'MoA 服务暂不可用',
|
|
371
|
+
'status.retry': '重试',
|
|
372
|
+
'status.error_saving': '保存失败:',
|
|
373
|
+
'error_boundary.title': '⚠️ MoA 界面错误:',
|
|
374
|
+
'error_boundary.retry': '重试',
|
|
375
|
+
|
|
376
|
+
// Diff Viewer
|
|
377
|
+
'diff.select_run': '运行记录:',
|
|
378
|
+
'diff.latest_run': '最新 / 当前工作区',
|
|
263
379
|
'updater.success': '成功更新至 v{version},请重启 DSH 使其生效。',
|
|
264
380
|
}
|
|
265
381
|
|
|
@@ -307,6 +423,55 @@ window.__ModuleLoader__.load({
|
|
|
307
423
|
Chevron = FallbackChevron
|
|
308
424
|
}
|
|
309
425
|
|
|
426
|
+
|
|
427
|
+
function renderSlotEditorList({ items, badgePrefix, onUpdate, onAdd, onRemove, addLabel, emptyWarning, availableModels, t }) {
|
|
428
|
+
return React.createElement(
|
|
429
|
+
'div',
|
|
430
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 } },
|
|
431
|
+
(Array.isArray(items) ? items : []).map((item, idx) =>
|
|
432
|
+
React.createElement(
|
|
433
|
+
'div',
|
|
434
|
+
{ key: idx, style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
435
|
+
React.createElement('span', { className: 'moa-cand-badge' }, `${badgePrefix} #${idx + 1}`),
|
|
436
|
+
React.createElement(
|
|
437
|
+
'div',
|
|
438
|
+
{ style: { flex: 1 } },
|
|
439
|
+
React.createElement(SearchableModelPicker, {
|
|
440
|
+
provider: (item && item.provider) || '',
|
|
441
|
+
model: (item && item.model) || '',
|
|
442
|
+
onChange: (prov, mod) => onUpdate(idx, prov, mod),
|
|
443
|
+
availableModels,
|
|
444
|
+
t,
|
|
445
|
+
})
|
|
446
|
+
),
|
|
447
|
+
React.createElement(
|
|
448
|
+
'button',
|
|
449
|
+
{
|
|
450
|
+
type: 'button',
|
|
451
|
+
className: 'moa-btn moa-btn-danger moa-btn-sub',
|
|
452
|
+
onClick: () => onRemove(idx),
|
|
453
|
+
},
|
|
454
|
+
'✕'
|
|
455
|
+
)
|
|
456
|
+
)
|
|
457
|
+
),
|
|
458
|
+
React.createElement(
|
|
459
|
+
'button',
|
|
460
|
+
{
|
|
461
|
+
type: 'button',
|
|
462
|
+
className: 'moa-btn moa-btn-sub',
|
|
463
|
+
style: { marginTop: 4, alignSelf: 'flex-start' },
|
|
464
|
+
onClick: onAdd,
|
|
465
|
+
},
|
|
466
|
+
addLabel
|
|
467
|
+
),
|
|
468
|
+
emptyWarning ? React.createElement(
|
|
469
|
+
'div',
|
|
470
|
+
{ style: { fontSize: 11, color: 'var(--dsw-alias-state-warning-primary, #e6a23c)', marginTop: 2 } },
|
|
471
|
+
emptyWarning
|
|
472
|
+
) : null
|
|
473
|
+
)
|
|
474
|
+
}
|
|
310
475
|
const STYLES = `
|
|
311
476
|
/* dsh-moa modern clinebot-aligned styling */
|
|
312
477
|
.moa-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}
|
|
@@ -635,8 +800,25 @@ window.__ModuleLoader__.load({
|
|
|
635
800
|
const [loading, setLoading] = React.useState(false)
|
|
636
801
|
const [error, setError] = React.useState(null)
|
|
637
802
|
const [hasFetched, setHasFetched] = React.useState(false)
|
|
803
|
+
const [historyRuns, setHistoryRuns] = React.useState([])
|
|
804
|
+
const [selectedRunId, setSelectedRunId] = React.useState('')
|
|
805
|
+
|
|
806
|
+
React.useEffect(() => {
|
|
807
|
+
fetch('/dsh-moa/history?limit=30', { cache: 'no-store' })
|
|
808
|
+
.then((r) => r.json())
|
|
809
|
+
.then((data) => {
|
|
810
|
+
if (data && data.ok && Array.isArray(data.runs)) {
|
|
811
|
+
setHistoryRuns(data.runs)
|
|
812
|
+
}
|
|
813
|
+
})
|
|
814
|
+
.catch(() => {})
|
|
815
|
+
}, [])
|
|
816
|
+
|
|
817
|
+
const selectedRun = historyRuns.find((r) => r.id === selectedRunId)
|
|
818
|
+
const candidateCount = selectedRun && Array.isArray(selectedRun.candidates) && selectedRun.candidates.length > 0
|
|
819
|
+
? selectedRun.candidates.length
|
|
820
|
+
: Math.max(currentRefs.length || 0, 2)
|
|
638
821
|
|
|
639
|
-
const candidateCount = Math.max(currentRefs.length || 0, 2)
|
|
640
822
|
const candidateOptions = []
|
|
641
823
|
for (let i = 1; i <= candidateCount; i++) {
|
|
642
824
|
candidateOptions.push({
|
|
@@ -649,6 +831,7 @@ window.__ModuleLoader__.load({
|
|
|
649
831
|
setLoading(true)
|
|
650
832
|
setError(null)
|
|
651
833
|
const params = new URLSearchParams()
|
|
834
|
+
if (selectedRunId) params.set('runId', selectedRunId)
|
|
652
835
|
if (fromCandidate) params.set('from', fromCandidate)
|
|
653
836
|
if (toCandidate) params.set('to', toCandidate)
|
|
654
837
|
if (targetFile) params.set('file', targetFile)
|
|
@@ -679,11 +862,11 @@ window.__ModuleLoader__.load({
|
|
|
679
862
|
setHasFetched(true)
|
|
680
863
|
setError(err.message || String(err))
|
|
681
864
|
})
|
|
682
|
-
}, [fromCandidate, toCandidate, selectedFile])
|
|
865
|
+
}, [fromCandidate, toCandidate, selectedFile, selectedRunId])
|
|
683
866
|
|
|
684
867
|
React.useEffect(() => {
|
|
685
868
|
loadDiff()
|
|
686
|
-
}, [fromCandidate, toCandidate])
|
|
869
|
+
}, [fromCandidate, toCandidate, selectedRunId])
|
|
687
870
|
|
|
688
871
|
const handleFileChange = (e) => {
|
|
689
872
|
const nextFile = e.target.value
|
|
@@ -707,6 +890,25 @@ window.__ModuleLoader__.load({
|
|
|
707
890
|
React.createElement(
|
|
708
891
|
'div',
|
|
709
892
|
{ className: 'moa-diff-controls' },
|
|
893
|
+
/* Run selector dropdown (#179) */
|
|
894
|
+
React.createElement(
|
|
895
|
+
'label',
|
|
896
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
|
|
897
|
+
t('diff.select_run') || 'Run:',
|
|
898
|
+
React.createElement(
|
|
899
|
+
'select',
|
|
900
|
+
{
|
|
901
|
+
className: 'moa-diff-select',
|
|
902
|
+
value: selectedRunId,
|
|
903
|
+
onChange: (e) => setSelectedRunId(e.target.value),
|
|
904
|
+
},
|
|
905
|
+
React.createElement('option', { value: '' }, t('diff.latest_run') || 'Latest / Active Workspace'),
|
|
906
|
+
historyRuns.map((r) => {
|
|
907
|
+
const label = `${r.preset || 'default'} [${(r.id || '').slice(0, 8)}] ${(r.prompt || '').slice(0, 24)}...`
|
|
908
|
+
return React.createElement('option', { key: r.id, value: r.id }, label)
|
|
909
|
+
})
|
|
910
|
+
)
|
|
911
|
+
),
|
|
710
912
|
React.createElement(
|
|
711
913
|
'label',
|
|
712
914
|
{ style: { display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
|
|
@@ -1331,7 +1533,7 @@ window.__ModuleLoader__.load({
|
|
|
1331
1533
|
})
|
|
1332
1534
|
)
|
|
1333
1535
|
),
|
|
1334
|
-
/* Multi-Judge Panel & Consensus Voting */
|
|
1536
|
+
/* Multi-Judge Panel & Consensus Voting (#175, #176) */
|
|
1335
1537
|
React.createElement(
|
|
1336
1538
|
'div',
|
|
1337
1539
|
{ style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
|
|
@@ -1344,29 +1546,62 @@ window.__ModuleLoader__.load({
|
|
|
1344
1546
|
onChange: (e) => updateCurrentPreset((p) => ({ ...p, multi_judge_enabled: e.target.checked })),
|
|
1345
1547
|
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1346
1548
|
}),
|
|
1347
|
-
'Multi-Judge Panel & Consensus Voting'
|
|
1549
|
+
t('aggregator.multi_judge_title') || 'Multi-Judge Panel & Consensus Voting'
|
|
1348
1550
|
),
|
|
1349
1551
|
React.createElement(
|
|
1350
1552
|
'div',
|
|
1351
1553
|
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
1352
|
-
'Deploy multiple judge models to score candidates and determine winner via consensus voting.'
|
|
1554
|
+
t('aggregator.multi_judge_desc') || 'Deploy multiple judge models to score candidates and determine winner via consensus voting.'
|
|
1353
1555
|
),
|
|
1354
1556
|
currentPreset.multi_judge_enabled && React.createElement(
|
|
1355
1557
|
'div',
|
|
1356
|
-
{ style: { marginLeft: 22, display: 'flex',
|
|
1357
|
-
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Voting Strategy:'),
|
|
1558
|
+
{ style: { marginLeft: 22, display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 } },
|
|
1358
1559
|
React.createElement(
|
|
1359
|
-
'
|
|
1360
|
-
{
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1560
|
+
'div',
|
|
1561
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
1562
|
+
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.voting_strategy_label') || 'Voting Strategy:'),
|
|
1563
|
+
React.createElement(
|
|
1564
|
+
'select',
|
|
1565
|
+
{
|
|
1566
|
+
className: 'moa-select',
|
|
1567
|
+
style: { width: 140, height: 28, fontSize: 12, padding: '0 6px' },
|
|
1568
|
+
value: currentPreset.judge_voting_strategy || 'majority',
|
|
1569
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, judge_voting_strategy: e.target.value })),
|
|
1570
|
+
},
|
|
1571
|
+
React.createElement('option', { value: 'majority' }, t('aggregator.voting_majority') || 'Majority Vote'),
|
|
1572
|
+
React.createElement('option', { value: 'highest_score' }, t('aggregator.voting_highest_score') || 'Highest Score'),
|
|
1573
|
+
React.createElement('option', { value: 'unanimous' }, t('aggregator.voting_unanimous') || 'Unanimous')
|
|
1574
|
+
)
|
|
1575
|
+
),
|
|
1576
|
+
renderSlotEditorList({
|
|
1577
|
+
items: currentPreset.judge_models,
|
|
1578
|
+
badgePrefix: 'Judge',
|
|
1579
|
+
onUpdate: (idx, prov, mod) => {
|
|
1580
|
+
updateCurrentPreset((p) => {
|
|
1581
|
+
const list = [...(p.judge_models || [])]
|
|
1582
|
+
list[idx] = { provider: prov, model: mod }
|
|
1583
|
+
return { ...p, judge_models: list }
|
|
1584
|
+
})
|
|
1365
1585
|
},
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1586
|
+
onRemove: (idx) => {
|
|
1587
|
+
updateCurrentPreset((p) => ({
|
|
1588
|
+
...p,
|
|
1589
|
+
judge_models: (p.judge_models || []).filter((_, i) => i !== idx),
|
|
1590
|
+
}))
|
|
1591
|
+
},
|
|
1592
|
+
onAdd: () => {
|
|
1593
|
+
updateCurrentPreset((p) => ({
|
|
1594
|
+
...p,
|
|
1595
|
+
judge_models: [...(p.judge_models || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
|
|
1596
|
+
}))
|
|
1597
|
+
},
|
|
1598
|
+
addLabel: t('aggregator.add_judge_btn') || '+ Add Judge Model',
|
|
1599
|
+
emptyWarning: (!currentPreset.judge_models || currentPreset.judge_models.length < 2)
|
|
1600
|
+
? (t('aggregator.judge_models_empty') || 'At least 2 judge models are required for consensus voting.')
|
|
1601
|
+
: null,
|
|
1602
|
+
availableModels,
|
|
1603
|
+
t,
|
|
1604
|
+
})
|
|
1370
1605
|
)
|
|
1371
1606
|
),
|
|
1372
1607
|
|
|
@@ -1383,12 +1618,12 @@ window.__ModuleLoader__.load({
|
|
|
1383
1618
|
onChange: (e) => updateCurrentPreset((p) => ({ ...p, composite_merge_enabled: e.target.checked })),
|
|
1384
1619
|
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1385
1620
|
}),
|
|
1386
|
-
'Composite Hybrid Synthesis (AST / Block Merge)'
|
|
1621
|
+
t('aggregator.composite_title') || 'Composite Hybrid Synthesis (AST / Block Merge)'
|
|
1387
1622
|
),
|
|
1388
1623
|
React.createElement(
|
|
1389
1624
|
'div',
|
|
1390
1625
|
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
1391
|
-
'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.'
|
|
1626
|
+
t('aggregator.composite_desc') || 'Synthesize modular code blocks, types, and algorithms from multiple candidates into a unified composite.'
|
|
1392
1627
|
)
|
|
1393
1628
|
),
|
|
1394
1629
|
|
|
@@ -1405,12 +1640,12 @@ window.__ModuleLoader__.load({
|
|
|
1405
1640
|
onChange: (e) => updateCurrentPreset((p) => ({ ...p, budget_guard_enabled: e.target.checked })),
|
|
1406
1641
|
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1407
1642
|
}),
|
|
1408
|
-
'Cost Budget Guardrails & Auto-Fallback'
|
|
1643
|
+
t('aggregator.budget_title') || 'Cost Budget Guardrails & Auto-Fallback'
|
|
1409
1644
|
),
|
|
1410
1645
|
React.createElement(
|
|
1411
1646
|
'div',
|
|
1412
1647
|
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
1413
|
-
'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.'
|
|
1648
|
+
t('aggregator.budget_desc') || 'Cap maximum run cost; auto-trim candidate pool or abort to prevent accidental token spend.'
|
|
1414
1649
|
),
|
|
1415
1650
|
currentPreset.budget_guard_enabled && React.createElement(
|
|
1416
1651
|
'div',
|
|
@@ -1418,7 +1653,7 @@ window.__ModuleLoader__.load({
|
|
|
1418
1653
|
React.createElement(
|
|
1419
1654
|
'div',
|
|
1420
1655
|
{ style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
1421
|
-
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Max USD ($):'),
|
|
1656
|
+
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.budget_max_usd') || 'Max USD ($):'),
|
|
1422
1657
|
React.createElement('input', {
|
|
1423
1658
|
type: 'number',
|
|
1424
1659
|
step: '0.01',
|
|
@@ -1434,7 +1669,7 @@ window.__ModuleLoader__.load({
|
|
|
1434
1669
|
React.createElement(
|
|
1435
1670
|
'div',
|
|
1436
1671
|
{ style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
1437
|
-
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'Action:'),
|
|
1672
|
+
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('aggregator.budget_action') || 'Action:'),
|
|
1438
1673
|
React.createElement(
|
|
1439
1674
|
'select',
|
|
1440
1675
|
{
|
|
@@ -1443,8 +1678,8 @@ window.__ModuleLoader__.load({
|
|
|
1443
1678
|
value: currentPreset.budget_action || 'trim',
|
|
1444
1679
|
onChange: (e) => updateCurrentPreset((p) => ({ ...p, budget_action: e.target.value })),
|
|
1445
1680
|
},
|
|
1446
|
-
React.createElement('option', { value: 'trim' }, 'Trim Pool'),
|
|
1447
|
-
React.createElement('option', { value: 'abort' }, 'Abort')
|
|
1681
|
+
React.createElement('option', { value: 'trim' }, t('aggregator.budget_action_trim') || 'Trim Pool'),
|
|
1682
|
+
React.createElement('option', { value: 'abort' }, t('aggregator.budget_action_abort') || 'Abort')
|
|
1448
1683
|
)
|
|
1449
1684
|
)
|
|
1450
1685
|
)
|
|
@@ -1463,16 +1698,16 @@ window.__ModuleLoader__.load({
|
|
|
1463
1698
|
onChange: (e) => updateCurrentPreset((p) => ({ ...p, report_generation_enabled: e.target.checked })),
|
|
1464
1699
|
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1465
1700
|
}),
|
|
1466
|
-
'Automated Benchmark & Post-Mortem Report'
|
|
1701
|
+
t('aggregator.report_title') || 'Automated Benchmark & Post-Mortem Report'
|
|
1467
1702
|
),
|
|
1468
1703
|
React.createElement(
|
|
1469
1704
|
'div',
|
|
1470
1705
|
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
1471
|
-
'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.'
|
|
1706
|
+
t('aggregator.report_desc') || 'Generate downloadable Markdown & JSON reports detailing candidate latency, agreement, test gate, and cost breakdown.'
|
|
1472
1707
|
)
|
|
1473
1708
|
),
|
|
1474
1709
|
|
|
1475
|
-
/* Graceful Degradation & Local Fallback */
|
|
1710
|
+
/* Graceful Degradation & Local Fallback (#175, #176) */
|
|
1476
1711
|
React.createElement(
|
|
1477
1712
|
'div',
|
|
1478
1713
|
{ style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
|
|
@@ -1485,86 +1720,154 @@ window.__ModuleLoader__.load({
|
|
|
1485
1720
|
onChange: (e) => updateCurrentPreset((p) => ({ ...p, local_fallback_enabled: e.target.checked })),
|
|
1486
1721
|
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1487
1722
|
}),
|
|
1488
|
-
'Graceful Degradation & Local Fallback'
|
|
1723
|
+
t('aggregator.local_fallback_title') || 'Graceful Degradation & Local Fallback'
|
|
1489
1724
|
),
|
|
1490
1725
|
React.createElement(
|
|
1491
1726
|
'div',
|
|
1492
1727
|
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
1493
|
-
'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.'
|
|
1728
|
+
t('aggregator.local_fallback_desc') || 'Automatically fall back to local Ollama / MiniPC models when online candidate APIs fail.'
|
|
1729
|
+
),
|
|
1730
|
+
currentPreset.local_fallback_enabled && React.createElement(
|
|
1731
|
+
'div',
|
|
1732
|
+
{ style: { marginLeft: 22, marginTop: 4 } },
|
|
1733
|
+
renderSlotEditorList({
|
|
1734
|
+
items: currentPreset.local_fallback_models,
|
|
1735
|
+
badgePrefix: 'Local',
|
|
1736
|
+
onUpdate: (idx, prov, mod) => {
|
|
1737
|
+
updateCurrentPreset((p) => {
|
|
1738
|
+
const list = [...(p.local_fallback_models || [])]
|
|
1739
|
+
list[idx] = { provider: prov, model: mod }
|
|
1740
|
+
return { ...p, local_fallback_models: list }
|
|
1741
|
+
})
|
|
1742
|
+
},
|
|
1743
|
+
onRemove: (idx) => {
|
|
1744
|
+
updateCurrentPreset((p) => ({
|
|
1745
|
+
...p,
|
|
1746
|
+
local_fallback_models: (p.local_fallback_models || []).filter((_, i) => i !== idx),
|
|
1747
|
+
}))
|
|
1748
|
+
},
|
|
1749
|
+
onAdd: () => {
|
|
1750
|
+
updateCurrentPreset((p) => ({
|
|
1751
|
+
...p,
|
|
1752
|
+
local_fallback_models: [...(p.local_fallback_models || []), { provider: 'ollama', model: '' }],
|
|
1753
|
+
}))
|
|
1754
|
+
},
|
|
1755
|
+
addLabel: t('aggregator.add_local_btn') || '+ Add Local Model',
|
|
1756
|
+
emptyWarning: (!currentPreset.local_fallback_models || currentPreset.local_fallback_models.length === 0)
|
|
1757
|
+
? (t('aggregator.local_models_empty') || 'Configure at least one local fallback model (e.g. Ollama).')
|
|
1758
|
+
: null,
|
|
1759
|
+
availableModels,
|
|
1760
|
+
t,
|
|
1761
|
+
})
|
|
1494
1762
|
)
|
|
1495
1763
|
),
|
|
1496
1764
|
|
|
1497
|
-
/*
|
|
1765
|
+
/* Smart Prompt Routing (#176) */
|
|
1498
1766
|
React.createElement(
|
|
1499
1767
|
'div',
|
|
1500
|
-
{ style: {
|
|
1768
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
|
|
1501
1769
|
React.createElement(
|
|
1502
|
-
'
|
|
1503
|
-
{ style: {
|
|
1504
|
-
|
|
1770
|
+
'label',
|
|
1771
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
|
|
1772
|
+
React.createElement('input', {
|
|
1773
|
+
type: 'checkbox',
|
|
1774
|
+
checked: Boolean(currentPreset.smart_routing_enabled),
|
|
1775
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, smart_routing_enabled: e.target.checked })),
|
|
1776
|
+
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1777
|
+
}),
|
|
1778
|
+
t('config.smart_routing_title') || 'Smart Prompt Routing'
|
|
1505
1779
|
),
|
|
1506
1780
|
React.createElement(
|
|
1507
1781
|
'div',
|
|
1508
|
-
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)',
|
|
1509
|
-
t('
|
|
1510
|
-
),
|
|
1511
|
-
(Array.isArray(currentPreset.aggregator_fallbacks) ? currentPreset.aggregator_fallbacks : []).map((fb, fbIdx) =>
|
|
1512
|
-
React.createElement(
|
|
1513
|
-
'div',
|
|
1514
|
-
{ key: fbIdx, style: { display: 'flex', alignItems: 'center', gap: 8 } },
|
|
1515
|
-
React.createElement('span', { className: 'moa-cand-badge' }, `Fallback #${fbIdx + 1}`),
|
|
1516
|
-
React.createElement(
|
|
1517
|
-
'div',
|
|
1518
|
-
{ style: { flex: 1 } },
|
|
1519
|
-
React.createElement(SearchableModelPicker, {
|
|
1520
|
-
provider: (fb && fb.provider) || '',
|
|
1521
|
-
model: (fb && fb.model) || '',
|
|
1522
|
-
onChange: (prov, mod) => {
|
|
1523
|
-
updateCurrentPreset((p) => {
|
|
1524
|
-
const list = [...(p.aggregator_fallbacks || [])]
|
|
1525
|
-
list[fbIdx] = { provider: prov, model: mod }
|
|
1526
|
-
return { ...p, aggregator_fallbacks: list }
|
|
1527
|
-
})
|
|
1528
|
-
},
|
|
1529
|
-
availableModels,
|
|
1530
|
-
t,
|
|
1531
|
-
})
|
|
1532
|
-
),
|
|
1533
|
-
React.createElement(
|
|
1534
|
-
'button',
|
|
1535
|
-
{
|
|
1536
|
-
type: 'button',
|
|
1537
|
-
className: 'moa-btn moa-btn-danger moa-btn-sub',
|
|
1538
|
-
onClick: () => {
|
|
1539
|
-
updateCurrentPreset((p) => ({
|
|
1540
|
-
...p,
|
|
1541
|
-
aggregator_fallbacks: (p.aggregator_fallbacks || []).filter((_, i) => i !== fbIdx),
|
|
1542
|
-
}))
|
|
1543
|
-
},
|
|
1544
|
-
},
|
|
1545
|
-
'✕'
|
|
1546
|
-
)
|
|
1547
|
-
)
|
|
1782
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
1783
|
+
t('config.smart_routing_desc') || 'Analyze task complexity and automatically select the optimal preset.'
|
|
1548
1784
|
),
|
|
1549
|
-
React.createElement(
|
|
1785
|
+
currentPreset.smart_routing_enabled && React.createElement(
|
|
1550
1786
|
'div',
|
|
1551
|
-
|
|
1787
|
+
{ style: { marginLeft: 22, marginTop: 4, display: 'flex', alignItems: 'center', gap: 8 } },
|
|
1788
|
+
React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('config.smart_routing_model') || 'Routing Model:'),
|
|
1552
1789
|
React.createElement(
|
|
1553
|
-
'
|
|
1554
|
-
{
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1790
|
+
'div',
|
|
1791
|
+
{ style: { flex: 1, maxWidth: 320 } },
|
|
1792
|
+
React.createElement(SearchableModelPicker, {
|
|
1793
|
+
provider: currentPreset.smart_routing_model?.provider || '',
|
|
1794
|
+
model: currentPreset.smart_routing_model?.model || '',
|
|
1795
|
+
onChange: (prov, mod) => {
|
|
1559
1796
|
updateCurrentPreset((p) => ({
|
|
1560
1797
|
...p,
|
|
1561
|
-
|
|
1798
|
+
smart_routing_model: { provider: prov, model: mod },
|
|
1562
1799
|
}))
|
|
1563
1800
|
},
|
|
1564
|
-
|
|
1565
|
-
|
|
1801
|
+
availableModels,
|
|
1802
|
+
t,
|
|
1803
|
+
})
|
|
1566
1804
|
)
|
|
1567
1805
|
)
|
|
1806
|
+
),
|
|
1807
|
+
|
|
1808
|
+
/* Multi-Turn Context & Diff Pruning (#176) */
|
|
1809
|
+
React.createElement(
|
|
1810
|
+
'div',
|
|
1811
|
+
{ style: { display: 'flex', flexDirection: 'column', gap: 6, padding: '8px 12px', background: 'var(--dsw-alias-bg-layer-2)', borderRadius: 8, border: '1px solid var(--dsw-alias-border-l2)', marginTop: 8 } },
|
|
1812
|
+
React.createElement(
|
|
1813
|
+
'label',
|
|
1814
|
+
{ style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
|
|
1815
|
+
React.createElement('input', {
|
|
1816
|
+
type: 'checkbox',
|
|
1817
|
+
checked: currentPreset.multi_turn_enabled !== false,
|
|
1818
|
+
onChange: (e) => updateCurrentPreset((p) => ({ ...p, multi_turn_enabled: e.target.checked })),
|
|
1819
|
+
style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
|
|
1820
|
+
}),
|
|
1821
|
+
t('aggregator.multi_turn_title') || 'Multi-Turn Context & Diff Pruning'
|
|
1822
|
+
),
|
|
1823
|
+
React.createElement(
|
|
1824
|
+
'div',
|
|
1825
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
|
|
1826
|
+
t('aggregator.multi_turn_desc') || 'Retain conversation context across turns while pruning duplicate code blocks.'
|
|
1827
|
+
)
|
|
1828
|
+
),
|
|
1829
|
+
|
|
1830
|
+
/* Fallback Judges Chain */
|
|
1831
|
+
React.createElement(
|
|
1832
|
+
'div',
|
|
1833
|
+
{ style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 } },
|
|
1834
|
+
React.createElement(
|
|
1835
|
+
'div',
|
|
1836
|
+
{ style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
|
|
1837
|
+
t('aggregator.fallbacks_title')
|
|
1838
|
+
),
|
|
1839
|
+
React.createElement(
|
|
1840
|
+
'div',
|
|
1841
|
+
{ style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } },
|
|
1842
|
+
t('aggregator.fallbacks_desc')
|
|
1843
|
+
),
|
|
1844
|
+
renderSlotEditorList({
|
|
1845
|
+
items: currentPreset.aggregator_fallbacks,
|
|
1846
|
+
badgePrefix: 'Fallback',
|
|
1847
|
+
onUpdate: (idx, prov, mod) => {
|
|
1848
|
+
updateCurrentPreset((p) => {
|
|
1849
|
+
const list = [...(p.aggregator_fallbacks || [])]
|
|
1850
|
+
list[idx] = { provider: prov, model: mod }
|
|
1851
|
+
return { ...p, aggregator_fallbacks: list }
|
|
1852
|
+
})
|
|
1853
|
+
},
|
|
1854
|
+
onRemove: (idx) => {
|
|
1855
|
+
updateCurrentPreset((p) => ({
|
|
1856
|
+
...p,
|
|
1857
|
+
aggregator_fallbacks: (p.aggregator_fallbacks || []).filter((_, i) => i !== idx),
|
|
1858
|
+
}))
|
|
1859
|
+
},
|
|
1860
|
+
onAdd: () => {
|
|
1861
|
+
updateCurrentPreset((p) => ({
|
|
1862
|
+
...p,
|
|
1863
|
+
aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: (availableModels[0] && availableModels[0].provider) || '', model: (availableModels[0] && availableModels[0].model) || '' }],
|
|
1864
|
+
}))
|
|
1865
|
+
},
|
|
1866
|
+
addLabel: t('aggregator.add_fallback_btn'),
|
|
1867
|
+
emptyWarning: null,
|
|
1868
|
+
availableModels,
|
|
1869
|
+
t,
|
|
1870
|
+
})
|
|
1568
1871
|
)
|
|
1569
1872
|
)
|
|
1570
1873
|
)
|
|
@@ -2021,6 +2324,9 @@ window.__ModuleLoader__.load({
|
|
|
2021
2324
|
)
|
|
2022
2325
|
}
|
|
2023
2326
|
|
|
2327
|
+
const UNAVAILABLE_SNAPSHOT = Object.freeze({ status: 'unavailable' })
|
|
2328
|
+
const LOADING_SNAPSHOT = Object.freeze({ status: 'loading' })
|
|
2329
|
+
|
|
2024
2330
|
function useMoASettings(props) {
|
|
2025
2331
|
const effectiveCtx = (props && props.ctx) || rootCtx
|
|
2026
2332
|
// English is canonical; prefer the core-provided translator for the
|
|
@@ -2033,12 +2339,12 @@ window.__ModuleLoader__.load({
|
|
|
2033
2339
|
return s && typeof s.get === 'function' ? s.get(NS) : undefined
|
|
2034
2340
|
}, [effectiveCtx])
|
|
2035
2341
|
|
|
2342
|
+
const getSnapshot = React.useCallback(() => (scope ? scope.getSnapshot() : UNAVAILABLE_SNAPSHOT), [scope])
|
|
2343
|
+
const getServerSnapshot = React.useCallback(() => LOADING_SNAPSHOT, [])
|
|
2344
|
+
const subscribe = React.useCallback((cb) => (scope ? scope.subscribe(cb) : () => {}), [scope])
|
|
2345
|
+
|
|
2036
2346
|
const snapshot = React.useSyncExternalStore
|
|
2037
|
-
? React.useSyncExternalStore(
|
|
2038
|
-
React.useMemo(() => (cb) => (scope ? scope.subscribe(cb) : () => {}), [scope]),
|
|
2039
|
-
React.useCallback(() => (scope ? scope.getSnapshot() : { status: 'unavailable' }), [scope]),
|
|
2040
|
-
React.useCallback(() => ({ status: 'loading' }), [])
|
|
2041
|
-
)
|
|
2347
|
+
? React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
|
2042
2348
|
: null
|
|
2043
2349
|
|
|
2044
2350
|
const [status, setStatus] = React.useState('loading')
|
|
@@ -2052,6 +2358,7 @@ window.__ModuleLoader__.load({
|
|
|
2052
2358
|
currentVersion: '0.2.16',
|
|
2053
2359
|
latestVersion: undefined,
|
|
2054
2360
|
updateAvailable: false,
|
|
2361
|
+
canAutoUpdate: true,
|
|
2055
2362
|
notice: null,
|
|
2056
2363
|
error: null,
|
|
2057
2364
|
})
|
|
@@ -2068,10 +2375,11 @@ window.__ModuleLoader__.load({
|
|
|
2068
2375
|
currentVersion: data.currentVersion,
|
|
2069
2376
|
latestVersion: data.latestVersion,
|
|
2070
2377
|
updateAvailable: Boolean(data.updateAvailable),
|
|
2378
|
+
canAutoUpdate: data.canAutoUpdate !== undefined ? Boolean(data.canAutoUpdate) : prev.canAutoUpdate,
|
|
2071
2379
|
error: data.latestCheckFailed ? (t('updater.checkFailed') || 'Registry check failed') : null,
|
|
2072
2380
|
}))
|
|
2073
2381
|
} else {
|
|
2074
|
-
setUpdateState((prev) => ({ ...prev, checking: false, error: data
|
|
2382
|
+
setUpdateState((prev) => ({ ...prev, checking: false, error: data?.error || 'Check failed' }))
|
|
2075
2383
|
}
|
|
2076
2384
|
} catch (err) {
|
|
2077
2385
|
setUpdateState((prev) => ({ ...prev, checking: false, error: String(err?.message || err) }))
|
|
@@ -2086,19 +2394,20 @@ window.__ModuleLoader__.load({
|
|
|
2086
2394
|
headers: { 'Content-Type': 'application/json', 'x-dsh-plugin-update': '1' },
|
|
2087
2395
|
})
|
|
2088
2396
|
const data = await res.json()
|
|
2089
|
-
if (data && data.updated) {
|
|
2397
|
+
if (data && (data.updated || data.updatedVersion || data.restartRequired || data.ok)) {
|
|
2398
|
+
const nextVer = data.updatedVersion || data.latestVersion || data.currentVersion
|
|
2090
2399
|
setUpdateState((prev) => ({
|
|
2091
2400
|
...prev,
|
|
2092
2401
|
updating: false,
|
|
2093
2402
|
updateAvailable: false,
|
|
2094
|
-
currentVersion:
|
|
2095
|
-
notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}',
|
|
2403
|
+
currentVersion: nextVer,
|
|
2404
|
+
notice: (t('updater.success') || 'Successfully updated to v{version}. Restart DSH to apply.').replace('{version}', nextVer),
|
|
2096
2405
|
}))
|
|
2097
2406
|
} else {
|
|
2098
2407
|
setUpdateState((prev) => ({
|
|
2099
2408
|
...prev,
|
|
2100
2409
|
updating: false,
|
|
2101
|
-
error: data
|
|
2410
|
+
error: data?.error || data?.message || 'Update failed',
|
|
2102
2411
|
}))
|
|
2103
2412
|
}
|
|
2104
2413
|
} catch (err) {
|
|
@@ -2258,7 +2567,7 @@ window.__ModuleLoader__.load({
|
|
|
2258
2567
|
if (res.defaultPreset) setDefaultPreset(res.defaultPreset)
|
|
2259
2568
|
setTimeout(() => setSaveStatus(''), 3000)
|
|
2260
2569
|
} else {
|
|
2261
|
-
setSaveStatus('Error saving: ' + (res?.error || 'failed'))
|
|
2570
|
+
setSaveStatus((t('status.error_saving') || 'Error saving: ') + (res?.error || 'failed'))
|
|
2262
2571
|
}
|
|
2263
2572
|
})
|
|
2264
2573
|
.catch((err) => {
|