NeonClary Cursor commited on
Commit
d38c4fe
·
1 Parent(s): 53edcb8

refactor(ui): shorten Settings menu — accordions + Model Selection merge

Browse files

The Settings panel had grown long enough that scanning it required
scrolling. Three changes to make it manageable:

- Drop the Downloads section at the bottom. Every item it offered
(chat .txt, chat .md, summary .csv) is already in the header
DownloadMenu; duplicating them here was just noise. Removed the
corresponding props from DevMenu and from the Header forwarding.
- Convert each multi-item category (Model Selection, Display options,
Transparency) into a collapsible accordion that defaults to closed.
Added a new SectionHeader subcomponent and matching
.dev-panel-section-header CSS that mimics the existing
.dev-panel-label styling so collapsed sections still read as plain
uppercase labels. Single-item categories (Theme, Max participants,
Advanced) stay inline since collapsing a one-item section would
just add a click without saving any vertical space.
- Merge the old "Models" and "Participants" categories into a single
"Model Selection" accordion containing Orchestrator model,
Summarizer model, Create Expert Persona, and the per-participant
rows. Closing the accordion while a model sub-panel is open also
clears activeSub so the picker doesn't float without its anchor row.

Co-authored-by: Cursor <cursoragent@cursor.com>

frontend/src/components/DevMenu.js CHANGED
@@ -1,6 +1,6 @@
1
  import React, { useState, useMemo, useRef, useEffect } from 'react';
2
  import {
3
- ChevronRight, Download, Settings2, Search, Sun, Moon,
4
  Square, CheckSquare, UserPlus, ScrollText, SlidersHorizontal,
5
  BookOpen,
6
  } from 'lucide-react';
@@ -8,28 +8,26 @@ import {
8
  /**
9
  * Settings menu (gear-icon dropdown in the header).
10
  *
11
- * Layout, top-to-bottom, all category items indented under the small
12
- * uppercase label that introduces them:
13
- * - Theme (Sun / Moon toggle)
14
- * - Models (Orchestrator / Summarizer — stacked rows
15
- * with the currently-chosen model name on a
16
- * second line, ellipsis-truncated)
17
- * - Max participants (- / value / + stepper, 3-9)
18
- * - Participants (Create Expert Persona…, then per-participant
19
- * rows in the same stacked-row style)
20
- * - Display options (toggles)
21
- * - Transparency (Credential Summary, Prompt Catalog — no
22
- * right-side chevrons; just the labelled
23
- * buttons)
24
- * - Advanced (Conversation limits…)
25
- * - Downloads (chat .txt / .md / summary .csv — same items
26
- * are also reachable from the header
27
- * DownloadMenu; the "Full API history" item
28
- * is *only* in DownloadMenu)
29
  *
30
- * The header download strip that used to live in this component has
31
- * moved into a sibling DownloadMenu component, so this file no longer
32
- * renders any header-bar buttons; just the gear and its panel.
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  */
34
  export default function DevMenu({
35
  theme,
@@ -54,14 +52,18 @@ export default function DevMenu({
54
  onShowPromptCatalog,
55
  onShowConversationLimits,
56
  conversationLimitsOverridden,
57
- onDownloadChatTxt,
58
- onDownloadChatMd,
59
- onDownloadCsvTable,
60
- hasChat,
61
  }) {
62
  const [open, setOpen] = useState(false);
63
  const [activeSub, setActiveSub] = useState(null); // null | "orch" | "sum" | <participant_id>
64
  const [q, setQ] = useState('');
 
 
 
 
 
 
 
 
65
  const wrapRef = useRef(null);
66
  const searchRef = useRef(null);
67
 
@@ -106,6 +108,23 @@ export default function DevMenu({
106
  else if (subject) onModelAssignmentChange(subject, id);
107
  };
108
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  return (
110
  <div className="dev-wrap" ref={wrapRef}>
111
  <div className="dev-dropdown-header">
@@ -119,7 +138,7 @@ export default function DevMenu({
119
  {open && (
120
  <div className="dev-panel">
121
 
122
- {/* ── Theme ─────────────────────────────────────────── */}
123
  <div className="dev-panel-label">Theme</div>
124
  <button
125
  className="dev-panel-row"
@@ -134,32 +153,65 @@ export default function DevMenu({
134
 
135
  <div className="dev-panel-divider" />
136
 
137
- {/* ── Models ────────────────────────────────────────── */}
138
- <div className="dev-panel-label">Models</div>
139
- <button
140
- className="dev-panel-row dev-panel-row-stack"
141
- onClick={() => { setActiveSub(s => s === 'orch' ? null : 'orch'); setQ(''); }}
142
- >
143
- <div className="dev-panel-row-text">
144
- <div className="dev-panel-row-name">Orchestrator model…</div>
145
- <div className="dev-panel-row-sub">{orchName}</div>
146
- </div>
147
- <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
148
- </button>
149
- <button
150
- className="dev-panel-row dev-panel-row-stack"
151
- onClick={() => { setActiveSub(s => s === 'sum' ? null : 'sum'); setQ(''); }}
152
- >
153
- <div className="dev-panel-row-text">
154
- <div className="dev-panel-row-name">Summarizer model…</div>
155
- <div className="dev-panel-row-sub">{sumName}</div>
156
- </div>
157
- <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
158
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  <div className="dev-panel-divider" />
161
 
162
- {/* ── Max participants ──────────────────────────────── */}
163
  <div className="dev-panel-label">Max participants ({maxParticipants})</div>
164
  <div className="ccai-stepper-row">
165
  <button
@@ -178,86 +230,71 @@ export default function DevMenu({
178
 
179
  <div className="dev-panel-divider" />
180
 
181
- {/* ── Participants ──────────────────────────────────── */}
182
- <div className="dev-panel-label">Participants</div>
183
- <button
184
- className="dev-panel-row"
185
- onClick={() => { onOpenExpertModal(null); setOpen(false); }}
186
- >
187
- <UserPlus size={14} className="dev-check-icon" />
188
- Create Expert Persona…
189
- </button>
190
- {(participants || []).map(p => {
191
- const assigned = modelAssignments[p.participant_id];
192
- const labelName = assigned ? nameForModel(assigned)
193
- : (p.default_model_id ? nameForModel(p.default_model_id) : '(default)');
194
- return (
195
  <button
196
- key={p.participant_id}
197
- className="dev-panel-row dev-panel-row-stack"
198
- onClick={() => { setActiveSub(s => s === p.participant_id ? null : p.participant_id); setQ(''); }}
199
  >
200
- <div className="dev-panel-row-text">
201
- <div className="dev-panel-row-name">{p.name}</div>
202
- <div className="dev-panel-row-sub">{labelName}</div>
203
- </div>
204
- <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
205
  </button>
206
- );
207
- })}
208
-
209
- <div className="dev-panel-divider" />
210
-
211
- {/* ── Display options ───────────────────────────────── */}
212
- <div className="dev-panel-label">Display options</div>
213
- <button
214
- className={`dev-panel-choice ${showResponseTime ? 'dev-panel-choice-active' : ''}`}
215
- onClick={() => onShowResponseTimeChange(!showResponseTime)}
216
- >
217
- {showResponseTime ? <CheckSquare size={16} className="dev-check-icon" /> : <Square size={16} className="dev-check-icon" />}
218
- Response times on messages
219
- </button>
220
- <button
221
- className={`dev-panel-choice ${showChatStats ? 'dev-panel-choice-active' : ''}`}
222
- onClick={() => onShowChatStatsChange(!showChatStats)}
223
- >
224
- {showChatStats ? <CheckSquare size={16} className="dev-check-icon" /> : <Square size={16} className="dev-check-icon" />}
225
- Chat stats after end
226
- </button>
227
 
228
  <div className="dev-panel-divider" />
229
 
230
- {/* ── Transparency ──────────────────────────────────── */}
231
- {/* No right-side chevrons here: these buttons trigger a
232
- modal and don't expand a sub-panel, so the chevron the
233
- old layout had was misleading and (per user feedback)
234
- wrapped onto a noisy second line. */}
235
- <div className="dev-panel-label">Transparency</div>
236
- <button
237
- className="dev-panel-row"
238
- disabled={!hasCredentials}
239
- onClick={() => { onShowCredentials?.(); setOpen(false); }}
240
- title={
241
- hasCredentials
242
- ? "View the orchestrator's per-participant Credential Summary"
243
- : "Credential Summary is built after Phase 1 (initial opinions). Start a chat first."
244
- }
245
- >
246
- <ScrollText size={14} className="dev-check-icon" />
247
- View Credential Summary…
248
- </button>
249
- <button
250
- className="dev-panel-row"
251
- onClick={() => { onShowPromptCatalog?.(); setOpen(false); }}
252
- title="View every prompt template the orchestrator and participants use, grouped by phase."
253
- >
254
- <BookOpen size={14} className="dev-check-icon" />
255
- View current chat prompts…
256
- </button>
 
 
 
 
 
 
 
257
 
258
  <div className="dev-panel-divider" />
259
 
260
- {/* ── Advanced ──────────────────────────────────────── */}
261
  <div className="dev-panel-label">Advanced</div>
262
  <button
263
  className="dev-panel-row"
@@ -280,43 +317,13 @@ export default function DevMenu({
280
  )}
281
  <ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5, flexShrink: 0 }} />
282
  </button>
283
-
284
- <div className="dev-panel-divider" />
285
-
286
- {/* ── Downloads ─────────────────────────────────────── */}
287
- {/* These three are intentional duplicates of items in the
288
- header DownloadMenu. The "Full API history" item lives
289
- only in DownloadMenu (per UX request) and is therefore
290
- not listed here. */}
291
- <div className="dev-panel-label">Downloads</div>
292
- <button
293
- className="dev-panel-row"
294
- disabled={!hasChat}
295
- onClick={() => { onDownloadChatTxt(); setOpen(false); }}
296
- >
297
- <Download size={14} className="dev-check-icon" />
298
- Chat as .txt
299
- </button>
300
- <button
301
- className="dev-panel-row"
302
- disabled={!hasChat}
303
- onClick={() => { onDownloadChatMd(); setOpen(false); }}
304
- >
305
- <Download size={14} className="dev-check-icon" />
306
- Chat as .md
307
- </button>
308
- <button
309
- className="dev-panel-row"
310
- disabled={!hasChat}
311
- onClick={() => { onDownloadCsvTable(); setOpen(false); }}
312
- >
313
- <Download size={14} className="dev-check-icon" />
314
- Summary table as .csv
315
- </button>
316
  </div>
317
  )}
318
 
319
- {open && activeSub && (
 
 
 
320
  <div className="dev-sub-panel">
321
  <div className="dev-sub-header">
322
  <span className="dev-sub-title">
@@ -402,3 +409,24 @@ export default function DevMenu({
402
  </div>
403
  );
404
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import React, { useState, useMemo, useRef, useEffect } from 'react';
2
  import {
3
+ ChevronRight, ChevronDown, Settings2, Search, Sun, Moon,
4
  Square, CheckSquare, UserPlus, ScrollText, SlidersHorizontal,
5
  BookOpen,
6
  } from 'lucide-react';
 
8
  /**
9
  * Settings menu (gear-icon dropdown in the header).
10
  *
11
+ * Layout, top-to-bottom. Multi-item categories are *collapsible
12
+ * accordions* that default to closed; single-item categories stay
13
+ * inline beneath their small uppercase label.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  *
15
+ * - Theme (single item: Sun/Moon toggle)
16
+ * - Model Selection (accordion — merges what used to be the
17
+ * separate "Models" and "Participants"
18
+ * categories: Orchestrator model, Summarizer
19
+ * model, Create Expert Persona, then one
20
+ * stacked row per active participant)
21
+ * - Max participants (single item: - / value / + stepper, 3-9)
22
+ * - Display options (accordion — two toggles)
23
+ * - Transparency (accordion — Credential Summary, Prompt
24
+ * Catalog)
25
+ * - Advanced (single item: Conversation limits…)
26
+ *
27
+ * The Downloads section that previously lived at the bottom of this
28
+ * panel has been removed; every item it offered is already reachable
29
+ * from the header DownloadMenu, so duplicating them here just made the
30
+ * settings menu unnecessarily long.
31
  */
32
  export default function DevMenu({
33
  theme,
 
52
  onShowPromptCatalog,
53
  onShowConversationLimits,
54
  conversationLimitsOverridden,
 
 
 
 
55
  }) {
56
  const [open, setOpen] = useState(false);
57
  const [activeSub, setActiveSub] = useState(null); // null | "orch" | "sum" | <participant_id>
58
  const [q, setQ] = useState('');
59
+ // Collapsed-by-default accordions. Keys correspond to the section
60
+ // ids the SectionHeader below toggles. If we ever add a fourth
61
+ // multi-item category, just add a key here.
62
+ const [openSections, setOpenSections] = useState({
63
+ modelSel: false,
64
+ display: false,
65
+ transparency: false,
66
+ });
67
  const wrapRef = useRef(null);
68
  const searchRef = useRef(null);
69
 
 
108
  else if (subject) onModelAssignmentChange(subject, id);
109
  };
110
 
111
+ // Toggle a multi-item accordion. Closing the Model Selection section
112
+ // while a model sub-panel is open would leave the sub-panel orphaned
113
+ // (its anchor row is no longer rendered), so we also clear activeSub
114
+ // in that case.
115
+ const toggleSection = (id) => {
116
+ setOpenSections(prev => {
117
+ const next = { ...prev, [id]: !prev[id] };
118
+ if (id === 'modelSel' && !next.modelSel) {
119
+ setActiveSub(null);
120
+ setQ('');
121
+ }
122
+ return next;
123
+ });
124
+ };
125
+
126
+ const modelSelOpen = openSections.modelSel;
127
+
128
  return (
129
  <div className="dev-wrap" ref={wrapRef}>
130
  <div className="dev-dropdown-header">
 
138
  {open && (
139
  <div className="dev-panel">
140
 
141
+ {/* ── Theme (single item) ─────────────────────────────── */}
142
  <div className="dev-panel-label">Theme</div>
143
  <button
144
  className="dev-panel-row"
 
153
 
154
  <div className="dev-panel-divider" />
155
 
156
+ {/* ── Model Selection (accordion: merged Models + Participants) ─ */}
157
+ <SectionHeader
158
+ label="Model Selection"
159
+ open={modelSelOpen}
160
+ onToggle={() => toggleSection('modelSel')}
161
+ />
162
+ {modelSelOpen && (
163
+ <>
164
+ <button
165
+ className="dev-panel-row dev-panel-row-stack"
166
+ onClick={() => { setActiveSub(s => s === 'orch' ? null : 'orch'); setQ(''); }}
167
+ >
168
+ <div className="dev-panel-row-text">
169
+ <div className="dev-panel-row-name">Orchestrator model…</div>
170
+ <div className="dev-panel-row-sub">{orchName}</div>
171
+ </div>
172
+ <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
173
+ </button>
174
+ <button
175
+ className="dev-panel-row dev-panel-row-stack"
176
+ onClick={() => { setActiveSub(s => s === 'sum' ? null : 'sum'); setQ(''); }}
177
+ >
178
+ <div className="dev-panel-row-text">
179
+ <div className="dev-panel-row-name">Summarizer model…</div>
180
+ <div className="dev-panel-row-sub">{sumName}</div>
181
+ </div>
182
+ <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
183
+ </button>
184
+ <button
185
+ className="dev-panel-row"
186
+ onClick={() => { onOpenExpertModal(null); setOpen(false); }}
187
+ >
188
+ <UserPlus size={14} className="dev-check-icon" />
189
+ Create Expert Persona…
190
+ </button>
191
+ {(participants || []).map(p => {
192
+ const assigned = modelAssignments[p.participant_id];
193
+ const labelName = assigned ? nameForModel(assigned)
194
+ : (p.default_model_id ? nameForModel(p.default_model_id) : '(default)');
195
+ return (
196
+ <button
197
+ key={p.participant_id}
198
+ className="dev-panel-row dev-panel-row-stack"
199
+ onClick={() => { setActiveSub(s => s === p.participant_id ? null : p.participant_id); setQ(''); }}
200
+ >
201
+ <div className="dev-panel-row-text">
202
+ <div className="dev-panel-row-name">{p.name}</div>
203
+ <div className="dev-panel-row-sub">{labelName}</div>
204
+ </div>
205
+ <ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
206
+ </button>
207
+ );
208
+ })}
209
+ </>
210
+ )}
211
 
212
  <div className="dev-panel-divider" />
213
 
214
+ {/* ── Max participants (single item) ─────────────────── */}
215
  <div className="dev-panel-label">Max participants ({maxParticipants})</div>
216
  <div className="ccai-stepper-row">
217
  <button
 
230
 
231
  <div className="dev-panel-divider" />
232
 
233
+ {/* ── Display options (accordion) ────────────────────── */}
234
+ <SectionHeader
235
+ label="Display options"
236
+ open={openSections.display}
237
+ onToggle={() => toggleSection('display')}
238
+ />
239
+ {openSections.display && (
240
+ <>
 
 
 
 
 
 
241
  <button
242
+ className={`dev-panel-choice ${showResponseTime ? 'dev-panel-choice-active' : ''}`}
243
+ onClick={() => onShowResponseTimeChange(!showResponseTime)}
 
244
  >
245
+ {showResponseTime ? <CheckSquare size={16} className="dev-check-icon" /> : <Square size={16} className="dev-check-icon" />}
246
+ Response times on messages
 
 
 
247
  </button>
248
+ <button
249
+ className={`dev-panel-choice ${showChatStats ? 'dev-panel-choice-active' : ''}`}
250
+ onClick={() => onShowChatStatsChange(!showChatStats)}
251
+ >
252
+ {showChatStats ? <CheckSquare size={16} className="dev-check-icon" /> : <Square size={16} className="dev-check-icon" />}
253
+ Chat stats after end
254
+ </button>
255
+ </>
256
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  <div className="dev-panel-divider" />
259
 
260
+ {/* ── Transparency (accordion) ───────────────────────── */}
261
+ {/* No right-side chevrons on the rows themselves: these
262
+ buttons open a modal and don't expand a sub-panel, so a
263
+ row-level chevron would be misleading. */}
264
+ <SectionHeader
265
+ label="Transparency"
266
+ open={openSections.transparency}
267
+ onToggle={() => toggleSection('transparency')}
268
+ />
269
+ {openSections.transparency && (
270
+ <>
271
+ <button
272
+ className="dev-panel-row"
273
+ disabled={!hasCredentials}
274
+ onClick={() => { onShowCredentials?.(); setOpen(false); }}
275
+ title={
276
+ hasCredentials
277
+ ? "View the orchestrator's per-participant Credential Summary"
278
+ : "Credential Summary is built after Phase 1 (initial opinions). Start a chat first."
279
+ }
280
+ >
281
+ <ScrollText size={14} className="dev-check-icon" />
282
+ View Credential Summary…
283
+ </button>
284
+ <button
285
+ className="dev-panel-row"
286
+ onClick={() => { onShowPromptCatalog?.(); setOpen(false); }}
287
+ title="View every prompt template the orchestrator and participants use, grouped by phase."
288
+ >
289
+ <BookOpen size={14} className="dev-check-icon" />
290
+ View current chat prompts…
291
+ </button>
292
+ </>
293
+ )}
294
 
295
  <div className="dev-panel-divider" />
296
 
297
+ {/* ── Advanced (single item) ─────────────────────────── */}
298
  <div className="dev-panel-label">Advanced</div>
299
  <button
300
  className="dev-panel-row"
 
317
  )}
318
  <ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5, flexShrink: 0 }} />
319
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  </div>
321
  )}
322
 
323
+ {/* Model picker sub-panel — only meaningful while the Model
324
+ Selection accordion is open, since that's the only section
325
+ whose rows set activeSub. */}
326
+ {open && modelSelOpen && activeSub && (
327
  <div className="dev-sub-panel">
328
  <div className="dev-sub-header">
329
  <span className="dev-sub-title">
 
409
  </div>
410
  );
411
  }
412
+
413
+ /**
414
+ * Clickable section header for the multi-item categories. Visually
415
+ * matches the existing uppercase `dev-panel-label` style, but is a
416
+ * button with a chevron that flips when the section is open.
417
+ */
418
+ function SectionHeader({ label, open, onToggle }) {
419
+ return (
420
+ <button
421
+ type="button"
422
+ className={`dev-panel-section-header ${open ? 'dev-panel-section-header-open' : ''}`}
423
+ onClick={onToggle}
424
+ aria-expanded={open}
425
+ >
426
+ <span>{label}</span>
427
+ {open
428
+ ? <ChevronDown size={12} className="dev-panel-section-chevron" />
429
+ : <ChevronRight size={12} className="dev-panel-section-chevron" />}
430
+ </button>
431
+ );
432
+ }
frontend/src/components/Header.js CHANGED
@@ -159,10 +159,6 @@ export default function Header({
159
  onShowPromptCatalog={onShowPromptCatalog}
160
  onShowConversationLimits={onShowConversationLimits}
161
  conversationLimitsOverridden={conversationLimitsOverridden}
162
- onDownloadChatTxt={onDownloadChatTxt}
163
- onDownloadChatMd={onDownloadChatMd}
164
- onDownloadCsvTable={onDownloadCsvTable}
165
- hasChat={hasChat}
166
  />
167
  </div>
168
  </header>
 
159
  onShowPromptCatalog={onShowPromptCatalog}
160
  onShowConversationLimits={onShowConversationLimits}
161
  conversationLimitsOverridden={conversationLimitsOverridden}
 
 
 
 
162
  />
163
  </div>
164
  </header>
frontend/src/styles/components.css CHANGED
@@ -831,6 +831,38 @@
831
  padding: 4px 10px 2px;
832
  }
833
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
834
  /* Indented row used for every actionable item inside a category, so
835
  labels (left-aligned at 10px) read as clear group headers and the
836
  items below them sit at a 22px hang. Matches the look of the
 
831
  padding: 4px 10px 2px;
832
  }
833
 
834
+ /* Clickable section header used for multi-item accordion categories
835
+ inside the settings panel. Matches the .dev-panel-label visual style
836
+ (same uppercase / size / color) so collapsed sections read as plain
837
+ labels at rest; the chevron makes the click affordance discoverable
838
+ and the hover background tells the user it's interactive. */
839
+ .dev-panel-section-header {
840
+ display: flex !important;
841
+ align-items: center;
842
+ justify-content: space-between;
843
+ width: 100%;
844
+ padding: 4px 10px 2px !important;
845
+ border: none;
846
+ background: transparent;
847
+ font-size: 10px !important;
848
+ font-weight: 600 !important;
849
+ text-transform: uppercase;
850
+ letter-spacing: 0.05em;
851
+ color: var(--text-secondary) !important;
852
+ text-align: left;
853
+ cursor: pointer;
854
+ }
855
+
856
+ .dev-panel-section-header:hover {
857
+ background: var(--bg-tertiary);
858
+ color: var(--text-primary) !important;
859
+ }
860
+
861
+ .dev-panel-section-chevron {
862
+ flex-shrink: 0;
863
+ opacity: 0.6;
864
+ }
865
+
866
  /* Indented row used for every actionable item inside a category, so
867
  labels (left-aligned at 10px) read as clear group headers and the
868
  items below them sit at a 22px hang. Matches the look of the