@goodandready/dsh-moa 0.2.8 → 0.2.10

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 CHANGED
@@ -1,135 +1,249 @@
1
+ // Plugin Settings Card (settings.plugin.item).
2
+ //
3
+ // Aligned with dsh-clinebot design language:
4
+ // - Header with real status chips (host online/offline/disabled via /dsh-moa/status)
5
+ // - Clean section cards (.moa-section-card) using native DSH design tokens (--dsw-alias-*)
6
+ // - Segmented preset selectors and count pills
7
+ // - Refined temperature sliders and searchable model picker
8
+ // - Telemetry summary grid fed by /dsh-moa/history
9
+ // - Robust error boundary and settingsScope reactivity
10
+ //
11
+ // English is the canonical source language; the ru translation is provided
12
+ // by the DSH translation plugin at runtime (no bundled ru duplicate).
13
+
1
14
  window.__ModuleLoader__.load({
2
15
  id: '@goodandready/dsh-moa',
3
16
  factory: (require) => {
4
17
  var module = { exports: {} }
5
18
  var exports = module.exports
6
- let React = require('react')
7
-
19
+ const React = require('react')
8
20
  const NS = 'dsh-moa'
9
- let localeRegistered = false
21
+ const TITLE = 'Mixture of Agents (MoA)'
22
+ const SUBTITLE = 'Ensemble parallel candidate generation with judge synthesis via /moa.'
10
23
 
11
- let ChevronIcon = null
12
- try {
13
- const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
14
- ChevronIcon = primitives && primitives.IconChevronDownOutline14
15
- } catch {
16
- ChevronIcon = null
24
+ let rootCtx = null
25
+
26
+ const en = {
27
+ title: 'Mixture of Agents (MoA)',
28
+ description: 'Ensemble model orchestration: parallel candidate proposals + judge synthesis via /moa <prompt>.',
29
+ 'header.title': 'Mixture of Agents (MoA) Orchestration',
30
+ 'header.sub': 'Parallel generation by candidate proposers with final evaluation and code promotion by a leading aggregator/judge. Returns to your session model automatically.',
31
+ 'badge.online': 'MoA Active',
32
+ 'badge.offline': 'Host Unreachable',
33
+ 'badge.preset': 'Preset: {name}',
34
+ 'badge.models': '{count} models available',
35
+ 'badge.candidates': '{count} proposers',
36
+ 'presets.title': '📋 Preset Management',
37
+ 'presets.desc': 'Select active preset or create custom configurations for specific tasks (e.g. code review, math, quick triage).',
38
+ 'presets.active_badge': 'Active Default',
39
+ 'presets.set_default': 'Set as Default',
40
+ 'presets.new_btn': '+ New Preset',
41
+ 'presets.del_btn': 'Delete',
42
+ 'presets.del_confirm': 'Are you sure you want to delete preset "{name}"?',
43
+ 'presets.create': 'Create',
44
+ 'presets.cancel': 'Cancel',
45
+ 'presets.name_placeholder': 'New preset name (e.g. fast-audit)',
46
+ 'aggregator.title': '⚖️ Leading Model / Judge (Synthesis & Audit)',
47
+ 'aggregator.desc': 'Compares candidate proposals, resolves discrepancies, filters hallucination, and synthesizes final solution.',
48
+ 'aggregator.temp_label': 'Judge Temperature',
49
+ 'aggregator.temp_hint': 'Recommended: 0.2 – 0.4 for strict validation and deterministic synthesis.',
50
+ 'aggregator.criteria_label': 'Judge Evaluation Criteria (Optional):',
51
+ 'aggregator.criteria_placeholder': 'e.g. Priority: performance, zero external dependencies, robust edge-case handling...',
52
+ 'aggregator.questions_label': 'Ask clarifying questions on broad tasks (Questionnaire)',
53
+ 'aggregator.questions_hint': 'When enabled: for short/vague prompts, the judge synthesizes a 2-4 question checklist before code generation.',
54
+ 'aggregator.curator_label': 'Curator Synthesis & Master Assembler Advice',
55
+ 'aggregator.curator_hint': 'When enabled: the curator highlights the finest components of each candidate, applies the antipatterns rubric, and advises which agent model should assemble the solution.',
56
+ 'aggregator.stream_label': 'Live Stream Curator/Judge Thinking',
57
+ 'aggregator.stream_hint': 'Streams aggregator tokens directly to chat in real time for zero-latency initial response.',
58
+ 'aggregator.fallbacks_title': 'Fallback Judges Chain',
59
+ 'aggregator.fallbacks_desc': 'Sequential backup models invoked automatically if the primary judge experiences rate limits or outages.',
60
+ 'aggregator.add_fallback_btn': '+ Add Fallback Judge',
61
+ 'proposers.quorum_label': 'Quorum Fast-Forward (Straggler Mitigation)',
62
+ 'proposers.quorum_hint': 'When 67%+ candidates finish, start a grace timer (default: 10s) and proceed without waiting for hanging providers.',
63
+ 'proposers.grace_label': 'Grace Period (sec):',
64
+ 'proposers.title': '👥 Candidate Models (Proposers)',
65
+ 'proposers.desc': 'Generate independent solutions in parallel to maximize diversity and solution space coverage.',
66
+ 'proposers.parallel_count': 'Parallel Proposers',
67
+ 'proposers.candidate': 'Candidate',
68
+ 'proposers.add_btn': '+ Add Candidate Model',
69
+ 'proposers.temp_label': 'Candidates Temperature',
70
+ 'proposers.temp_hint': 'Recommended: 0.6 – 0.8 for creative, independent solution exploration.',
71
+ 'stats.title': '📈 MoA Analytics & Telemetry',
72
+ 'stats.desc': 'Session telemetry, historical runs, and model win-rate tracking.',
73
+ 'stats.total_runs': 'Total Runs',
74
+ 'stats.avg_cost': 'Avg Run Cost',
75
+ 'stats.active_models': 'Configured Models',
76
+ 'stats.preset_count': 'Total Presets',
77
+ 'picker.select': 'Select model…',
78
+ 'picker.search_placeholder': 'Search model by name or provider…',
79
+ 'picker.found': 'Found',
80
+ 'picker.of': 'of',
81
+ 'picker.clear': 'Clear',
82
+ 'picker.none': 'No matching models found.',
83
+ 'picker.custom': 'Use custom: ',
84
+ 'actions.save': 'Save Configuration',
85
+ 'actions.saving': 'Saving…',
86
+ 'actions.saved': 'Configuration saved successfully!',
87
+ 'actions.retry': 'Reload Configuration',
88
+ 'badge.disabled': 'MoA Disabled',
89
+ 'config.enabled_label': 'Enable MoA (slash command and turn routing)',
90
+ 'slash.desc': 'Run turn with Mixture of Agents ensemble synthesis (/moa [preset] <prompt>)',
91
+ }
92
+
93
+ function makeT(dict, fallback) {
94
+ return function t(key, vars) {
95
+ let val = (dict && dict[key]) || (fallback && fallback[key]) || key
96
+ if (vars && typeof val === 'string') {
97
+ for (const k of Object.keys(vars)) {
98
+ val = val.replace(new RegExp('\\{' + k + '\\}', 'g'), String(vars[k]))
99
+ }
100
+ }
101
+ return val
102
+ }
17
103
  }
18
104
 
19
- function FallbackChevron({ className }) {
105
+ function FallbackChevron({ className, style }) {
20
106
  return React.createElement(
21
107
  'svg',
22
108
  {
23
- className,
24
109
  width: 14,
25
110
  height: 14,
26
111
  viewBox: '0 0 14 14',
27
112
  fill: 'none',
113
+ 'aria-hidden': true,
114
+ className,
115
+ style,
116
+ },
117
+ React.createElement('path', {
118
+ d: 'M3.5 5.25L7 8.75L10.5 5.25',
28
119
  stroke: 'currentColor',
29
120
  strokeWidth: 1.5,
30
121
  strokeLinecap: 'round',
31
122
  strokeLinejoin: 'round',
32
- },
33
- React.createElement('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' })
123
+ })
34
124
  )
35
125
  }
36
-
37
- const Chevron = ChevronIcon || FallbackChevron
126
+ let Chevron = FallbackChevron
127
+ try {
128
+ const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
129
+ if (primitives && typeof primitives.IconChevronDownOutline14 === 'function') {
130
+ Chevron = primitives.IconChevronDownOutline14
131
+ }
132
+ } catch (noPrimitives) {
133
+ Chevron = FallbackChevron
134
+ }
38
135
 
39
136
  const STYLES = `
40
- .moa-section { display:flex; flex-direction:column; gap:16px; padding:4px 0; max-width:780px; }
41
- .moa-section-title { font-size:18px; font-weight:700; color:var(--dsw-alias-label-primary); }
42
- .moa-section-sub { font-size:13px; color:var(--dsw-alias-label-secondary); margin-top:2px; margin-bottom:6px; }
43
- .moa-card { border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); border-radius:12px; list-style:none; margin-bottom:12px; }
44
- .moa-head { appearance:none; width:100%; font:inherit; color:inherit; text-align:left; cursor:pointer; background:0 0; border:0; border-radius:12px; display:flex; align-items:center; gap:12px; padding:14px 16px; }
45
- .moa-head:focus-visible { outline:2px solid var(--dsw-alias-label-primary); outline-offset:2px; }
46
- .moa-title { color:var(--dsw-alias-label-primary); font-size:15px; font-weight:600; line-height:1.4; }
47
- .moa-sub { color:var(--dsw-alias-label-secondary); font-size:13px; margin-top:2px; }
48
- .moa-chev { margin-left:auto; flex:none; color:var(--dsw-alias-label-tertiary); transition:transform .16s ease; }
49
- .moa-chev-open { transform:rotate(180deg); }
50
- .moa-body { border-top:1px solid var(--dsw-alias-border-l2); margin:0 16px; padding:12px 0 16px; display:flex; flex-direction:column; gap:14px; }
51
- .moa-box { border:1px solid var(--dsw-alias-border-l2); border-radius:10px; padding:14px 16px; background:var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.02)); display:flex; flex-direction:column; gap:12px; }
52
- .moa-box-title { font-size:14px; font-weight:600; color:var(--dsw-alias-label-primary); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px; }
53
- .moa-field { display:flex; flex-direction:column; gap:6px; }
54
- .moa-label { font-size:13px; font-weight:500; color:var(--dsw-alias-label-primary); }
55
- .moa-hint { font-size:12px; color:var(--dsw-alias-label-secondary); line-height:1.4; }
56
- .moa-select { height:36px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:8px; padding:0 10px; font-size:13px; }
57
- .moa-select:focus { outline:1px solid var(--dsw-alias-label-primary); }
58
- .moa-count-bar { display:flex; align-items:center; gap:8px; }
59
- .moa-count-btn { appearance:none; font:inherit; padding:4px 12px; border-radius:6px; border:1px solid var(--dsw-alias-border-l2); background:transparent; color:var(--dsw-alias-label-primary); font-size:12px; cursor:pointer; }
60
- .moa-count-btn-active { background:var(--dsw-alias-label-primary); color:var(--dsw-alias-bg-layer-3); border-color:transparent; font-weight:600; }
61
- .moa-refs-list { display:flex; flex-direction:column; gap:10px; }
62
- .moa-ref-row { display:flex; align-items:center; gap:8px; }
63
- .moa-ref-badge { font-size:12px; font-weight:600; color:var(--dsw-alias-label-secondary); width:85px; flex-shrink:0; }
64
- .moa-ref-picker { flex:1; min-width:0; }
65
- .moa-btn-del { appearance:none; font:inherit; border:none; background:transparent; color:var(--dsw-alias-label-tertiary); cursor:pointer; padding:6px 10px; font-size:14px; border-radius:6px; }
66
- .moa-btn-del:hover { color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-3); }
67
- .moa-btn-add { appearance:none; font:inherit; border:1px dashed var(--dsw-alias-border-l2); background:transparent; color:var(--dsw-alias-label-primary); border-radius:8px; padding:8px 12px; font-size:13px; font-weight:500; cursor:pointer; text-align:center; transition:border-color .15s; }
68
- .moa-btn-add:hover { border-color:var(--dsw-alias-label-primary); }
69
- .moa-foot { border-top:1px solid var(--dsw-alias-border-l2); display:flex; justify-content:flex-end; align-items:center; gap:10px; padding:14px 0 4px; }
70
- .moa-save { appearance:none; font:inherit; cursor:pointer; border:1px solid transparent; border-radius:8px; padding:8px 20px; font-size:13px; font-weight:600; background:var(--dsw-alias-label-primary); color:var(--dsw-alias-bg-layer-3); transition:opacity .15s; }
71
- .moa-save:hover { opacity:0.9; }
72
- .moa-status-text { font-size:12px; color:var(--dsw-alias-label-secondary); }
73
-
74
- /* Preset Toolbar & Actions */
75
- .moa-preset-bar { display:flex; flex-direction:column; gap:8px; margin-bottom:4px; }
76
- .moa-preset-row { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
77
- .moa-btn-sub { appearance:none; font:inherit; padding:7px 12px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2); background:transparent; color:var(--dsw-alias-label-primary); font-size:12px; font-weight:500; cursor:pointer; white-space:nowrap; }
78
- .moa-btn-sub:hover { border-color:var(--dsw-alias-label-primary); background:var(--dsw-alias-bg-layer-3); }
79
- .moa-btn-del-preset { appearance:none; font:inherit; padding:7px 12px; border-radius:8px; border:1px solid var(--dsw-alias-border-l2); background:transparent; color:#ef4444; font-size:12px; font-weight:500; cursor:pointer; white-space:nowrap; }
80
- .moa-btn-del-preset:hover { border-color:#ef4444; background:rgba(239,68,68,0.08); }
81
- .moa-new-preset-box { display:flex; align-items:center; gap:8px; padding:10px 12px; border:1px dashed var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)); border-radius:8px; background:var(--dsw-alias-bg-layer-2); margin-top:4px; }
82
- .moa-new-input { flex:1; height:32px; padding:0 10px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:6px; font-size:13px; outline:none; }
83
- .moa-new-input:focus { border-color:var(--dsw-alias-label-primary); }
84
-
85
- /* Temperature Sliders */
86
- .moa-temp-ctrl { display:flex; align-items:center; gap:12px; margin-top:2px; }
87
- .moa-temp-slider { flex:1; accent-color:var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary)); cursor:pointer; height:4px; }
88
- .moa-temp-input { width:62px; height:30px; text-align:center; padding:0 6px; border:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-3); color:var(--dsw-alias-label-primary); border-radius:6px; font-size:13px; font-weight:600; outline:none; }
89
- .moa-temp-input:focus { border-color:var(--dsw-alias-label-primary); }
90
-
91
- /* Searchable Model Picker */
92
- .moa-picker-container { position:relative; width:100%; }
93
- .moa-picker-btn { width:100%; min-height:36px; padding:5px 12px; background:var(--dsw-alias-bg-layer-3); border:1px solid var(--dsw-alias-border-l2); border-radius:8px; color:var(--dsw-alias-label-primary); display:flex; align-items:center; justify-content:space-between; cursor:pointer; font-size:13px; text-align:left; box-sizing:border-box; gap:8px; }
94
- .moa-picker-btn:hover { border-color:var(--dsw-alias-label-secondary); }
95
- .moa-picker-btn:focus-visible { outline:2px solid var(--dsw-alias-label-primary); outline-offset:1px; }
96
- .moa-picker-summary { display:flex; align-items:center; gap:8px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
97
- .moa-prov-chip { font-size:11px; font-weight:600; text-transform:uppercase; padding:2px 6px; border-radius:4px; background:var(--dsw-alias-bg-layer-1, rgba(255,255,255,0.08)); color:var(--dsw-alias-label-secondary); flex-shrink:0; }
98
- .moa-model-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:500; }
99
- .moa-picker-popover { position:absolute; top:calc(100% + 4px); left:0; right:0; background:var(--dsw-alias-bg-layer-3); border:1px solid var(--dsw-alias-border-l2); border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,0.4); z-index:1000; max-height:360px; display:flex; flex-direction:column; overflow:hidden; }
100
- .moa-picker-search-bar { padding:8px 10px; border-bottom:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); display:flex; flex-direction:column; gap:4px; }
101
- .moa-search-input { width:100%; height:32px; padding:0 10px; background:var(--dsw-alias-bg-layer-3); border:1px solid var(--dsw-alias-border-l2); border-radius:6px; color:var(--dsw-alias-label-primary); font-size:13px; outline:none; box-sizing:border-box; }
102
- .moa-search-input:focus { border-color:var(--dsw-alias-label-primary); }
103
- .moa-search-info { font-size:11px; color:var(--dsw-alias-label-tertiary); padding:0 2px; display:flex; justify-content:space-between; }
104
- .moa-picker-list { overflow-y:auto; max-height:280px; padding:6px; display:flex; flex-direction:column; gap:2px; }
105
- .moa-group-title { font-size:11px; font-weight:700; text-transform:uppercase; color:var(--dsw-alias-label-tertiary); padding:8px 8px 4px; }
106
- .moa-picker-item { display:flex; align-items:center; justify-content:space-between; padding:7px 10px; border-radius:6px; font-size:13px; color:var(--dsw-alias-label-primary); cursor:pointer; text-align:left; border:none; background:transparent; width:100%; gap:8px; }
107
- .moa-picker-item:hover, .moa-picker-item-selected { background:var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06)); }
108
- .moa-picker-check { color:var(--dsw-alias-brand-primary, #10b981); font-weight:bold; font-size:14px; flex-shrink:0; }
109
- .moa-custom-opt { padding:8px 10px; border-top:1px solid var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); font-size:12px; color:var(--dsw-alias-label-primary); cursor:pointer; display:flex; align-items:center; gap:6px; }
110
- .moa-custom-opt:hover { background:var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.06)); }
111
- `
137
+ /* dsh-moa modern clinebot-aligned styling */
138
+ .moa-page{display:flex;flex-direction:column;gap:18px;padding:4px 0 24px;max-width:960px}
139
+ .moa-header{display:flex;flex-direction:column;gap:8px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}
140
+ .moa-page-title{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:10px;flex-wrap:wrap}
141
+ .moa-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}
142
+
143
+ .moa-badge-row{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:2px}
144
+ .moa-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}
145
+ .moa-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent)}
146
+ .moa-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary) 10%, transparent)}
147
+ .moa-badge-brand{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
148
+
149
+ .moa-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:14px}
150
+ .moa-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}
151
+ .moa-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}
152
+
153
+ .moa-grid-2{display:grid;grid-template-columns:repeat(auto-fit, minmax(200px, 1fr));gap:12px}
154
+ .moa-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
155
+ .moa-stat-val{font-size:18px;font-weight:700;color:var(--dsw-alias-label-primary)}
156
+ .moa-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}
157
+
158
+ .moa-input{height:36px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;width:100%;box-sizing:border-box}
159
+ .moa-input:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))}
160
+ .moa-textarea{width:100%;box-sizing:border-box;padding:10px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:13px;resize:vertical;outline:none;line-height:1.4}
161
+ .moa-textarea:focus{border-color:var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))}
162
+
163
+ .moa-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}
164
+ .moa-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4, var(--dsw-alias-bg-layer-1));border-color:var(--dsw-alias-label-secondary)}
165
+ .moa-btn-primary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent}
166
+ .moa-btn-primary:hover:not(:disabled){background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;opacity:0.9}
167
+ .moa-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 35%, transparent)}
168
+ .moa-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 12%, transparent) !important;border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent)}
169
+ .moa-btn-sub{padding:5px 12px;font-size:12px;border-radius:6px}
170
+
171
+ /* Segmented Buttons */
172
+ .moa-seg-group{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
173
+ .moa-seg-btn{appearance:none;font:inherit;padding:6px 14px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);font-size:13px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s ease}
174
+ .moa-seg-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-secondary)}
175
+ .moa-seg-btn-active{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent;font-weight:600}
176
+ .moa-seg-btn-active:hover{background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important}
177
+
178
+ /* Sliders */
179
+ .moa-slider-row{display:flex;align-items:center;gap:14px}
180
+ .moa-slider{flex:1;accent-color:var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary));cursor:pointer;height:6px}
181
+ .moa-slider-badge{min-width:48px;height:30px;padding:0 8px;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:6px;font-size:13px;font-weight:700;color:var(--dsw-alias-label-primary)}
182
+
183
+ /* Candidate Rows */
184
+ .moa-cand-list{display:flex;flex-direction:column;gap:10px}
185
+ .moa-cand-row{display:flex;align-items:center;gap:10px}
186
+ .moa-cand-badge{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);min-width:92px;flex-shrink:0}
187
+ .moa-cand-picker{flex:1;min-width:0}
188
+
189
+ /* Searchable Model Picker */
190
+ .moa-picker-container{position:relative;width:100%}
191
+ .moa-picker-btn{width:100%;min-height:38px;padding:6px 12px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;cursor:pointer;font-size:13px;text-align:left;box-sizing:border-box;gap:8px}
192
+ .moa-picker-btn:hover{border-color:var(--dsw-alias-label-secondary)}
193
+ .moa-picker-summary{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
194
+ .moa-prov-chip{font-size:11px;font-weight:600;text-transform:uppercase;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);flex-shrink:0}
195
+ .moa-model-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}
196
+ .moa-picker-popover{position:absolute;top:calc(100% + 4px);left:0;right:0;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;box-shadow:0 12px 36px color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);z-index:1000;max-height:360px;display:flex;flex-direction:column;overflow:hidden}
197
+ .moa-picker-search-bar{padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}
198
+ .moa-search-input{width:100%;height:32px;padding:0 10px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;color:var(--dsw-alias-label-primary);font-size:13px;outline:none;box-sizing:border-box}
199
+ .moa-search-input:focus{border-color:var(--dsw-alias-label-primary)}
200
+ .moa-search-info{font-size:11px;color:var(--dsw-alias-label-tertiary);padding:0 2px;display:flex;justify-content:space-between}
201
+ .moa-picker-list{overflow-y:auto;max-height:280px;padding:6px;display:flex;flex-direction:column;gap:2px}
202
+ .moa-group-title{font-size:11px;font-weight:700;text-transform:uppercase;color:var(--dsw-alias-label-tertiary);padding:8px 8px 4px}
203
+ .moa-picker-item{display:flex;align-items:center;justify-content:space-between;padding:7px 10px;border-radius:6px;font-size:13px;color:var(--dsw-alias-label-primary);cursor:pointer;text-align:left;border:none;background:transparent;width:100%;gap:8px}
204
+ .moa-picker-item:hover, .moa-picker-item-selected{background:var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-2))}
205
+ .moa-picker-check{color:var(--dsw-alias-state-success-primary);font-weight:bold;font-size:14px;flex-shrink:0}
206
+ .moa-custom-opt{padding:8px 10px;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);font-size:12px;color:var(--dsw-alias-label-primary);cursor:pointer;display:flex;align-items:center;gap:6px}
207
+
208
+ /* Alerts & Collapsible Card */
209
+ .moa-alert-ok{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);color:var(--dsw-alias-state-success-primary);font-size:13px;font-weight:500}
210
+ .moa-alert-err{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px;font-weight:500}
211
+ .moa-card{list-style:none;margin:0;padding:0;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;overflow:hidden;transition:border-color .15s ease}
212
+ .moa-head{width:100%;padding:14px 18px;display:flex;align-items:center;justify-content:space-between;background:transparent;border:none;color:inherit;cursor:pointer;text-align:left}
213
+ .moa-head:hover{background:var(--dsw-alias-bg-layer-2)}
214
+ .moa-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary)}
215
+ .moa-sub{font-size:12px;color:var(--dsw-alias-label-secondary);margin-top:2px}
216
+ .moa-chev{transition:transform .2s ease;color:var(--dsw-alias-label-secondary)}
217
+ .moa-chev-open{transform:rotate(180deg)}
218
+ .moa-body{padding:0 18px 20px;border-top:1px solid var(--dsw-alias-border-l2)}
219
+ `
112
220
 
113
221
  let stylesInjected = false
114
222
  function ensureStyles() {
115
223
  if (stylesInjected || typeof document === 'undefined') return
116
224
  stylesInjected = true
117
225
  const style = document.createElement('style')
226
+ style.id = 'dsh-moa-full-css'
227
+ if (style.dataset) {
228
+ style.dataset.dshPlugin = NS
229
+ } else if (typeof style.setAttribute === 'function') {
230
+ style.setAttribute('data-dsh-plugin', NS)
231
+ }
118
232
  style.textContent = STYLES
119
233
  document.head.appendChild(style)
120
234
  }
121
235
 
122
236
  const PROVIDER_TITLES = {
123
- codex: 'ChatGPT Codex (Subscription)',
124
- claude: 'Claude (Anthropic Subscription)',
125
- grok: 'Grok (xAI Subscription)',
237
+ codex: 'ChatGPT Codex',
238
+ claude: 'Claude (Anthropic)',
239
+ grok: 'Grok (xAI)',
126
240
  antigravity: 'Antigravity (Google)',
127
241
  gemini: 'Google Gemini',
128
242
  commandcode: 'CommandCode Provider',
129
243
  'opencode-go': 'OpenCode-Go',
130
244
  'qwen-token-plan': 'Qwen (Token Plan)',
131
245
  groq: 'Groq Cloud',
132
- zai: 'Zhipu GLM (Zai)',
246
+ zai: 'Zhipu GLM',
133
247
  ollama: 'Ollama (Local / Cloud)',
134
248
  'kimi-coding': 'Kimi Coding',
135
249
  minimax: 'MiniMax',
@@ -141,7 +255,7 @@ window.__ModuleLoader__.load({
141
255
  provider,
142
256
  model,
143
257
  onChange,
144
- availableModels,
258
+ availableModels = [],
145
259
  t,
146
260
  }) {
147
261
  const [open, setOpen] = React.useState(false)
@@ -150,7 +264,7 @@ window.__ModuleLoader__.load({
150
264
  const inputRef = React.useRef(null)
151
265
 
152
266
  const selectedLabel = React.useMemo(() => {
153
- if (!provider && !model) return t('selectModel') || 'Выберите модель...'
267
+ if (!provider && !model) return t('picker.select')
154
268
  const found = availableModels.find((m) => m.provider === provider && m.model === model)
155
269
  return found ? (found.label || `${provider}: ${found.model}`) : `${provider}:${model}`
156
270
  }, [availableModels, provider, model, t])
@@ -178,19 +292,18 @@ window.__ModuleLoader__.load({
178
292
  const title = PROVIDER_TITLES[prov] || prov.toUpperCase()
179
293
  groups.push({ provider: prov, title, items })
180
294
  }
181
- groups.sort((a, b) => a.title.localeCompare(b.title))
182
295
  return groups
183
296
  }, [filtered])
184
297
 
185
298
  React.useEffect(() => {
186
299
  if (!open) return
187
- const onDown = (e) => {
300
+ const handleClickOutside = (e) => {
188
301
  if (rootRef.current && !rootRef.current.contains(e.target)) {
189
302
  setOpen(false)
190
303
  }
191
304
  }
192
- document.addEventListener('mousedown', onDown)
193
- return () => document.removeEventListener('mousedown', onDown)
305
+ document.addEventListener('mousedown', handleClickOutside)
306
+ return () => document.removeEventListener('mousedown', handleClickOutside)
194
307
  }, [open])
195
308
 
196
309
  React.useEffect(() => {
@@ -206,21 +319,23 @@ window.__ModuleLoader__.load({
206
319
  }
207
320
 
208
321
  const handleCustom = () => {
209
- const q = query.trim()
210
- if (!q) return
211
- const parts = q.split(':')
212
- if (parts.length >= 2) {
213
- onChange(parts[0].trim(), parts.slice(1).join(':').trim())
214
- } else {
215
- onChange(provider || 'custom', q)
322
+ const trimmed = query.trim()
323
+ if (!trimmed) return
324
+ let prov = provider || 'custom'
325
+ let mod = trimmed
326
+ if (trimmed.includes(':')) {
327
+ const [p, ...rest] = trimmed.split(':')
328
+ prov = p.trim()
329
+ mod = rest.join(':').trim()
216
330
  }
331
+ onChange(prov, mod)
217
332
  setOpen(false)
218
333
  setQuery('')
219
334
  }
220
335
 
221
336
  return React.createElement(
222
337
  'div',
223
- { ref: rootRef, className: 'moa-picker-container' },
338
+ { className: 'moa-picker-container', ref: rootRef },
224
339
  React.createElement(
225
340
  'button',
226
341
  {
@@ -234,7 +349,7 @@ window.__ModuleLoader__.load({
234
349
  provider && React.createElement('span', { className: 'moa-prov-chip' }, provider),
235
350
  React.createElement('span', { className: 'moa-model-name' }, selectedLabel)
236
351
  ),
237
- React.createElement(Chevron, { className: open ? 'moa-chev moa-chev-open' : 'moa-chev' })
352
+ React.createElement(Chevron, { style: { transform: open ? 'rotate(180deg)' : 'none' } })
238
353
  ),
239
354
  open &&
240
355
  React.createElement(
@@ -247,7 +362,7 @@ window.__ModuleLoader__.load({
247
362
  ref: inputRef,
248
363
  type: 'text',
249
364
  className: 'moa-search-input',
250
- placeholder: t('searchPlaceholder') || 'Поиск по названию или провайдеру...',
365
+ placeholder: t('picker.search_placeholder'),
251
366
  value: query,
252
367
  onChange: (e) => setQuery(e.target.value),
253
368
  onKeyDown: (e) => {
@@ -261,13 +376,13 @@ window.__ModuleLoader__.load({
261
376
  React.createElement(
262
377
  'span',
263
378
  null,
264
- `${t('modelsFound') || 'Найдено'}: ${filtered.length} ${t('of') || 'из'} ${availableModels.length}`
379
+ `${t('picker.found')}: ${filtered.length} ${t('picker.of')} ${availableModels.length}`
265
380
  ),
266
381
  query &&
267
382
  React.createElement(
268
383
  'span',
269
- { style: { cursor: 'pointer' }, onClick: () => setQuery('') },
270
- t('clear') || 'Сбросить'
384
+ { style: { cursor: 'pointer', textDecoration: 'underline' }, onClick: () => setQuery('') },
385
+ t('picker.clear')
271
386
  )
272
387
  )
273
388
  ),
@@ -304,7 +419,7 @@ window.__ModuleLoader__.load({
304
419
  React.createElement(
305
420
  'div',
306
421
  { style: { padding: '12px 10px', fontSize: 13, color: 'var(--dsw-alias-label-secondary)' } },
307
- t('noModelsMatch') || 'Модели не найдены.'
422
+ t('picker.none')
308
423
  )
309
424
  ),
310
425
  query.trim() &&
@@ -315,7 +430,7 @@ window.__ModuleLoader__.load({
315
430
  React.createElement(
316
431
  'span',
317
432
  null,
318
- `${t('useCustom') || 'Использовать'}: "${query.trim()}"`
433
+ `${t('picker.custom')}"${query.trim()}"`
319
434
  )
320
435
  )
321
436
  )
@@ -329,14 +444,18 @@ window.__ModuleLoader__.load({
329
444
  defaultPreset,
330
445
  setDefaultPreset,
331
446
  availableModels,
447
+ enabled,
448
+ setEnabled,
449
+ hostStatus,
450
+ stats,
332
451
  handleSave,
333
452
  saveStatus,
453
+ reload,
334
454
  }) {
335
455
  const [selectedPresetName, setSelectedPresetName] = React.useState(defaultPreset || (presets[0] && presets[0].name) || 'default')
336
456
  const [isCreating, setIsCreating] = React.useState(false)
337
457
  const [newPresetName, setNewPresetName] = React.useState('')
338
458
 
339
- // Sync active preset if selected one is removed
340
459
  React.useEffect(() => {
341
460
  if (!presets.some((p) => p.name === selectedPresetName)) {
342
461
  setSelectedPresetName(defaultPreset || (presets[0] && presets[0].name) || 'default')
@@ -346,9 +465,11 @@ window.__ModuleLoader__.load({
346
465
  const currentPreset = presets.find((p) => p.name === selectedPresetName) || presets[0] || {
347
466
  name: 'default',
348
467
  reference_models: [],
349
- aggregator: { provider: '', model: '' },
468
+ aggregator: { provider: 'codex', model: 'gpt-5.6-sol' },
350
469
  aggregator_temperature: 0.4,
351
470
  reference_temperature: 0.6,
471
+ judge_criteria: '',
472
+ ask_clarifying_questions: true,
352
473
  }
353
474
 
354
475
  const updateCurrentPreset = (modifier) => {
@@ -363,10 +484,7 @@ window.__ModuleLoader__.load({
363
484
  }
364
485
 
365
486
  const setAggregatorModel = (prov, mod) => {
366
- updateCurrentPreset((p) => ({
367
- ...p,
368
- aggregator: { provider: prov, model: mod },
369
- }))
487
+ updateCurrentPreset((p) => ({ ...p, aggregator: { provider: prov, model: mod } }))
370
488
  }
371
489
 
372
490
  const setAggregatorTemp = (temp) => {
@@ -441,115 +559,146 @@ window.__ModuleLoader__.load({
441
559
  const created = {
442
560
  name,
443
561
  enabled: true,
562
+ ask_clarifying_questions: true,
444
563
  reference_models: currentPreset?.reference_models?.length ? structuredClone(currentPreset.reference_models) : [
445
564
  { provider: 'opencode-go', model: 'deepseek-v4-flash' },
446
565
  { provider: 'codex', model: 'gpt-5.6-sol' },
447
566
  ],
448
567
  aggregator: currentPreset?.aggregator?.provider ? structuredClone(currentPreset.aggregator) : {
449
- provider: 'antigravity',
450
- model: 'gemini-3-flash',
568
+ provider: 'codex',
569
+ model: 'gpt-5.6-sol',
451
570
  },
452
- reference_temperature: currentPreset?.reference_temperature ?? 0.6,
453
- aggregator_temperature: currentPreset?.aggregator_temperature ?? 0.4,
454
- max_tokens: currentPreset?.max_tokens ?? 4096,
571
+ reference_temperature: 0.6,
572
+ aggregator_temperature: 0.4,
573
+ max_tokens: 4096,
574
+ judge_criteria: '',
455
575
  }
456
- setPresets((prev) => [...prev, created])
576
+ setPresets((prev) => [...(prev || []), created])
457
577
  setSelectedPresetName(name)
458
578
  setIsCreating(false)
459
579
  setNewPresetName('')
460
580
  }
461
581
 
462
- const handleDeletePreset = () => {
582
+ const handleDeletePreset = (nameToDelete) => {
463
583
  if (presets.length <= 1) return
464
- if (!confirm(`${t('confirmDeletePreset') || 'Удалить пресет'} "${currentPreset.name}"?`)) return
465
- const remaining = presets.filter((p) => p.name !== currentPreset.name)
466
- if (defaultPreset === currentPreset.name) {
467
- setDefaultPreset(remaining[0].name)
584
+ if (typeof window !== 'undefined' && window.confirm) {
585
+ if (!window.confirm(t('presets.del_confirm', { name: nameToDelete }))) return
586
+ }
587
+ setPresets((prev) => prev.filter((p) => p.name !== nameToDelete))
588
+ if (defaultPreset === nameToDelete) {
589
+ const nextDefault = presets.find((p) => p.name !== nameToDelete)?.name || 'default'
590
+ setDefaultPreset(nextDefault)
468
591
  }
469
- setPresets(remaining)
470
- setSelectedPresetName(remaining[0].name)
471
592
  }
472
593
 
594
+ const aggTemp = currentPreset.aggregator_temperature !== undefined ? currentPreset.aggregator_temperature : 0.4
595
+ const refTemp = currentPreset.reference_temperature !== undefined ? currentPreset.reference_temperature : 0.6
473
596
  const currentRefs = currentPreset.reference_models || []
474
- const aggTemp = currentPreset.aggregator_temperature ?? 0.4
475
- const refTemp = currentPreset.reference_temperature ?? 0.6
476
597
  const judgeCriteria = currentPreset.judge_criteria || ''
598
+ const isDefault = defaultPreset === currentPreset.name
599
+ const hostStatusBadge = hostStatus === 'online'
600
+ ? React.createElement('span', { className: 'moa-badge moa-badge-ok' }, '🟢 ' + t('badge.online'))
601
+ : hostStatus === 'disabled'
602
+ ? React.createElement('span', { className: 'moa-badge moa-badge-warn' }, '⏸ ' + t('badge.disabled'))
603
+ : React.createElement('span', { className: 'moa-badge moa-badge-warn' }, '🔴 ' + t('badge.offline'))
477
604
 
478
605
  return React.createElement(
479
- React.Fragment,
480
- null,
481
- /* Preset Selector Toolbar (No top tab buttons!) */
606
+ 'div',
607
+ { className: 'moa-page' },
608
+
609
+ /* ── HEADER WITH LIVE STATUS CHIPS ── */
482
610
  React.createElement(
483
611
  'div',
484
- { className: 'moa-preset-bar' },
612
+ { className: 'moa-header' },
485
613
  React.createElement(
486
614
  'div',
487
- { className: 'moa-field' },
488
- React.createElement('label', { className: 'moa-label' }, t('presetSelectLabel') || 'Текущий пресет для настройки:'),
615
+ { className: 'moa-page-title' },
616
+ React.createElement('span', null, '🧠 ' + t('header.title')),
617
+ React.createElement(
618
+ 'div',
619
+ { className: 'moa-badge-row' },
620
+ hostStatusBadge,
621
+ React.createElement('span', { className: 'moa-badge moa-badge-brand' }, t('badge.preset', { name: currentPreset.name })),
622
+ React.createElement('span', { className: 'moa-badge moa-badge-brand' }, t('badge.models', { count: availableModels.length })),
623
+ React.createElement('span', { className: 'moa-badge moa-badge-brand' }, t('badge.candidates', { count: currentRefs.length }))
624
+ )
625
+ ),
626
+ React.createElement('div', { className: 'moa-page-sub' }, t('header.sub'))
627
+ ),
628
+
629
+ /* ── SECTION 1: PRESET MANAGEMENT ── */
630
+ React.createElement(
631
+ 'div',
632
+ { className: 'moa-section-card' },
633
+ React.createElement(
634
+ 'div',
635
+ { className: 'moa-section-title' },
636
+ React.createElement('span', null, t('presets.title')),
489
637
  React.createElement(
490
638
  'div',
491
- { className: 'moa-preset-row' },
492
- React.createElement(
493
- 'select',
494
- {
495
- className: 'moa-select',
496
- style: { flex: 1, minWidth: 200 },
497
- value: isCreating ? '__new__' : currentPreset.name,
498
- onChange: (e) => {
499
- if (e.target.value === '__new__') {
500
- setIsCreating(true)
501
- } else {
502
- setIsCreating(false)
503
- setSelectedPresetName(e.target.value)
504
- }
505
- },
506
- },
507
- presets.map((pr) =>
508
- React.createElement(
509
- 'option',
510
- { key: pr.name, value: pr.name },
511
- pr.name + (pr.name === defaultPreset ? ` ⭐ (${t('isDefault') || 'основной'})` : '')
512
- )
513
- ),
514
- React.createElement('option', { disabled: true }, '──────────────────'),
515
- React.createElement(
516
- 'option',
517
- { value: '__new__' },
518
- '+ ' + (t('createNewPreset') || 'Создать новый пресет...')
519
- )
520
- ),
521
- currentPreset.name !== defaultPreset &&
639
+ { style: { display: 'flex', gap: 8, alignItems: 'center' } },
640
+ !isDefault &&
522
641
  React.createElement(
523
642
  'button',
524
643
  {
525
644
  type: 'button',
526
- className: 'moa-btn-sub',
645
+ className: 'moa-btn moa-btn-sub',
527
646
  onClick: () => setDefaultPreset(currentPreset.name),
528
- title: t('makeDefaultHint') || 'Использовать этот пресет при вызове /moa без аргументов',
529
647
  },
530
- '⭐ ' + (t('makeDefault') || 'Сделать основным')
648
+ '★ ' + t('presets.set_default')
531
649
  ),
532
- presets.length > 1 &&
650
+ isDefault &&
651
+ React.createElement('span', { className: 'moa-badge moa-badge-ok' }, '✓ ' + t('presets.active_badge')),
652
+ !isCreating &&
533
653
  React.createElement(
534
654
  'button',
535
655
  {
536
656
  type: 'button',
537
- className: 'moa-btn-del-preset',
538
- onClick: handleDeletePreset,
539
- title: t('deletePreset') || 'Удалить пресет',
657
+ className: 'moa-btn moa-btn-sub',
658
+ onClick: () => setIsCreating(true),
540
659
  },
541
- '🗑️ ' + (t('delete') || 'Удалить')
660
+ t('presets.new_btn')
542
661
  )
543
662
  )
544
663
  ),
664
+ React.createElement('div', { className: 'moa-section-desc' }, t('presets.desc')),
665
+ React.createElement(
666
+ 'div',
667
+ { className: 'moa-seg-group' },
668
+ presets.map((p) => {
669
+ const isCur = p.name === selectedPresetName
670
+ return React.createElement(
671
+ 'button',
672
+ {
673
+ key: p.name,
674
+ type: 'button',
675
+ className: isCur ? 'moa-seg-btn moa-seg-btn-active' : 'moa-seg-btn',
676
+ onClick: () => setSelectedPresetName(p.name),
677
+ },
678
+ p.name === defaultPreset ? `★ ${p.name}` : p.name
679
+ )
680
+ }),
681
+ presets.length > 1 &&
682
+ React.createElement(
683
+ 'button',
684
+ {
685
+ type: 'button',
686
+ className: 'moa-btn moa-btn-danger moa-btn-sub',
687
+ onClick: () => handleDeletePreset(currentPreset.name),
688
+ title: t('presets.del_btn'),
689
+ },
690
+ '🗑 ' + t('presets.del_btn')
691
+ )
692
+ ),
545
693
  isCreating &&
546
694
  React.createElement(
547
695
  'div',
548
- { className: 'moa-new-preset-box' },
696
+ { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 4 } },
549
697
  React.createElement('input', {
550
698
  type: 'text',
551
- className: 'moa-new-input',
552
- placeholder: t('newPresetPlaceholder') || 'Название нового пресета (например: fast-audit)',
699
+ className: 'moa-input',
700
+ style: { maxWidth: 320 },
701
+ placeholder: t('presets.name_placeholder'),
553
702
  value: newPresetName,
554
703
  onChange: (e) => setNewPresetName(e.target.value),
555
704
  onKeyDown: (e) => {
@@ -561,39 +710,34 @@ window.__ModuleLoader__.load({
561
710
  'button',
562
711
  {
563
712
  type: 'button',
564
- className: 'moa-btn-sub',
713
+ className: 'moa-btn moa-btn-primary',
565
714
  onClick: handleCreatePreset,
566
- style: { background: 'var(--dsw-alias-label-primary)', color: 'var(--dsw-alias-bg-layer-3)' },
567
715
  },
568
- t('create') || 'Создать'
716
+ t('presets.create')
569
717
  ),
570
718
  React.createElement(
571
719
  'button',
572
720
  {
573
721
  type: 'button',
574
- className: 'moa-btn-sub',
722
+ className: 'moa-btn',
575
723
  onClick: () => setIsCreating(false),
576
724
  },
577
- t('cancel') || 'Отмена'
725
+ t('presets.cancel')
578
726
  )
579
727
  )
580
728
  ),
581
- /* Aggregator / Judge Box with Temperature Slider */
729
+
730
+ /* ── SECTION 2: AGGREGATOR / JUDGE ── */
582
731
  React.createElement(
583
732
  'div',
584
- { className: 'moa-box' },
733
+ { className: 'moa-section-card' },
585
734
  React.createElement(
586
735
  'div',
587
- { className: 'moa-box-title' },
588
- React.createElement('span', null, t('aggregatorTitle')),
589
- React.createElement(
590
- 'div',
591
- { style: { display: 'flex', alignItems: 'center', gap: 6 } },
592
- React.createElement('span', { className: 'moa-hint' }, `${t('temperature') || 'Температура'}:`),
593
- React.createElement('span', { style: { fontWeight: 600, fontSize: 13 } }, aggTemp)
594
- )
736
+ { className: 'moa-section-title' },
737
+ React.createElement('span', null, t('aggregator.title')),
738
+ React.createElement('span', { className: 'moa-badge moa-badge-brand' }, `Temp: ${aggTemp}`)
595
739
  ),
596
- React.createElement('div', { className: 'moa-hint' }, t('aggregatorDesc')),
740
+ React.createElement('div', { className: 'moa-section-desc' }, t('aggregator.desc')),
597
741
  React.createElement(SearchableModelPicker, {
598
742
  provider: currentPreset.aggregator?.provider || '',
599
743
  model: currentPreset.aggregator?.model || '',
@@ -603,57 +747,42 @@ window.__ModuleLoader__.load({
603
747
  }),
604
748
  React.createElement(
605
749
  'div',
606
- { className: 'moa-field', style: { marginTop: 4 } },
750
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4 } },
751
+ React.createElement(
752
+ 'div',
753
+ { style: { display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 500 } },
754
+ React.createElement('span', null, t('aggregator.temp_label')),
755
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } }, t('aggregator.temp_hint'))
756
+ ),
607
757
  React.createElement(
608
758
  'div',
609
- { className: 'moa-temp-ctrl' },
759
+ { className: 'moa-slider-row' },
610
760
  React.createElement('input', {
611
761
  type: 'range',
612
- className: 'moa-temp-slider',
762
+ className: 'moa-slider',
613
763
  min: '0',
614
764
  max: '2',
615
765
  step: '0.05',
616
766
  value: aggTemp,
617
767
  onChange: (e) => setAggregatorTemp(e.target.value),
618
768
  }),
619
- React.createElement('input', {
620
- type: 'number',
621
- className: 'moa-temp-input',
622
- min: '0',
623
- max: '2',
624
- step: '0.05',
625
- value: aggTemp,
626
- onChange: (e) => setAggregatorTemp(e.target.value),
627
- })
628
- ),
629
- React.createElement('div', { className: 'moa-hint' }, t('aggTempHint') || 'Рекомендуется 0.2 – 0.4 для точной проверки и синтеза без галлюцинаций.')
769
+ React.createElement('div', { className: 'moa-slider-badge' }, aggTemp)
770
+ )
630
771
  ),
631
772
  React.createElement(
632
773
  'div',
633
- { className: 'moa-field', style: { marginTop: 4, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 8 } },
634
- React.createElement('label', { className: 'moa-label', style: { fontSize: 12 } }, t('judgeCriteriaLabel') || 'Критерии оценки судьи (опционально):'),
774
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 10 } },
775
+ React.createElement('label', { style: { fontSize: 13, fontWeight: 500 } }, t('aggregator.criteria_label')),
635
776
  React.createElement('textarea', {
636
777
  className: 'moa-textarea',
637
778
  rows: 2,
638
- style: {
639
- width: '100%',
640
- boxSizing: 'border-box',
641
- padding: '8px 10px',
642
- borderRadius: 8,
643
- border: '1px solid var(--dsw-alias-border-l2)',
644
- background: 'var(--dsw-alias-bg-layer-3)',
645
- color: 'var(--dsw-alias-label-primary)',
646
- fontSize: 13,
647
- resize: 'vertical',
648
- outline: 'none',
649
- },
650
- placeholder: t('judgeCriteriaPlaceholder') || 'например: Приоритет — производительность, чистый код, минимум зависимостей...',
779
+ placeholder: t('aggregator.criteria_placeholder'),
651
780
  value: judgeCriteria,
652
781
  onChange: (e) => setJudgeCriteria(e.target.value),
653
782
  }),
654
783
  React.createElement(
655
784
  'div',
656
- { style: { marginTop: 8, display: 'flex', flexDirection: 'column', gap: 4 } },
785
+ { style: { marginTop: 6, display: 'flex', flexDirection: 'column', gap: 3 } },
657
786
  React.createElement(
658
787
  'label',
659
788
  { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
@@ -661,37 +790,141 @@ window.__ModuleLoader__.load({
661
790
  type: 'checkbox',
662
791
  checked: currentPreset.ask_clarifying_questions !== false,
663
792
  onChange: (e) => updateCurrentPreset((p) => ({ ...p, ask_clarifying_questions: e.target.checked })),
664
- style: { accentColor: 'var(--dsw-alias-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
793
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
665
794
  }),
666
- t('askQuestionsLabel') || 'Спрашивать уточняющие вопросы при неясных задачах (Опросник)'
795
+ t('aggregator.questions_label')
667
796
  ),
668
797
  React.createElement(
669
798
  'div',
670
- { className: 'moa-hint', style: { marginLeft: 22 } },
671
- t('askQuestionsHint') || 'Если включено: при коротких задачах судья сформирует опросник. Если выключено: MoA сразу переходит к генерации решения.'
799
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
800
+ t('aggregator.questions_hint')
801
+ ),
802
+ React.createElement(
803
+ 'label',
804
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
805
+ React.createElement('input', {
806
+ type: 'checkbox',
807
+ checked: Boolean(currentPreset.curator_synthesis),
808
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, curator_synthesis: e.target.checked })),
809
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
810
+ }),
811
+ t('aggregator.curator_label')
812
+ ),
813
+ React.createElement(
814
+ 'div',
815
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
816
+ t('aggregator.curator_hint')
817
+ ),
818
+ React.createElement(
819
+ 'label',
820
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, marginTop: 8 } },
821
+ React.createElement('input', {
822
+ type: 'checkbox',
823
+ checked: currentPreset.stream_aggregator !== false,
824
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, stream_aggregator: e.target.checked })),
825
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
826
+ }),
827
+ t('aggregator.stream_label')
828
+ ),
829
+ React.createElement(
830
+ 'div',
831
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
832
+ t('aggregator.stream_hint')
833
+ ),
834
+ /* Fallback Judges Chain */
835
+ React.createElement(
836
+ 'div',
837
+ { style: { marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 } },
838
+ React.createElement(
839
+ 'div',
840
+ { style: { fontSize: 13, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } },
841
+ t('aggregator.fallbacks_title')
842
+ ),
843
+ React.createElement(
844
+ 'div',
845
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginTop: -4 } },
846
+ t('aggregator.fallbacks_desc')
847
+ ),
848
+ (Array.isArray(currentPreset.aggregator_fallbacks) ? currentPreset.aggregator_fallbacks : []).map((fb, fbIdx) =>
849
+ React.createElement(
850
+ 'div',
851
+ { key: fbIdx, style: { display: 'flex', alignItems: 'center', gap: 8 } },
852
+ React.createElement('span', { className: 'moa-cand-badge' }, `Fallback #${fbIdx + 1}`),
853
+ React.createElement(
854
+ 'div',
855
+ { style: { flex: 1 } },
856
+ React.createElement(SearchableModelPicker, {
857
+ value: fb,
858
+ onChange: (val) => {
859
+ updateCurrentPreset((p) => {
860
+ const list = [...(p.aggregator_fallbacks || [])]
861
+ list[fbIdx] = val
862
+ return { ...p, aggregator_fallbacks: list }
863
+ })
864
+ },
865
+ allModels,
866
+ t,
867
+ })
868
+ ),
869
+ React.createElement(
870
+ 'button',
871
+ {
872
+ type: 'button',
873
+ className: 'moa-btn moa-btn-danger moa-btn-sub',
874
+ onClick: () => {
875
+ updateCurrentPreset((p) => ({
876
+ ...p,
877
+ aggregator_fallbacks: (p.aggregator_fallbacks || []).filter((_, i) => i !== fbIdx),
878
+ }))
879
+ },
880
+ },
881
+ '✕'
882
+ )
883
+ )
884
+ ),
885
+ React.createElement(
886
+ 'div',
887
+ null,
888
+ React.createElement(
889
+ 'button',
890
+ {
891
+ type: 'button',
892
+ className: 'moa-btn moa-btn-sub',
893
+ style: { marginTop: 4 },
894
+ onClick: () => {
895
+ updateCurrentPreset((p) => ({
896
+ ...p,
897
+ aggregator_fallbacks: [...(p.aggregator_fallbacks || []), { provider: 'opencode-go', model: 'deepseek-v4-flash' }],
898
+ }))
899
+ },
900
+ },
901
+ t('aggregator.add_fallback_btn')
902
+ )
903
+ )
672
904
  )
673
905
  )
674
906
  )
675
907
  ),
676
- /* Proposers / Advisors Box with Temperature Slider */
908
+
909
+ /* ── SECTION 3: CANDIDATE PROPOSERS ── */
677
910
  React.createElement(
678
911
  'div',
679
- { className: 'moa-box' },
912
+ { className: 'moa-section-card' },
680
913
  React.createElement(
681
914
  'div',
682
- { className: 'moa-box-title' },
683
- React.createElement('span', null, t('proposersTitle')),
915
+ { className: 'moa-section-title' },
916
+ React.createElement('span', null, t('proposers.title')),
684
917
  React.createElement(
685
918
  'div',
686
- { className: 'moa-count-bar' },
687
- React.createElement('span', { className: 'moa-hint' }, t('parallelCount') + ':'),
919
+ { className: 'moa-seg-group' },
920
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('proposers.parallel_count') + ':'),
688
921
  [2, 3, 4, 5].map((cnt) =>
689
922
  React.createElement(
690
923
  'button',
691
924
  {
692
925
  key: cnt,
693
926
  type: 'button',
694
- className: currentRefs.length === cnt ? 'moa-count-btn moa-count-btn-active' : 'moa-count-btn',
927
+ className: currentRefs.length === cnt ? 'moa-seg-btn moa-seg-btn-active moa-btn-sub' : 'moa-seg-btn moa-btn-sub',
695
928
  onClick: () => setReferenceCount(cnt),
696
929
  },
697
930
  cnt
@@ -699,18 +932,55 @@ window.__ModuleLoader__.load({
699
932
  )
700
933
  )
701
934
  ),
702
- React.createElement('div', { className: 'moa-hint' }, t('proposersDesc')),
935
+ React.createElement('div', { className: 'moa-section-desc' }, t('proposers.desc')),
703
936
  React.createElement(
704
937
  'div',
705
- { className: 'moa-refs-list' },
938
+ { 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)' } },
939
+ React.createElement(
940
+ 'label',
941
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500 } },
942
+ React.createElement('input', {
943
+ type: 'checkbox',
944
+ checked: Boolean(currentPreset.quorum_enabled),
945
+ onChange: (e) => updateCurrentPreset((p) => ({ ...p, quorum_enabled: e.target.checked })),
946
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
947
+ }),
948
+ t('proposers.quorum_label')
949
+ ),
950
+ React.createElement(
951
+ 'div',
952
+ { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)', marginLeft: 22 } },
953
+ t('proposers.quorum_hint')
954
+ ),
955
+ currentPreset.quorum_enabled && React.createElement(
956
+ 'div',
957
+ { style: { marginLeft: 22, display: 'flex', alignItems: 'center', gap: 8, marginTop: 4 } },
958
+ React.createElement('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, t('proposers.grace_label')),
959
+ React.createElement('input', {
960
+ type: 'number',
961
+ className: 'moa-input',
962
+ style: { width: 70, height: 28, padding: '0 6px', fontSize: 12 },
963
+ min: 2,
964
+ max: 60,
965
+ value: currentPreset.grace_period_sec ?? 10,
966
+ onChange: (e) => {
967
+ const val = parseInt(e.target.value, 10)
968
+ updateCurrentPreset((p) => ({ ...p, grace_period_sec: isNaN(val) ? 10 : val }))
969
+ },
970
+ })
971
+ )
972
+ ),
973
+ React.createElement(
974
+ 'div',
975
+ { className: 'moa-cand-list' },
706
976
  currentRefs.map((ref, idx) =>
707
977
  React.createElement(
708
978
  'div',
709
- { key: idx, className: 'moa-ref-row' },
710
- React.createElement('span', { className: 'moa-ref-badge' }, `${t('candidate')} #${idx + 1}:`),
979
+ { key: idx, className: 'moa-cand-row' },
980
+ React.createElement('span', { className: 'moa-cand-badge' }, `${t('proposers.candidate')} #${idx + 1}`),
711
981
  React.createElement(
712
982
  'div',
713
- { className: 'moa-ref-picker' },
983
+ { className: 'moa-cand-picker' },
714
984
  React.createElement(SearchableModelPicker, {
715
985
  provider: ref.provider || '',
716
986
  model: ref.model || '',
@@ -724,8 +994,8 @@ window.__ModuleLoader__.load({
724
994
  'button',
725
995
  {
726
996
  type: 'button',
727
- className: 'moa-btn-del',
728
- title: t('remove'),
997
+ className: 'moa-btn moa-btn-danger moa-btn-sub',
998
+ title: 'Remove',
729
999
  onClick: () => removeReference(idx),
730
1000
  },
731
1001
  '✕'
@@ -737,180 +1007,128 @@ window.__ModuleLoader__.load({
737
1007
  'button',
738
1008
  {
739
1009
  type: 'button',
740
- className: 'moa-btn-add',
1010
+ className: 'moa-btn',
1011
+ style: { alignSelf: 'flex-start' },
741
1012
  onClick: addReference,
742
1013
  },
743
- '+ ' + t('addCandidate')
1014
+ t('proposers.add_btn')
744
1015
  ),
745
1016
  React.createElement(
746
1017
  'div',
747
- { className: 'moa-field', style: { marginTop: 4, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 10 } },
1018
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, marginTop: 4, borderTop: '1px solid var(--dsw-alias-border-l2)', paddingTop: 10 } },
748
1019
  React.createElement(
749
1020
  'div',
750
- { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' } },
751
- React.createElement('span', { className: 'moa-label', style: { fontSize: 12 } }, t('refTempLabel') || 'Температура кандидатов (разнообразие решений):'),
752
- React.createElement('span', { style: { fontWeight: 600, fontSize: 13 } }, refTemp)
1021
+ { style: { display: 'flex', justifyContent: 'space-between', fontSize: 13, fontWeight: 500 } },
1022
+ React.createElement('span', null, t('proposers.temp_label')),
1023
+ React.createElement('span', { style: { color: 'var(--dsw-alias-label-secondary)', fontSize: 12 } }, t('proposers.temp_hint'))
753
1024
  ),
754
1025
  React.createElement(
755
1026
  'div',
756
- { className: 'moa-temp-ctrl' },
1027
+ { className: 'moa-slider-row' },
757
1028
  React.createElement('input', {
758
1029
  type: 'range',
759
- className: 'moa-temp-slider',
1030
+ className: 'moa-slider',
760
1031
  min: '0',
761
1032
  max: '2',
762
1033
  step: '0.05',
763
1034
  value: refTemp,
764
1035
  onChange: (e) => setReferenceTemp(e.target.value),
765
1036
  }),
766
- React.createElement('input', {
767
- type: 'number',
768
- className: 'moa-temp-input',
769
- min: '0',
770
- max: '2',
771
- step: '0.05',
772
- value: refTemp,
773
- onChange: (e) => setReferenceTemp(e.target.value),
774
- })
1037
+ React.createElement('div', { className: 'moa-slider-badge' }, refTemp)
1038
+ )
1039
+ )
1040
+ ),
1041
+
1042
+ /* ── SECTION 4: TELEMETRY & STATS SUMMARY ── */
1043
+ React.createElement(
1044
+ 'div',
1045
+ { className: 'moa-section-card' },
1046
+ React.createElement('div', { className: 'moa-section-title' }, t('stats.title')),
1047
+ React.createElement('div', { className: 'moa-section-desc' }, t('stats.desc')),
1048
+ React.createElement(
1049
+ 'div',
1050
+ { className: 'moa-grid-2' },
1051
+ React.createElement(
1052
+ 'div',
1053
+ { className: 'moa-stat-box' },
1054
+ React.createElement('div', { className: 'moa-stat-val' }, String(presets.length)),
1055
+ React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.preset_count'))
1056
+ ),
1057
+ React.createElement(
1058
+ 'div',
1059
+ { className: 'moa-stat-box' },
1060
+ React.createElement('div', { className: 'moa-stat-val' }, (stats.totalRuns === null || stats.totalRuns === undefined) ? '—' : String(stats.totalRuns)),
1061
+ React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.total_runs'))
775
1062
  ),
776
- React.createElement('div', { className: 'moa-hint' }, t('refTempHint') || 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.')
1063
+ React.createElement(
1064
+ 'div',
1065
+ { className: 'moa-stat-box' },
1066
+ React.createElement('div', { className: 'moa-stat-val' }, (stats.avgCostUsd === null || stats.avgCostUsd === undefined) ? '—' : `~\$${stats.avgCostUsd.toFixed(4)}`),
1067
+ React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.avg_cost'))
1068
+ ),
1069
+ React.createElement(
1070
+ 'div',
1071
+ { className: 'moa-stat-box' },
1072
+ React.createElement('div', { className: 'moa-stat-val' }, String(currentRefs.length)),
1073
+ React.createElement('div', { className: 'moa-stat-lbl' }, t('stats.active_models'))
1074
+ )
777
1075
  )
778
1076
  ),
779
- /* Save Footer */
1077
+
1078
+ /* ── ENABLED TOGGLE ── */
1079
+ React.createElement(
1080
+ 'label',
1081
+ { style: { display: 'flex', alignItems: 'center', gap: 8, cursor: 'pointer', fontSize: 13, fontWeight: 500, alignSelf: 'flex-start' } },
1082
+ React.createElement('input', {
1083
+ type: 'checkbox',
1084
+ checked: enabled !== false,
1085
+ onChange: (e) => setEnabled(e.target.checked),
1086
+ style: { accentColor: 'var(--dsw-alias-state-brand-primary, var(--dsw-alias-label-primary))', cursor: 'pointer' },
1087
+ }),
1088
+ t('config.enabled_label')
1089
+ ),
1090
+
1091
+ /* ── FOOTER ACTIONS & SAVE ── */
780
1092
  React.createElement(
781
1093
  'div',
782
- { className: 'moa-foot' },
783
- saveStatus && React.createElement('span', { className: 'moa-status-text' }, saveStatus),
1094
+ { style: { display: 'flex', alignItems: 'center', gap: 12, marginTop: 4 } },
784
1095
  React.createElement(
785
1096
  'button',
786
1097
  {
787
1098
  type: 'button',
788
- className: 'moa-save',
1099
+ className: 'moa-btn moa-btn-primary',
1100
+ style: { padding: '10px 24px', fontSize: 14, fontWeight: 600 },
789
1101
  onClick: handleSave,
790
1102
  },
791
- t('save')
792
- )
1103
+ saveStatus === t('actions.saving') ? t('actions.saving') : t('actions.save')
1104
+ ),
1105
+ React.createElement(
1106
+ 'button',
1107
+ {
1108
+ type: 'button',
1109
+ className: 'moa-btn',
1110
+ onClick: reload,
1111
+ },
1112
+ t('actions.retry')
1113
+ ),
1114
+ saveStatus &&
1115
+ React.createElement(
1116
+ 'span',
1117
+ {
1118
+ className: saveStatus.startsWith('Error') ? 'moa-alert-err' : 'moa-alert-ok',
1119
+ },
1120
+ saveStatus
1121
+ )
793
1122
  )
794
1123
  )
795
1124
  }
796
1125
 
797
- let rootCtx = null
798
-
799
- const en = {
800
- title: 'Mixture of Agents (MoA)',
801
- description: 'Run tasks with multiple parallel proposer models and a synthesizing judge model',
802
- slashMoa: 'Run task through Mixture of Agents (/moa <prompt>)',
803
- defaultPreset: 'Default Preset (used on /moa without arguments)',
804
- isDefault: 'default',
805
- presetSelectLabel: 'Active Preset to Configure:',
806
- createNewPreset: 'Create new preset...',
807
- makeDefault: 'Make Default',
808
- makeDefaultHint: 'Use this preset for /moa when no preset argument is given',
809
- deletePreset: 'Delete preset',
810
- delete: 'Delete',
811
- confirmDeletePreset: 'Are you sure you want to delete preset',
812
- newPresetPlaceholder: 'New preset name (e.g. fast-audit)',
813
- create: 'Create',
814
- cancel: 'Cancel',
815
- aggregatorTitle: 'Judge / Aggregator Model (who reviews and synthesizes)',
816
- aggregatorDesc: 'Evaluates candidate proposals, corrects hallucinations, and outputs the final unified solution.',
817
- temperature: 'Temperature',
818
- aggTempHint: 'Recommended 0.2 - 0.4 for rigorous verification and factual synthesis.',
819
- proposersTitle: 'Candidate Models (Advisors)',
820
- proposersDesc: 'Generates independent candidate answers in parallel to provide diverse perspectives.',
821
- refTempLabel: 'Candidates Temperature (solution diversity):',
822
- refTempHint: 'Recommended 0.6 - 0.8 for diverse creative hypotheses.',
823
- parallelCount: 'Parallel workers',
824
- candidate: 'Candidate',
825
- addCandidate: 'Add parallel model slot',
826
- remove: 'Remove',
827
- save: 'Save Changes',
828
- saved: 'Saved successfully',
829
- loading: 'Saving…',
830
- retry: 'Retry connection',
831
- unavailable: 'MoA service temporarily unavailable (reconnecting...)',
832
- searchPlaceholder: 'Search by model name or provider...',
833
- modelsFound: 'Found',
834
- of: 'of',
835
- clear: 'Clear',
836
- noModelsMatch: 'No models found matching query.',
837
- useCustom: 'Use custom',
838
- selectModel: 'Select model...',
839
- judgeCriteriaLabel: 'Custom Judge Evaluation Criteria (optional):',
840
- judgeCriteriaPlaceholder: 'e.g. Priority: performance, zero external dependencies, robust edge-case handling...',
841
- }
842
-
843
- const ru = {
844
- title: 'Mixture of Agents (MoA)',
845
- description: 'Запуск задач через ансамбль параллельных моделей-советников и модель-судью',
846
- slashMoa: 'Запустить задачу через архитектуру Mixture of Agents (/moa <prompt>)',
847
- defaultPreset: 'Основной пресет (при вызове /moa без параметров)',
848
- isDefault: 'основной',
849
- presetSelectLabel: 'Текущий пресет для настройки:',
850
- createNewPreset: 'Создать новый пресет...',
851
- makeDefault: 'Сделать основным',
852
- makeDefaultHint: 'Использовать этот пресет для /moa при вызове без параметров',
853
- deletePreset: 'Удалить пресет',
854
- delete: 'Удалить',
855
- confirmDeletePreset: 'Вы уверены, что хотите удалить пресет',
856
- newPresetPlaceholder: 'Название нового пресета (например: fast-audit)',
857
- create: 'Создать',
858
- cancel: 'Отмена',
859
- aggregatorTitle: 'Ведущая модель / Судья (кто проверяет и объединяет)',
860
- aggregatorDesc: 'Сопоставляет варианты кандидатов, устраняет ошибки и синтезирует итоговое решение.',
861
- temperature: 'Температура',
862
- aggTempHint: 'Рекомендуется 0.2 – 0.4 для строгой проверки и синтеза без выдумок.',
863
- proposersTitle: 'Модели-кандидаты (советники)',
864
- proposersDesc: 'Параллельно формируют независимые варианты решения одной задачи.',
865
- refTempLabel: 'Температура кандидатов (разнообразие решений):',
866
- refTempHint: 'Рекомендуется 0.6 – 0.8 для получения независимых и креативных вариантов.',
867
- parallelCount: 'Параллельных моделей',
868
- candidate: 'Кандидат',
869
- addCandidate: 'Добавить модель в параллель',
870
- remove: 'Удалить',
871
- save: 'Сохранить',
872
- saved: 'Успешно сохранено',
873
- loading: 'Сохранение…',
874
- retry: 'Повторить подключение',
875
- unavailable: 'Служба MoA временно недоступна (переподключение...)',
876
- searchPlaceholder: 'Поиск модели по названию или провайдеру...',
877
- modelsFound: 'Найдено',
878
- of: 'из',
879
- clear: 'Сбросить',
880
- noModelsMatch: 'Модели не найдены.',
881
- useCustom: 'Использовать кастомное',
882
- selectModel: 'Выберите модель...',
883
- judgeCriteriaLabel: 'Критерии оценки судьи (опционально):',
884
- judgeCriteriaPlaceholder: 'например: Приоритет — производительность, минимум зависимостей, обработка граничных случаев...',
885
- }
886
-
887
- function makeT(ctx, props) {
888
- return (key) => {
889
- if (props && typeof props.t === 'function') {
890
- try {
891
- const res = props.t(key)
892
- if (res && res !== key) return res
893
- } catch (_) {}
894
- }
895
- const effectiveCtx = (props && props.ctx) || ctx || rootCtx
896
- if (effectiveCtx && effectiveCtx.locale && typeof effectiveCtx.locale.bind === 'function') {
897
- try {
898
- const bound = effectiveCtx.locale.bind(NS)
899
- const res = bound(key)
900
- if (res && res !== key) return res
901
- } catch (_) {}
902
- }
903
- const curLocale = (effectiveCtx && effectiveCtx.locale && effectiveCtx.locale.getSnapshot && effectiveCtx.locale.getSnapshot().active) || 'ru'
904
- const dict = curLocale === 'en' ? en : ru
905
- return dict[key] || ru[key] || en[key] || key
906
- }
907
- }
908
-
909
1126
  function useMoASettings(props) {
910
- const t = makeT(rootCtx, props)
911
1127
  const effectiveCtx = (props && props.ctx) || rootCtx
1128
+ // English is canonical; prefer the core-provided translator for the
1129
+ // active UI language (the slot declares locale: NS), fall back to en.
1130
+ const t = typeof props?.t === 'function' ? props.t : makeT(en, null)
912
1131
 
913
- // Reactive binding to settingsScope if available
914
1132
  const scope = React.useMemo(() => {
915
1133
  if (!effectiveCtx) return undefined
916
1134
  const s = effectiveCtx.get ? effectiveCtx.get('settingsScope') : effectiveCtx.settingsScope
@@ -930,8 +1148,10 @@ window.__ModuleLoader__.load({
930
1148
  const [defaultPreset, setDefaultPreset] = React.useState('default')
931
1149
  const [availableModels, setAvailableModels] = React.useState([])
932
1150
  const [saveStatus, setSaveStatus] = React.useState('')
1151
+ const [enabled, setEnabled] = React.useState(true)
1152
+ const [hostStatus, setHostStatus] = React.useState('connecting')
1153
+ const [stats, setStats] = React.useState({ totalRuns: null, avgCostUsd: null })
933
1154
 
934
- // Sync from settingsScope snapshot when available
935
1155
  React.useEffect(() => {
936
1156
  if (!scope || !snapshot) return
937
1157
  const snapStatus = snapshot.status || 'ready'
@@ -939,6 +1159,7 @@ window.__ModuleLoader__.load({
939
1159
  const val = snapshot.value || {}
940
1160
  if (Array.isArray(val.presets)) setPresets(val.presets)
941
1161
  if (val.default_preset) setDefaultPreset(val.default_preset)
1162
+ if (typeof val.enabled === 'boolean') setEnabled(val.enabled)
942
1163
  setStatus('ready')
943
1164
  } else if (snapStatus === 'unavailable') {
944
1165
  setStatus('unavailable')
@@ -948,6 +1169,35 @@ window.__ModuleLoader__.load({
948
1169
  const reload = React.useCallback(() => {
949
1170
  if (!scope) setStatus('loading')
950
1171
 
1172
+ fetch('/dsh-moa/status', { cache: 'no-store' })
1173
+ .then((r) => r.json())
1174
+ .then((data) => {
1175
+ if (data && data.ok) {
1176
+ setHostStatus(data.enabled === false ? 'disabled' : 'online')
1177
+ } else {
1178
+ setHostStatus('offline')
1179
+ }
1180
+ })
1181
+ .catch(() => {
1182
+ setHostStatus('offline')
1183
+ })
1184
+
1185
+ fetch('/dsh-moa/history?limit=100', { cache: 'no-store' })
1186
+ .then((r) => r.json())
1187
+ .then((data) => {
1188
+ if (data && data.ok && Array.isArray(data.runs)) {
1189
+ const runs = data.runs
1190
+ const withCost = runs.filter((run) => typeof run.totalCostUsd === 'number')
1191
+ const avgCostUsd = withCost.length > 0
1192
+ ? withCost.reduce((acc, run) => acc + run.totalCostUsd, 0) / withCost.length
1193
+ : null
1194
+ setStats({ totalRuns: data.total || runs.length, avgCostUsd })
1195
+ }
1196
+ })
1197
+ .catch((err) => {
1198
+ console.warn('[dsh-moa] History fetch failed:', err)
1199
+ })
1200
+
951
1201
  fetch('/dsh-moa/presets', { cache: 'no-store' })
952
1202
  .then((r) => {
953
1203
  if (!r.ok) throw new Error(`HTTP ${r.status}`)
@@ -984,16 +1234,17 @@ window.__ModuleLoader__.load({
984
1234
  }, [reload])
985
1235
 
986
1236
  const handleSave = () => {
987
- setSaveStatus(t('loading'))
1237
+ setSaveStatus(t('actions.saving'))
988
1238
 
989
1239
  if (scope && typeof scope.set === 'function') {
990
1240
  Promise.all([
1241
+ scope.set('enabled', enabled),
991
1242
  scope.set('presets', presets),
992
1243
  scope.set('default_preset', defaultPreset),
993
1244
  ])
994
1245
  .then(() => {
995
- setSaveStatus(t('saved'))
996
- setTimeout(() => setSaveStatus(''), 2500)
1246
+ setSaveStatus(t('actions.saved'))
1247
+ setTimeout(() => setSaveStatus(''), 3000)
997
1248
  })
998
1249
  .catch((err) => {
999
1250
  console.warn('[dsh-moa] scope.set failed, trying REST fallback:', err)
@@ -1009,15 +1260,15 @@ window.__ModuleLoader__.load({
1009
1260
  fetch('/dsh-moa/presets', {
1010
1261
  method: 'POST',
1011
1262
  headers: { 'Content-Type': 'application/json' },
1012
- body: JSON.stringify({ presets, default_preset: defaultPreset }),
1263
+ body: JSON.stringify({ enabled, presets, default_preset: defaultPreset }),
1013
1264
  })
1014
1265
  .then((r) => r.json())
1015
1266
  .then((res) => {
1016
1267
  if (res && res.ok) {
1017
- setSaveStatus(t('saved'))
1268
+ setSaveStatus(t('actions.saved'))
1018
1269
  if (Array.isArray(res.presets)) setPresets(res.presets)
1019
1270
  if (res.defaultPreset) setDefaultPreset(res.defaultPreset)
1020
- setTimeout(() => setSaveStatus(''), 2500)
1271
+ setTimeout(() => setSaveStatus(''), 3000)
1021
1272
  } else {
1022
1273
  setSaveStatus('Error saving: ' + (res?.error || 'failed'))
1023
1274
  }
@@ -1035,92 +1286,44 @@ window.__ModuleLoader__.load({
1035
1286
  defaultPreset,
1036
1287
  setDefaultPreset,
1037
1288
  availableModels,
1289
+ enabled,
1290
+ setEnabled,
1291
+ hostStatus,
1292
+ stats,
1038
1293
  handleSave,
1039
1294
  saveStatus,
1040
1295
  reload,
1041
1296
  }
1042
1297
  }
1043
1298
 
1044
- function MoASection(props) {
1045
- ensureStyles()
1046
- const state = useMoASettings(props)
1047
-
1048
- return React.createElement(
1049
- 'div',
1050
- { className: 'moa-section' },
1051
- React.createElement('div', { className: 'moa-section-title' }, state.t('title')),
1052
- React.createElement('div', { className: 'moa-section-sub' }, state.t('description')),
1053
- state.status === 'loading'
1054
- ? React.createElement('div', { className: 'moa-hint' }, state.t('loading'))
1055
- : state.status === 'unavailable'
1056
- ? React.createElement(
1057
- 'div',
1058
- { className: 'moa-field' },
1059
- React.createElement('div', { className: 'moa-hint' }, state.t('unavailable')),
1060
- React.createElement(
1061
- 'button',
1062
- {
1063
- type: 'button',
1064
- className: 'moa-count-btn',
1065
- style: { width: 'fit-content', marginTop: 8 },
1066
- onClick: state.reload,
1067
- },
1068
- state.t('retry')
1069
- )
1070
- )
1071
- : React.createElement(MoAEditor, state)
1072
- )
1073
- }
1074
-
1075
-
1076
- // ── ERROR BOUNDARY ──────────────────────────────────────────────
1077
1299
  class MoAErrorBoundary extends React.Component {
1078
1300
  constructor(props) {
1079
1301
  super(props)
1080
- this.state = { hasError: false, error: null, errorInfo: null }
1302
+ this.state = { hasError: false, error: null }
1081
1303
  }
1082
1304
  static getDerivedStateFromError(error) {
1083
1305
  return { hasError: true, error }
1084
1306
  }
1085
1307
  componentDidCatch(error, errorInfo) {
1086
1308
  console.error('[dsh-moa] MoAErrorBoundary caught error:', error, errorInfo)
1087
- this.setState({ errorInfo })
1088
1309
  }
1089
1310
  render() {
1090
1311
  if (this.state.hasError) {
1091
1312
  return React.createElement(
1092
1313
  'div',
1093
1314
  {
1094
- style: {
1095
- padding: '16px',
1096
- borderRadius: '8px',
1097
- border: '1px solid #ef4444',
1098
- background: 'rgba(239,68,68,0.08)',
1099
- color: '#ef4444',
1100
- fontSize: '13px',
1101
- fontFamily: 'monospace',
1102
- whiteSpace: 'pre-wrap',
1103
- wordBreak: 'break-word',
1104
- },
1315
+ className: 'moa-alert-err',
1316
+ style: { margin: '12px 0', padding: '14px', borderRadius: '8px' },
1105
1317
  },
1106
- React.createElement('div', { style: { fontWeight: 700, marginBottom: 8 } }, '⚠ MoA Settings Error'),
1107
- React.createElement('div', null, String(this.state.error)),
1108
- this.state.errorInfo && React.createElement(
1109
- 'details',
1110
- { style: { marginTop: 8 } },
1111
- React.createElement('summary', null, 'Component stack'),
1112
- React.createElement('pre', { style: { fontSize: 11 } }, this.state.errorInfo.componentStack || '')
1113
- ),
1318
+ React.createElement('div', { style: { fontWeight: 600, marginBottom: '6px' } }, '⚠️ MoA UI Error:'),
1319
+ React.createElement('div', { style: { fontSize: '12px', wordBreak: 'break-all' } }, String(this.state.error?.message || this.state.error)),
1114
1320
  React.createElement(
1115
1321
  'button',
1116
1322
  {
1117
1323
  type: 'button',
1118
- style: {
1119
- marginTop: 12, padding: '6px 16px', border: '1px solid #ef4444',
1120
- background: 'transparent', color: '#ef4444', borderRadius: 6,
1121
- cursor: 'pointer', fontSize: 12,
1122
- },
1123
- onClick: () => this.setState({ hasError: false, error: null, errorInfo: null }),
1324
+ className: 'moa-btn',
1325
+ style: { marginTop: '10px', fontSize: '12px', padding: '4px 10px' },
1326
+ onClick: () => this.setState({ hasError: false, error: null }),
1124
1327
  },
1125
1328
  'Retry'
1126
1329
  )
@@ -1149,8 +1352,8 @@ window.__ModuleLoader__.load({
1149
1352
  React.createElement(
1150
1353
  'div',
1151
1354
  null,
1152
- React.createElement('div', { className: 'moa-title' }, state.t('title')),
1153
- React.createElement('div', { className: 'moa-sub' }, state.t('description'))
1355
+ React.createElement('div', { className: 'moa-title' }, '🧠 ' + (state.t('title') || TITLE)),
1356
+ React.createElement('div', { className: 'moa-sub' }, state.t('description') || SUBTITLE)
1154
1357
  ),
1155
1358
  React.createElement(Chevron, {
1156
1359
  className: open ? 'moa-chev moa-chev-open' : 'moa-chev',
@@ -1161,21 +1364,21 @@ window.__ModuleLoader__.load({
1161
1364
  'div',
1162
1365
  { className: 'moa-body' },
1163
1366
  state.status === 'loading'
1164
- ? React.createElement('div', { className: 'moa-hint' }, state.t('loading'))
1367
+ ? React.createElement('div', { style: { padding: '16px 0', color: 'var(--dsw-alias-label-secondary)', fontSize: 13 } }, 'Loading MoA settings…')
1165
1368
  : state.status === 'unavailable'
1166
1369
  ? React.createElement(
1167
1370
  'div',
1168
- { className: 'moa-field' },
1169
- React.createElement('div', { className: 'moa-hint' }, state.t('unavailable')),
1371
+ { style: { padding: '16px 0', display: 'flex', flexDirection: 'column', gap: 10 } },
1372
+ React.createElement('div', { className: 'moa-alert-err' }, 'MoA service temporarily unavailable'),
1170
1373
  React.createElement(
1171
1374
  'button',
1172
1375
  {
1173
1376
  type: 'button',
1174
- className: 'moa-count-btn',
1175
- style: { width: 'fit-content', marginTop: 8 },
1377
+ className: 'moa-btn',
1378
+ style: { width: 'fit-content' },
1176
1379
  onClick: state.reload,
1177
1380
  },
1178
- state.t('retry')
1381
+ state.t('actions.retry')
1179
1382
  )
1180
1383
  )
1181
1384
  : React.createElement(MoAEditor, state)
@@ -1186,14 +1389,13 @@ window.__ModuleLoader__.load({
1186
1389
  exports.inject = ['slots', 'locale', 'inputTriggers', 'settingsScope']
1187
1390
  exports.apply = function apply(ctx) {
1188
1391
  rootCtx = ctx
1189
- const t = makeT(ctx)
1392
+
1190
1393
  const addLocale = (locale, dictionary) => {
1191
1394
  try {
1192
1395
  if (ctx.locale && typeof ctx.locale.register === 'function') {
1193
1396
  return ctx.locale.register(NS, locale, dictionary)
1194
1397
  }
1195
1398
  } catch {
1196
- // Если язык уже занят (например, dsh-russian-lang) или зарегистрирован ранее — уступаем без ошибки
1197
1399
  return () => {}
1198
1400
  }
1199
1401
  return () => {}
@@ -1201,7 +1403,7 @@ window.__ModuleLoader__.load({
1201
1403
 
1202
1404
  if (typeof ctx.effect === 'function') {
1203
1405
  ctx.effect(() => {
1204
- const undo = [addLocale('en', en), addLocale('ru', ru)]
1406
+ const undo = [addLocale('en', en)]
1205
1407
  return () => {
1206
1408
  for (const off of undo) {
1207
1409
  if (typeof off === 'function') {
@@ -1212,10 +1414,8 @@ window.__ModuleLoader__.load({
1212
1414
  }, 'dsh-moa: dictionaries')
1213
1415
  } else {
1214
1416
  addLocale('en', en)
1215
- addLocale('ru', ru)
1216
1417
  }
1217
1418
 
1218
-
1219
1419
  if (ctx.slots) {
1220
1420
  const registerCard = () => {
1221
1421
  try {
@@ -1250,7 +1450,7 @@ window.__ModuleLoader__.load({
1250
1450
  trigger: '/',
1251
1451
  name: 'moa',
1252
1452
  order: 35,
1253
- description: t('slashMoa'),
1453
+ description: en['slash.desc'],
1254
1454
  candidates: (_session, req) => {
1255
1455
  if (req.position !== 'leading') return Promise.resolve([])
1256
1456
  const q = req.query.trim().toLowerCase()
@@ -1258,7 +1458,7 @@ window.__ModuleLoader__.load({
1258
1458
  return Promise.resolve([
1259
1459
  {
1260
1460
  name: 'moa',
1261
- description: t('slashMoa'),
1461
+ description: en['slash.desc'],
1262
1462
  },
1263
1463
  ])
1264
1464
  },
@@ -1283,3 +1483,4 @@ window.__ModuleLoader__.load({
1283
1483
  return module.exports
1284
1484
  },
1285
1485
  })
1486
+