File size: 24,002 Bytes
c44fab6
0bb4dfa
d38c4fe
a808caa
478df90
0bb4dfa
c44fab6
0bb4dfa
a808caa
 
d38c4fe
 
 
a808caa
d38c4fe
 
 
 
 
 
 
cdef1a0
 
 
 
 
d38c4fe
de1db91
d38c4fe
 
 
 
 
 
 
0bb4dfa
c44fab6
a808caa
 
c44fab6
 
 
0bb4dfa
 
cdef1a0
 
e661e91
 
 
 
 
c44fab6
 
 
 
0bb4dfa
 
 
 
 
 
64d902c
 
478df90
1ec996f
 
c44fab6
 
0bb4dfa
c44fab6
d38c4fe
cdef1a0
d38c4fe
 
 
e661e91
cdef1a0
d38c4fe
 
 
c44fab6
 
 
 
0bb4dfa
 
c44fab6
 
 
 
 
0bb4dfa
c44fab6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c44fab6
d38c4fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c44fab6
 
 
0bb4dfa
 
 
 
 
c44fab6
 
 
 
a808caa
d38c4fe
a808caa
 
 
 
 
 
 
 
 
 
0bb4dfa
a808caa
 
 
d38c4fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0bb4dfa
 
a808caa
d38c4fe
0bb4dfa
 
 
 
 
0239f6d
0bb4dfa
 
 
 
 
0239f6d
0bb4dfa
 
 
 
 
a808caa
e661e91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cdef1a0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d38c4fe
 
 
 
 
 
 
 
0bb4dfa
d38c4fe
 
0bb4dfa
d38c4fe
 
0bb4dfa
d38c4fe
 
 
 
 
 
 
 
 
0bb4dfa
64d902c
a808caa
de1db91
d38c4fe
 
 
 
de1db91
d38c4fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64d902c
1ec996f
a808caa
d38c4fe
1ec996f
 
a808caa
1ec996f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a808caa
1ec996f
c44fab6
 
 
d38c4fe
 
 
 
c44fab6
 
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
c44fab6
 
 
 
 
 
 
 
 
 
 
 
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c44fab6
0bb4dfa
 
c44fab6
0bb4dfa
 
c44fab6
 
0bb4dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c44fab6
 
 
 
 
 
 
d38c4fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e661e91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
import React, { useState, useMemo, useRef, useEffect } from 'react';
import {
  ChevronRight, ChevronDown, Settings2, Search, Sun, Moon,
  Square, CheckSquare, UserPlus, ScrollText, SlidersHorizontal,
  BookOpen,
} from 'lucide-react';

/**
 * Settings menu (gear-icon dropdown in the header).
 *
 * Layout, top-to-bottom. Multi-item categories are *collapsible
 * accordions* that default to closed; single-item categories stay
 * inline beneath their small uppercase label.
 *
 *   - Theme               (single item: Sun/Moon toggle)
 *   - Model Selection     (accordion — merges what used to be the
 *                          separate "Models" and "Participants"
 *                          categories: Orchestrator model, Summarizer
 *                          model, Create Expert Persona, then one
 *                          stacked row per active participant)
 *   - Max participants    (single item: - / value / + stepper, 3-9)
 *   - Response priority   (accordion — Prioritize model choice vs.
 *                          conversation speed; under "speed", the
 *                          orchestrator races the chosen model
 *                          against a fast fallback and aggressively
 *                          substitutes failed LLMs)
 *   - Display options     (accordion — two toggles)
 *   - View Prompts        (accordion — Credential Summary, Prompt
 *                          Catalog)
 *   - Advanced            (single item: Conversation limits…)
 *
 * The Downloads section that previously lived at the bottom of this
 * panel has been removed; every item it offered is already reachable
 * from the header DownloadMenu, so duplicating them here just made the
 * settings menu unnecessarily long.
 */
export default function DevMenu({
  theme,
  onToggleTheme,
  allModels,
  orchestratorModel,
  onOrchestratorChange,
  summarizerModel,
  onSummarizerChange,
  speedPriority,
  onSpeedPriorityChange,
  conversationFormats,
  conversationStructureId,
  onConversationStructureChange,
  decisionMethodId,
  onDecisionMethodChange,
  showResponseTime,
  onShowResponseTimeChange,
  showChatStats,
  onShowChatStatsChange,
  maxParticipants,
  onMaxParticipantsChange,
  participants,
  modelAssignments,
  onModelAssignmentChange,
  onOpenExpertModal,
  onShowCredentials,
  hasCredentials,
  onShowPromptCatalog,
  onShowConversationLimits,
  conversationLimitsOverridden,
}) {
  const [open, setOpen] = useState(false);
  const [activeSub, setActiveSub] = useState(null); // null | "orch" | "sum" | <participant_id>
  const [q, setQ] = useState('');
  // Collapsed-by-default accordions. Keys correspond to the section
  // ids the SectionHeader below toggles. If we ever add a fifth
  // multi-item category, just add a key here.
  const [openSections, setOpenSections] = useState({
    modelSel: false,
    conversationFormat: false,
    responsePriority: false,
    display: false,
    transparency: false,
  });
  const wrapRef = useRef(null);
  const searchRef = useRef(null);

  useEffect(() => {
    if (activeSub && searchRef.current) searchRef.current.focus();
  }, [activeSub]);

  useEffect(() => {
    function handleClickOutside(e) {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) {
        setOpen(false);
        setActiveSub(null);
        setQ('');
      }
    }
    document.addEventListener('mousedown', handleClickOutside);
    return () => document.removeEventListener('mousedown', handleClickOutside);
  }, []);

  const filtered = useMemo(() => {
    const s = q.trim().toLowerCase();
    if (!s) return allModels;
    return allModels.filter(row => {
      const hay = `${row.name} ${row.id} ${row.provider || ''}`.toLowerCase();
      return hay.includes(s);
    });
  }, [allModels, q]);

  const nameForModel = (id) => {
    if (!id) return null;
    const m = allModels.find(x => x.id === id);
    return m ? m.name : id;
  };
  const orchName = nameForModel(orchestratorModel) || 'Default (backend)';
  const sumName = summarizerModel
    ? (nameForModel(summarizerModel) || summarizerModel)
    : 'Same as Orchestrator';

  const onPickForSubject = (id, subject) => {
    if (subject === 'orch') onOrchestratorChange(id);
    else if (subject === 'sum') onSummarizerChange(id);
    else if (subject) onModelAssignmentChange(subject, id);
  };

  // Toggle a multi-item accordion. Closing the Model Selection section
  // while a model sub-panel is open would leave the sub-panel orphaned
  // (its anchor row is no longer rendered), so we also clear activeSub
  // in that case.
  const toggleSection = (id) => {
    setOpenSections(prev => {
      const next = { ...prev, [id]: !prev[id] };
      if (id === 'modelSel' && !next.modelSel) {
        setActiveSub(null);
        setQ('');
      }
      return next;
    });
  };

  const modelSelOpen = openSections.modelSel;

  return (
    <div className="dev-wrap" ref={wrapRef}>
      <div className="dev-dropdown-header">
        <button
          className="icon-btn"
          onClick={() => { setOpen(o => !o); setActiveSub(null); setQ(''); }}
          title="Settings"
        >
          <Settings2 size={16} />
        </button>
        {open && (
          <div className="dev-panel">

            {/* ── Theme (single item) ─────────────────────────────── */}
            <div className="dev-panel-label">Theme</div>
            <button
              className="dev-panel-row"
              onClick={onToggleTheme}
              title="Toggle light/dark mode"
            >
              {theme === 'light'
                ? <Moon size={14} className="dev-check-icon" />
                : <Sun size={14} className="dev-check-icon" />}
              {theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
            </button>

            <div className="dev-panel-divider" />

            {/* ── Model Selection (accordion: merged Models + Participants) ─ */}
            <SectionHeader
              label="Model Selection"
              open={modelSelOpen}
              onToggle={() => toggleSection('modelSel')}
            />
            {modelSelOpen && (
              <>
                <button
                  className="dev-panel-row dev-panel-row-stack"
                  onClick={() => { setActiveSub(s => s === 'orch' ? null : 'orch'); setQ(''); }}
                >
                  <div className="dev-panel-row-text">
                    <div className="dev-panel-row-name">Orchestrator model…</div>
                    <div className="dev-panel-row-sub">{orchName}</div>
                  </div>
                  <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
                </button>
                <button
                  className="dev-panel-row dev-panel-row-stack"
                  onClick={() => { setActiveSub(s => s === 'sum' ? null : 'sum'); setQ(''); }}
                >
                  <div className="dev-panel-row-text">
                    <div className="dev-panel-row-name">Summarizer model…</div>
                    <div className="dev-panel-row-sub">{sumName}</div>
                  </div>
                  <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
                </button>
                <button
                  className="dev-panel-row"
                  onClick={() => { onOpenExpertModal(null); setOpen(false); }}
                >
                  <UserPlus size={14} className="dev-check-icon" />
                  Create Expert Persona…
                </button>
                {(participants || []).map(p => {
                  const assigned = modelAssignments[p.participant_id];
                  const labelName = assigned ? nameForModel(assigned)
                    : (p.default_model_id ? nameForModel(p.default_model_id) : '(default)');
                  return (
                    <button
                      key={p.participant_id}
                      className="dev-panel-row dev-panel-row-stack"
                      onClick={() => { setActiveSub(s => s === p.participant_id ? null : p.participant_id); setQ(''); }}
                    >
                      <div className="dev-panel-row-text">
                        <div className="dev-panel-row-name">{p.name}</div>
                        <div className="dev-panel-row-sub">{labelName}</div>
                      </div>
                      <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
                    </button>
                  );
                })}
              </>
            )}

            <div className="dev-panel-divider" />

            {/* ── Max participants (single item) ─────────────────── */}
            <div className="dev-panel-label">Max participants ({maxParticipants})</div>
            <div className="ccai-stepper-row">
              <button
                className="btn-sm btn-outline ccai-stepper-btn"
                disabled={maxParticipants <= 3}
                onClick={() => onMaxParticipantsChange(Math.max(3, maxParticipants - 1))}
              >−</button>
              <div className="ccai-stepper-val">{maxParticipants}</div>
              <button
                className="btn-sm btn-outline ccai-stepper-btn"
                disabled={maxParticipants >= 9}
                onClick={() => onMaxParticipantsChange(Math.min(9, maxParticipants + 1))}
              >+</button>
              <span className="dev-panel-hint">3-9</span>
            </div>

            <div className="dev-panel-divider" />

            {/* ── Conversation format (accordion) ───────────────── */}
            {/* Two mutually-exclusive picker lists. The catalog is
                served by /api/chat/conversation-formats so adding a
                new structure or decision-method plugin server-side
                doesn't need a frontend code change. */}
            <SectionHeader
              label="Conversation format"
              open={openSections.conversationFormat}
              onToggle={() => toggleSection('conversationFormat')}
            />
            {openSections.conversationFormat && (
              <ConversationFormatPicker
                catalog={conversationFormats}
                structureId={conversationStructureId}
                onStructureChange={onConversationStructureChange}
                decisionId={decisionMethodId}
                onDecisionChange={onDecisionMethodChange}
              />
            )}

            <div className="dev-panel-divider" />

            {/* ── Response priority (accordion) ──────────────────── */}
            {/* Two mutually-exclusive choices. Under "Prioritize
                conversation speed" the backend also races the chosen
                model against a fast fallback and aggressively
                substitutes failed LLMs (see backend/app/services/
                resilience.py). Under "Prioritize model choice" the
                user's selection is preserved and a failed turn just
                gets noted in chat. */}
            <SectionHeader
              label="Response priority"
              open={openSections.responsePriority}
              onToggle={() => toggleSection('responsePriority')}
            />
            {openSections.responsePriority && (
              <>
                <button
                  className={`dev-panel-choice ${!speedPriority ? 'dev-panel-choice-active' : ''}`}
                  onClick={() => onSpeedPriorityChange?.(false)}
                  title={
                    "Use the participant model you picked, and don't "
                    + "swap models in mid-chat if one is slow or fails."
                  }
                >
                  {!speedPriority
                    ? <CheckSquare size={16} className="dev-check-icon" />
                    : <Square size={16} className="dev-check-icon" />}
                  Prioritize model choice
                </button>
                <button
                  className={`dev-panel-choice ${speedPriority ? 'dev-panel-choice-active' : ''}`}
                  onClick={() => onSpeedPriorityChange?.(true)}
                  title={
                    "Race the chosen model against a fast fallback "
                    + "after 5s, and substitute another LLM behind the "
                    + "persona if the chosen one fails outright."
                  }
                >
                  {speedPriority
                    ? <CheckSquare size={16} className="dev-check-icon" />
                    : <Square size={16} className="dev-check-icon" />}
                  Prioritize conversation speed
                </button>
              </>
            )}

            <div className="dev-panel-divider" />

            {/* ── Display options (accordion) ────────────────────── */}
            <SectionHeader
              label="Display options"
              open={openSections.display}
              onToggle={() => toggleSection('display')}
            />
            {openSections.display && (
              <>
                <button
                  className={`dev-panel-choice ${showResponseTime ? 'dev-panel-choice-active' : ''}`}
                  onClick={() => onShowResponseTimeChange(!showResponseTime)}
                >
                  {showResponseTime ? <CheckSquare size={16} className="dev-check-icon" /> : <Square size={16} className="dev-check-icon" />}
                  Response times on messages
                </button>
                <button
                  className={`dev-panel-choice ${showChatStats ? 'dev-panel-choice-active' : ''}`}
                  onClick={() => onShowChatStatsChange(!showChatStats)}
                >
                  {showChatStats ? <CheckSquare size={16} className="dev-check-icon" /> : <Square size={16} className="dev-check-icon" />}
                  Chat stats after end
                </button>
              </>
            )}

            <div className="dev-panel-divider" />

            {/* ── View Prompts (accordion) ───────────────────────── */}
            {/* No right-side chevrons on the rows themselves: these
                buttons open a modal and don't expand a sub-panel, so a
                row-level chevron would be misleading. */}
            <SectionHeader
              label="View Prompts"
              open={openSections.transparency}
              onToggle={() => toggleSection('transparency')}
            />
            {openSections.transparency && (
              <>
                <button
                  className="dev-panel-row"
                  disabled={!hasCredentials}
                  onClick={() => { onShowCredentials?.(); setOpen(false); }}
                  title={
                    hasCredentials
                      ? "View the orchestrator's per-participant Credential Summary"
                      : "Credential Summary is built after Phase 1 (initial opinions). Start a chat first."
                  }
                >
                  <ScrollText size={14} className="dev-check-icon" />
                  View Credential Summary…
                </button>
                <button
                  className="dev-panel-row"
                  onClick={() => { onShowPromptCatalog?.(); setOpen(false); }}
                  title="View every prompt template the orchestrator and participants use, grouped by phase."
                >
                  <BookOpen size={14} className="dev-check-icon" />
                  View current chat prompts…
                </button>
              </>
            )}

            <div className="dev-panel-divider" />

            {/* ── Advanced (single item) ─────────────────────────── */}
            <div className="dev-panel-label">Advanced</div>
            <button
              className="dev-panel-row"
              onClick={() => { onShowConversationLimits?.(); setOpen(false); }}
              title="View and adjust the per-phase repetition limits and failsafe pause points."
            >
              <SlidersHorizontal size={14} className="dev-check-icon" />
              Conversation limits…
              {conversationLimitsOverridden && (
                <span
                  title="One or more limits are overridden from the defaults"
                  style={{
                    marginLeft: 6,
                    fontSize: 11,
                    color: 'var(--text-secondary)',
                  }}
                >
                  (custom)
                </span>
              )}
              <ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5, flexShrink: 0 }} />
            </button>
          </div>
        )}

        {/* Model picker sub-panel — only meaningful while the Model
            Selection accordion is open, since that's the only section
            whose rows set activeSub. */}
        {open && modelSelOpen && activeSub && (
          <div className="dev-sub-panel">
            <div className="dev-sub-header">
              <span className="dev-sub-title">
                {activeSub === 'orch' && 'Orchestrator model'}
                {activeSub === 'sum' && 'Summarizer model'}
                {activeSub !== 'orch' && activeSub !== 'sum' && (
                  <>Model for {participants.find(p => p.participant_id === activeSub)?.name || activeSub}</>
                )}
              </span>
              <span className="dev-sub-current">
                {activeSub === 'orch' && orchName}
                {activeSub === 'sum' && sumName}
                {activeSub !== 'orch' && activeSub !== 'sum' && (
                  nameForModel(modelAssignments[activeSub]) || '(default)'
                )}
              </span>
            </div>
            <div className="dev-sub-search">
              <Search size={14} className="dev-sub-search-icon" />
              <input
                ref={searchRef}
                type="search"
                placeholder="Search models…"
                value={q}
                onChange={e => setQ(e.target.value)}
              />
            </div>
            <ul className="dev-sub-list">
              {activeSub === 'sum' && (
                <li>
                  <button
                    className={`dev-sub-item ${!summarizerModel ? 'dev-sub-item-active' : ''}`}
                    onClick={() => { onPickForSubject(null, 'sum'); setActiveSub(null); setQ(''); }}
                  >
                    <strong>Same as Orchestrator (default)</strong>
                    <span className="dev-sub-provider">Use whichever model is currently the orchestrator</span>
                  </button>
                </li>
              )}
              {activeSub === 'orch' && (
                <li>
                  <button
                    className={`dev-sub-item ${!orchestratorModel ? 'dev-sub-item-active' : ''}`}
                    onClick={() => { onPickForSubject(null, 'orch'); setActiveSub(null); setQ(''); }}
                  >
                    <strong>Default (backend)</strong>
                    <span className="dev-sub-provider">Use server default</span>
                  </button>
                </li>
              )}
              {activeSub !== 'orch' && activeSub !== 'sum' && (
                <li>
                  <button
                    className={`dev-sub-item ${!modelAssignments[activeSub] ? 'dev-sub-item-active' : ''}`}
                    onClick={() => { onPickForSubject(null, activeSub); setActiveSub(null); setQ(''); }}
                  >
                    <strong>(persona default)</strong>
                    <span className="dev-sub-provider">Use the persona's bundled or saved default</span>
                  </button>
                </li>
              )}
              {filtered.map(m => {
                const currentId =
                  activeSub === 'orch' ? orchestratorModel
                    : activeSub === 'sum' ? summarizerModel
                    : modelAssignments[activeSub];
                return (
                  <li key={m.id}>
                    <button
                      className={`dev-sub-item ${currentId === m.id ? 'dev-sub-item-active' : ''}`}
                      onClick={() => { onPickForSubject(m.id, activeSub); setActiveSub(null); setQ(''); }}
                    >
                      <strong>{m.name}</strong>
                      <span className="dev-sub-provider">{m.provider}</span>
                    </button>
                  </li>
                );
              })}
            </ul>
          </div>
        )}
      </div>
    </div>
  );
}

/**
 * Clickable section header for the multi-item categories. Visually
 * matches the existing uppercase `dev-panel-label` style, but is a
 * button with a chevron that flips when the section is open.
 */
function SectionHeader({ label, open, onToggle }) {
  return (
    <button
      type="button"
      className={`dev-panel-section-header ${open ? 'dev-panel-section-header-open' : ''}`}
      onClick={onToggle}
      aria-expanded={open}
    >
      <span>{label}</span>
      {open
        ? <ChevronDown size={12} className="dev-panel-section-chevron" />
        : <ChevronRight size={12} className="dev-panel-section-chevron" />}
    </button>
  );
}


/**
 * Two stacked radio-style pickers for the conversation structure and
 * decision-making method. Driven entirely by the server catalog so
 * adding a plugin doesn't need a code change here. A null current
 * selection means "follow the backend's default" — we highlight that
 * default but the explicit user choice always wins when set.
 */
function ConversationFormatPicker({
  catalog,
  structureId, onStructureChange,
  decisionId, onDecisionChange,
}) {
  const structures = Array.isArray(catalog?.structures) ? catalog.structures : [];
  const decisions = Array.isArray(catalog?.decisions) ? catalog.decisions : [];
  const defStruct = catalog?.default_structure_id || null;
  const defDec = catalog?.default_decision_id || null;
  const effectiveStruct = structureId || defStruct;
  const effectiveDec = decisionId || defDec;

  return (
    <>
      <div className="dev-panel-label dev-panel-sublabel">Discussion structure</div>
      {structures.length === 0 && (
        <div className="dev-panel-hint" style={{ padding: '4px 10px' }}>
          (catalog unavailable)
        </div>
      )}
      {structures.map(s => (
        <button
          key={s.id}
          className={`dev-panel-choice ${effectiveStruct === s.id ? 'dev-panel-choice-active' : ''}`}
          onClick={() => onStructureChange?.(s.id)}
          title={s.description || ''}
        >
          {effectiveStruct === s.id
            ? <CheckSquare size={16} className="dev-check-icon" />
            : <Square size={16} className="dev-check-icon" />}
          {s.name}
        </button>
      ))}

      <div className="dev-panel-label dev-panel-sublabel" style={{ marginTop: 6 }}>
        Decision method
      </div>
      {decisions.length === 0 && (
        <div className="dev-panel-hint" style={{ padding: '4px 10px' }}>
          (catalog unavailable)
        </div>
      )}
      {decisions.map(d => (
        <button
          key={d.id}
          className={`dev-panel-choice ${effectiveDec === d.id ? 'dev-panel-choice-active' : ''}`}
          onClick={() => onDecisionChange?.(d.id)}
          title={d.description || ''}
        >
          {effectiveDec === d.id
            ? <CheckSquare size={16} className="dev-check-icon" />
            : <Square size={16} className="dev-check-icon" />}
          {d.name}
        </button>
      ))}
    </>
  );
}