Spaces:
Running
feat(ui): consolidate downloads, move theme into settings, restructure & darken settings menu, fix max-participants prop wiring
Browse filesHeader
* Replace the standalone Sun/Moon button + the four-button download strip with a single Download dropdown (DownloadMenu.js). Members: View summary table, Chat .txt, Chat .md, Summary table .csv, and Full API history (the only download that lives only here).
* Header now passes every prop to DevMenu explicitly. The previous '...devProps' rest spread silently dropped 'maxParticipants' because Header was destructuring it for ParticipantDropdown's use, leaving DevMenu's stepper to compute Math.max(3, undefined - 1) = NaN on every click. With explicit wiring the stepper receives a real number; the +/- buttons update state again.
Settings menu (DevMenu.js)
* New top item: Theme toggle (Sun/Moon flips Switch to dark/light mode label).
* Every category's items are now indented to match the old Display Options pattern, so the small uppercase category labels read as headers.
* Models / Per-participant rows render two-line: name on top, currently-selected model on a second line, ellipsis-truncated when it overflows. Same treatment for Orchestrator and Summarizer.
* Transparency rows (View Credential Summary, View current chat prompts) lose their right-side > chevron - they trigger modals, not sub-panels, so the chevron was misleading and was wrapping noisily.
* Full API history removed from this menu (lives in the new header DownloadMenu). The other three download items (chat .txt / .md / summary .csv) intentionally remain here too.
Contrast
* .dev-panel button text: --text-secondary -> --text-primary (#4B5563 -> #111827).
* .dev-panel-label and .dev-panel-hint: --text-muted -> --text-secondary.
* Disabled-button opacity raised 0.4 -> 0.55 since the base text is darker now.
* Stepper row indented (padding-left 22px) to align with .dev-panel-row.
Debug instrumentation
* DevMenu.js (panel-render snapshot + +/- click) and App.js handleMaxParticipantsChange include temporary fetch() calls posting to the local debug server so the upcoming reproduction also serves as the verification run for the prop-wiring fix. A follow-up commit will remove these once the logs confirm the bug is resolved.
Co-authored-by: Cursor <cursoragent@cursor.com>
- frontend/src/App.js +24 -1
- frontend/src/components/DevMenu.js +197 -64
- frontend/src/components/DownloadMenu.js +104 -0
- frontend/src/components/Header.js +78 -14
- frontend/src/styles/ccai.css +4 -1
- frontend/src/styles/components.css +61 -10
|
@@ -191,11 +191,34 @@ export default function App() {
|
|
| 191 |
}, []);
|
| 192 |
const handleMaxParticipantsChange = useCallback((n) => {
|
| 193 |
const clamped = Math.max(3, Math.min(9, n));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
setMaxParticipants(clamped);
|
| 195 |
if (selectedIds.length > clamped) {
|
| 196 |
setSelectedIds(prev => prev.slice(0, clamped));
|
| 197 |
}
|
| 198 |
-
}, [selectedIds]);
|
| 199 |
const handleModelAssignmentChange = useCallback((participantId, modelId) => {
|
| 200 |
setModelAssignments(prev => {
|
| 201 |
const next = { ...prev };
|
|
|
|
| 191 |
}, []);
|
| 192 |
const handleMaxParticipantsChange = useCallback((n) => {
|
| 193 |
const clamped = Math.max(3, Math.min(9, n));
|
| 194 |
+
// #region agent log
|
| 195 |
+
fetch('http://127.0.0.1:7660/ingest/b27d4bb5-c1ab-4767-aa98-1cdf1e8fb0ae', {
|
| 196 |
+
method: 'POST',
|
| 197 |
+
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '896623' },
|
| 198 |
+
body: JSON.stringify({
|
| 199 |
+
sessionId: '896623',
|
| 200 |
+
runId: 'maxparticipants-repro',
|
| 201 |
+
hypothesisId: 'H2',
|
| 202 |
+
location: 'App.js:handleMaxParticipantsChange',
|
| 203 |
+
message: 'Parent handler invoked',
|
| 204 |
+
data: {
|
| 205 |
+
n,
|
| 206 |
+
nType: typeof n,
|
| 207 |
+
nIsNaN: Number.isNaN(n),
|
| 208 |
+
clamped,
|
| 209 |
+
clampedIsNaN: Number.isNaN(clamped),
|
| 210 |
+
prevMaxParticipants: maxParticipants,
|
| 211 |
+
prevMaxParticipantsType: typeof maxParticipants,
|
| 212 |
+
},
|
| 213 |
+
timestamp: Date.now(),
|
| 214 |
+
}),
|
| 215 |
+
}).catch(() => {});
|
| 216 |
+
// #endregion
|
| 217 |
setMaxParticipants(clamped);
|
| 218 |
if (selectedIds.length > clamped) {
|
| 219 |
setSelectedIds(prev => prev.slice(0, clamped));
|
| 220 |
}
|
| 221 |
+
}, [selectedIds, maxParticipants]);
|
| 222 |
const handleModelAssignmentChange = useCallback((participantId, modelId) => {
|
| 223 |
setModelAssignments(prev => {
|
| 224 |
const next = { ...prev };
|
|
@@ -1,21 +1,39 @@
|
|
| 1 |
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
| 2 |
import {
|
| 3 |
-
ChevronRight, Download, Settings2, Search,
|
| 4 |
-
Square, CheckSquare, UserPlus,
|
| 5 |
BookOpen,
|
| 6 |
} from 'lucide-react';
|
| 7 |
|
| 8 |
/**
|
| 9 |
-
* Settings menu
|
| 10 |
-
*
|
| 11 |
-
*
|
| 12 |
-
*
|
| 13 |
-
* -
|
| 14 |
-
* -
|
| 15 |
-
*
|
| 16 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
*/
|
| 18 |
export default function DevMenu({
|
|
|
|
|
|
|
| 19 |
allModels,
|
| 20 |
orchestratorModel,
|
| 21 |
onOrchestratorChange,
|
|
@@ -31,7 +49,6 @@ export default function DevMenu({
|
|
| 31 |
modelAssignments,
|
| 32 |
onModelAssignmentChange,
|
| 33 |
onOpenExpertModal,
|
| 34 |
-
onShowTableView,
|
| 35 |
onShowCredentials,
|
| 36 |
hasCredentials,
|
| 37 |
onShowPromptCatalog,
|
|
@@ -40,8 +57,6 @@ export default function DevMenu({
|
|
| 40 |
onDownloadChatTxt,
|
| 41 |
onDownloadChatMd,
|
| 42 |
onDownloadCsvTable,
|
| 43 |
-
onDownloadApiLog,
|
| 44 |
-
hasApiLog,
|
| 45 |
hasChat,
|
| 46 |
}) {
|
| 47 |
const [open, setOpen] = useState(false);
|
|
@@ -75,6 +90,31 @@ export default function DevMenu({
|
|
| 75 |
});
|
| 76 |
}, [allModels, q]);
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
const nameForModel = (id) => {
|
| 79 |
if (!id) return null;
|
| 80 |
const m = allModels.find(x => x.id === id);
|
|
@@ -93,31 +133,6 @@ export default function DevMenu({
|
|
| 93 |
|
| 94 |
return (
|
| 95 |
<div className="dev-wrap" ref={wrapRef}>
|
| 96 |
-
<div className="dev-download-btns">
|
| 97 |
-
<button className="btn-sm btn-outline" disabled={!hasChat} onClick={onDownloadChatTxt}>
|
| 98 |
-
<Download size={14} /> .txt
|
| 99 |
-
</button>
|
| 100 |
-
<button className="btn-sm btn-outline" disabled={!hasChat} onClick={onDownloadChatMd}>
|
| 101 |
-
<Download size={14} /> .md
|
| 102 |
-
</button>
|
| 103 |
-
<button
|
| 104 |
-
className="btn-sm btn-outline"
|
| 105 |
-
disabled={!hasChat}
|
| 106 |
-
onClick={onShowTableView}
|
| 107 |
-
title="Open the conversation summary table"
|
| 108 |
-
>
|
| 109 |
-
<Table2 size={14} /> Table
|
| 110 |
-
</button>
|
| 111 |
-
<button
|
| 112 |
-
className="btn-sm btn-outline"
|
| 113 |
-
disabled={!hasChat}
|
| 114 |
-
onClick={onDownloadCsvTable}
|
| 115 |
-
title="Download the table view as CSV"
|
| 116 |
-
>
|
| 117 |
-
<Download size={14} /> .csv
|
| 118 |
-
</button>
|
| 119 |
-
</div>
|
| 120 |
-
|
| 121 |
<div className="dev-dropdown-header">
|
| 122 |
<button
|
| 123 |
className="icon-btn"
|
|
@@ -128,41 +143,125 @@ export default function DevMenu({
|
|
| 128 |
</button>
|
| 129 |
{open && (
|
| 130 |
<div className="dev-panel">
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
</button>
|
| 135 |
-
<button
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
</button>
|
| 139 |
|
| 140 |
<div className="dev-panel-divider" />
|
|
|
|
|
|
|
| 141 |
<div className="dev-panel-label">Max participants ({maxParticipants})</div>
|
| 142 |
<div className="ccai-stepper-row">
|
| 143 |
<button
|
| 144 |
className="btn-sm btn-outline ccai-stepper-btn"
|
| 145 |
disabled={maxParticipants <= 3}
|
| 146 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
>−</button>
|
| 148 |
<div className="ccai-stepper-val">{maxParticipants}</div>
|
| 149 |
<button
|
| 150 |
className="btn-sm btn-outline ccai-stepper-btn"
|
| 151 |
disabled={maxParticipants >= 9}
|
| 152 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
>+</button>
|
| 154 |
<span className="dev-panel-hint">3-9</span>
|
| 155 |
</div>
|
| 156 |
|
| 157 |
<div className="dev-panel-divider" />
|
|
|
|
|
|
|
| 158 |
<div className="dev-panel-label">Participants</div>
|
| 159 |
-
<button
|
|
|
|
|
|
|
|
|
|
| 160 |
<UserPlus size={14} className="dev-check-icon" />
|
| 161 |
Create Expert Persona…
|
| 162 |
</button>
|
| 163 |
-
{(participants || []).length > 0 && (
|
| 164 |
-
<div className="dev-panel-label">Per-participant model</div>
|
| 165 |
-
)}
|
| 166 |
{(participants || []).map(p => {
|
| 167 |
const assigned = modelAssignments[p.participant_id];
|
| 168 |
const labelName = assigned ? nameForModel(assigned)
|
|
@@ -170,15 +269,21 @@ export default function DevMenu({
|
|
| 170 |
return (
|
| 171 |
<button
|
| 172 |
key={p.participant_id}
|
|
|
|
| 173 |
onClick={() => { setActiveSub(s => s === p.participant_id ? null : p.participant_id); setQ(''); }}
|
| 174 |
>
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
| 177 |
</button>
|
| 178 |
);
|
| 179 |
})}
|
| 180 |
|
| 181 |
<div className="dev-panel-divider" />
|
|
|
|
|
|
|
| 182 |
<div className="dev-panel-label">Display options</div>
|
| 183 |
<button
|
| 184 |
className={`dev-panel-choice ${showResponseTime ? 'dev-panel-choice-active' : ''}`}
|
|
@@ -196,8 +301,15 @@ export default function DevMenu({
|
|
| 196 |
</button>
|
| 197 |
|
| 198 |
<div className="dev-panel-divider" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
<div className="dev-panel-label">Transparency</div>
|
| 200 |
<button
|
|
|
|
| 201 |
disabled={!hasCredentials}
|
| 202 |
onClick={() => { onShowCredentials?.(); setOpen(false); }}
|
| 203 |
title={
|
|
@@ -208,20 +320,22 @@ export default function DevMenu({
|
|
| 208 |
>
|
| 209 |
<ScrollText size={14} className="dev-check-icon" />
|
| 210 |
View Credential Summary…
|
| 211 |
-
<ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5 }} />
|
| 212 |
</button>
|
| 213 |
<button
|
|
|
|
| 214 |
onClick={() => { onShowPromptCatalog?.(); setOpen(false); }}
|
| 215 |
title="View every prompt template the orchestrator and participants use, grouped by phase."
|
| 216 |
>
|
| 217 |
<BookOpen size={14} className="dev-check-icon" />
|
| 218 |
View current chat prompts…
|
| 219 |
-
<ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5 }} />
|
| 220 |
</button>
|
| 221 |
|
| 222 |
<div className="dev-panel-divider" />
|
|
|
|
|
|
|
| 223 |
<div className="dev-panel-label">Advanced</div>
|
| 224 |
<button
|
|
|
|
| 225 |
onClick={() => { onShowConversationLimits?.(); setOpen(false); }}
|
| 226 |
title="View and adjust the per-phase repetition limits and failsafe pause points."
|
| 227 |
>
|
|
@@ -239,21 +353,40 @@ export default function DevMenu({
|
|
| 239 |
(custom)
|
| 240 |
</span>
|
| 241 |
)}
|
| 242 |
-
<ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5 }} />
|
| 243 |
</button>
|
| 244 |
|
| 245 |
<div className="dev-panel-divider" />
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
</button>
|
| 252 |
-
<button
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
</button>
|
| 255 |
-
<button
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
</button>
|
| 258 |
</div>
|
| 259 |
)}
|
|
|
|
| 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';
|
| 7 |
|
| 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,
|
| 36 |
+
onToggleTheme,
|
| 37 |
allModels,
|
| 38 |
orchestratorModel,
|
| 39 |
onOrchestratorChange,
|
|
|
|
| 49 |
modelAssignments,
|
| 50 |
onModelAssignmentChange,
|
| 51 |
onOpenExpertModal,
|
|
|
|
| 52 |
onShowCredentials,
|
| 53 |
hasCredentials,
|
| 54 |
onShowPromptCatalog,
|
|
|
|
| 57 |
onDownloadChatTxt,
|
| 58 |
onDownloadChatMd,
|
| 59 |
onDownloadCsvTable,
|
|
|
|
|
|
|
| 60 |
hasChat,
|
| 61 |
}) {
|
| 62 |
const [open, setOpen] = useState(false);
|
|
|
|
| 90 |
});
|
| 91 |
}, [allModels, q]);
|
| 92 |
|
| 93 |
+
// #region agent log
|
| 94 |
+
useEffect(() => {
|
| 95 |
+
if (!open) return;
|
| 96 |
+
fetch('http://127.0.0.1:7660/ingest/b27d4bb5-c1ab-4767-aa98-1cdf1e8fb0ae', {
|
| 97 |
+
method: 'POST',
|
| 98 |
+
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '896623' },
|
| 99 |
+
body: JSON.stringify({
|
| 100 |
+
sessionId: '896623',
|
| 101 |
+
runId: 'maxparticipants-repro',
|
| 102 |
+
hypothesisId: 'H1',
|
| 103 |
+
location: 'DevMenu.js:render',
|
| 104 |
+
message: 'DevMenu render snapshot (panel open)',
|
| 105 |
+
data: {
|
| 106 |
+
maxParticipants,
|
| 107 |
+
maxParticipantsType: typeof maxParticipants,
|
| 108 |
+
onMaxParticipantsChangeType: typeof onMaxParticipantsChange,
|
| 109 |
+
ge9: maxParticipants >= 9,
|
| 110 |
+
le3: maxParticipants <= 3,
|
| 111 |
+
},
|
| 112 |
+
timestamp: Date.now(),
|
| 113 |
+
}),
|
| 114 |
+
}).catch(() => {});
|
| 115 |
+
}, [open, maxParticipants, onMaxParticipantsChange]);
|
| 116 |
+
// #endregion
|
| 117 |
+
|
| 118 |
const nameForModel = (id) => {
|
| 119 |
if (!id) return null;
|
| 120 |
const m = allModels.find(x => x.id === id);
|
|
|
|
| 133 |
|
| 134 |
return (
|
| 135 |
<div className="dev-wrap" ref={wrapRef}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
<div className="dev-dropdown-header">
|
| 137 |
<button
|
| 138 |
className="icon-btn"
|
|
|
|
| 143 |
</button>
|
| 144 |
{open && (
|
| 145 |
<div className="dev-panel">
|
| 146 |
+
|
| 147 |
+
{/* ── Theme ─────────────────────────────────────────── */}
|
| 148 |
+
<div className="dev-panel-label">Theme</div>
|
| 149 |
+
<button
|
| 150 |
+
className="dev-panel-row"
|
| 151 |
+
onClick={onToggleTheme}
|
| 152 |
+
title="Toggle light/dark mode"
|
| 153 |
+
>
|
| 154 |
+
{theme === 'light'
|
| 155 |
+
? <Moon size={14} className="dev-check-icon" />
|
| 156 |
+
: <Sun size={14} className="dev-check-icon" />}
|
| 157 |
+
{theme === 'light' ? 'Switch to dark mode' : 'Switch to light mode'}
|
| 158 |
+
</button>
|
| 159 |
+
|
| 160 |
+
<div className="dev-panel-divider" />
|
| 161 |
+
|
| 162 |
+
{/* ── Models ────────────────────────────────────────── */}
|
| 163 |
+
<div className="dev-panel-label">Models</div>
|
| 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 |
|
| 185 |
<div className="dev-panel-divider" />
|
| 186 |
+
|
| 187 |
+
{/* ── Max participants ──────────────────────────────── */}
|
| 188 |
<div className="dev-panel-label">Max participants ({maxParticipants})</div>
|
| 189 |
<div className="ccai-stepper-row">
|
| 190 |
<button
|
| 191 |
className="btn-sm btn-outline ccai-stepper-btn"
|
| 192 |
disabled={maxParticipants <= 3}
|
| 193 |
+
onClick={() => {
|
| 194 |
+
const next = Math.max(3, maxParticipants - 1);
|
| 195 |
+
// #region agent log
|
| 196 |
+
fetch('http://127.0.0.1:7660/ingest/b27d4bb5-c1ab-4767-aa98-1cdf1e8fb0ae', {
|
| 197 |
+
method: 'POST',
|
| 198 |
+
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '896623' },
|
| 199 |
+
body: JSON.stringify({
|
| 200 |
+
sessionId: '896623',
|
| 201 |
+
runId: 'maxparticipants-repro',
|
| 202 |
+
hypothesisId: 'H1',
|
| 203 |
+
location: 'DevMenu.js:onMinusClick',
|
| 204 |
+
message: 'Minus button clicked',
|
| 205 |
+
data: {
|
| 206 |
+
maxParticipants,
|
| 207 |
+
maxParticipantsType: typeof maxParticipants,
|
| 208 |
+
next,
|
| 209 |
+
nextType: typeof next,
|
| 210 |
+
nextIsNaN: Number.isNaN(next),
|
| 211 |
+
onMaxParticipantsChangeType: typeof onMaxParticipantsChange,
|
| 212 |
+
},
|
| 213 |
+
timestamp: Date.now(),
|
| 214 |
+
}),
|
| 215 |
+
}).catch(() => {});
|
| 216 |
+
// #endregion
|
| 217 |
+
onMaxParticipantsChange(next);
|
| 218 |
+
}}
|
| 219 |
>−</button>
|
| 220 |
<div className="ccai-stepper-val">{maxParticipants}</div>
|
| 221 |
<button
|
| 222 |
className="btn-sm btn-outline ccai-stepper-btn"
|
| 223 |
disabled={maxParticipants >= 9}
|
| 224 |
+
onClick={() => {
|
| 225 |
+
const next = Math.min(9, maxParticipants + 1);
|
| 226 |
+
// #region agent log
|
| 227 |
+
fetch('http://127.0.0.1:7660/ingest/b27d4bb5-c1ab-4767-aa98-1cdf1e8fb0ae', {
|
| 228 |
+
method: 'POST',
|
| 229 |
+
headers: { 'Content-Type': 'application/json', 'X-Debug-Session-Id': '896623' },
|
| 230 |
+
body: JSON.stringify({
|
| 231 |
+
sessionId: '896623',
|
| 232 |
+
runId: 'maxparticipants-repro',
|
| 233 |
+
hypothesisId: 'H1',
|
| 234 |
+
location: 'DevMenu.js:onPlusClick',
|
| 235 |
+
message: 'Plus button clicked',
|
| 236 |
+
data: {
|
| 237 |
+
maxParticipants,
|
| 238 |
+
maxParticipantsType: typeof maxParticipants,
|
| 239 |
+
next,
|
| 240 |
+
nextType: typeof next,
|
| 241 |
+
nextIsNaN: Number.isNaN(next),
|
| 242 |
+
onMaxParticipantsChangeType: typeof onMaxParticipantsChange,
|
| 243 |
+
},
|
| 244 |
+
timestamp: Date.now(),
|
| 245 |
+
}),
|
| 246 |
+
}).catch(() => {});
|
| 247 |
+
// #endregion
|
| 248 |
+
onMaxParticipantsChange(next);
|
| 249 |
+
}}
|
| 250 |
>+</button>
|
| 251 |
<span className="dev-panel-hint">3-9</span>
|
| 252 |
</div>
|
| 253 |
|
| 254 |
<div className="dev-panel-divider" />
|
| 255 |
+
|
| 256 |
+
{/* ── Participants ──────────────────────────────────── */}
|
| 257 |
<div className="dev-panel-label">Participants</div>
|
| 258 |
+
<button
|
| 259 |
+
className="dev-panel-row"
|
| 260 |
+
onClick={() => { onOpenExpertModal(null); setOpen(false); }}
|
| 261 |
+
>
|
| 262 |
<UserPlus size={14} className="dev-check-icon" />
|
| 263 |
Create Expert Persona…
|
| 264 |
</button>
|
|
|
|
|
|
|
|
|
|
| 265 |
{(participants || []).map(p => {
|
| 266 |
const assigned = modelAssignments[p.participant_id];
|
| 267 |
const labelName = assigned ? nameForModel(assigned)
|
|
|
|
| 269 |
return (
|
| 270 |
<button
|
| 271 |
key={p.participant_id}
|
| 272 |
+
className="dev-panel-row dev-panel-row-stack"
|
| 273 |
onClick={() => { setActiveSub(s => s === p.participant_id ? null : p.participant_id); setQ(''); }}
|
| 274 |
>
|
| 275 |
+
<div className="dev-panel-row-text">
|
| 276 |
+
<div className="dev-panel-row-name">{p.name}</div>
|
| 277 |
+
<div className="dev-panel-row-sub">{labelName}</div>
|
| 278 |
+
</div>
|
| 279 |
+
<ChevronRight size={12} style={{ opacity: 0.5, flexShrink: 0 }} />
|
| 280 |
</button>
|
| 281 |
);
|
| 282 |
})}
|
| 283 |
|
| 284 |
<div className="dev-panel-divider" />
|
| 285 |
+
|
| 286 |
+
{/* ── Display options ───────────────────────────────── */}
|
| 287 |
<div className="dev-panel-label">Display options</div>
|
| 288 |
<button
|
| 289 |
className={`dev-panel-choice ${showResponseTime ? 'dev-panel-choice-active' : ''}`}
|
|
|
|
| 301 |
</button>
|
| 302 |
|
| 303 |
<div className="dev-panel-divider" />
|
| 304 |
+
|
| 305 |
+
{/* ── Transparency ──────────────────────────────────── */}
|
| 306 |
+
{/* No right-side chevrons here: these buttons trigger a
|
| 307 |
+
modal and don't expand a sub-panel, so the chevron the
|
| 308 |
+
old layout had was misleading and (per user feedback)
|
| 309 |
+
wrapped onto a noisy second line. */}
|
| 310 |
<div className="dev-panel-label">Transparency</div>
|
| 311 |
<button
|
| 312 |
+
className="dev-panel-row"
|
| 313 |
disabled={!hasCredentials}
|
| 314 |
onClick={() => { onShowCredentials?.(); setOpen(false); }}
|
| 315 |
title={
|
|
|
|
| 320 |
>
|
| 321 |
<ScrollText size={14} className="dev-check-icon" />
|
| 322 |
View Credential Summary…
|
|
|
|
| 323 |
</button>
|
| 324 |
<button
|
| 325 |
+
className="dev-panel-row"
|
| 326 |
onClick={() => { onShowPromptCatalog?.(); setOpen(false); }}
|
| 327 |
title="View every prompt template the orchestrator and participants use, grouped by phase."
|
| 328 |
>
|
| 329 |
<BookOpen size={14} className="dev-check-icon" />
|
| 330 |
View current chat prompts…
|
|
|
|
| 331 |
</button>
|
| 332 |
|
| 333 |
<div className="dev-panel-divider" />
|
| 334 |
+
|
| 335 |
+
{/* ── Advanced ──────────────────────────────────────── */}
|
| 336 |
<div className="dev-panel-label">Advanced</div>
|
| 337 |
<button
|
| 338 |
+
className="dev-panel-row"
|
| 339 |
onClick={() => { onShowConversationLimits?.(); setOpen(false); }}
|
| 340 |
title="View and adjust the per-phase repetition limits and failsafe pause points."
|
| 341 |
>
|
|
|
|
| 353 |
(custom)
|
| 354 |
</span>
|
| 355 |
)}
|
| 356 |
+
<ChevronRight size={12} style={{ marginLeft: 'auto', opacity: 0.5, flexShrink: 0 }} />
|
| 357 |
</button>
|
| 358 |
|
| 359 |
<div className="dev-panel-divider" />
|
| 360 |
+
|
| 361 |
+
{/* ── Downloads ─────────────────────────────────────── */}
|
| 362 |
+
{/* These three are intentional duplicates of items in the
|
| 363 |
+
header DownloadMenu. The "Full API history" item lives
|
| 364 |
+
only in DownloadMenu (per UX request) and is therefore
|
| 365 |
+
not listed here. */}
|
| 366 |
+
<div className="dev-panel-label">Downloads</div>
|
| 367 |
+
<button
|
| 368 |
+
className="dev-panel-row"
|
| 369 |
+
disabled={!hasChat}
|
| 370 |
+
onClick={() => { onDownloadChatTxt(); setOpen(false); }}
|
| 371 |
+
>
|
| 372 |
+
<Download size={14} className="dev-check-icon" />
|
| 373 |
+
Chat as .txt
|
| 374 |
</button>
|
| 375 |
+
<button
|
| 376 |
+
className="dev-panel-row"
|
| 377 |
+
disabled={!hasChat}
|
| 378 |
+
onClick={() => { onDownloadChatMd(); setOpen(false); }}
|
| 379 |
+
>
|
| 380 |
+
<Download size={14} className="dev-check-icon" />
|
| 381 |
+
Chat as .md
|
| 382 |
</button>
|
| 383 |
+
<button
|
| 384 |
+
className="dev-panel-row"
|
| 385 |
+
disabled={!hasChat}
|
| 386 |
+
onClick={() => { onDownloadCsvTable(); setOpen(false); }}
|
| 387 |
+
>
|
| 388 |
+
<Download size={14} className="dev-check-icon" />
|
| 389 |
+
Summary table as .csv
|
| 390 |
</button>
|
| 391 |
</div>
|
| 392 |
)}
|
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
import { Download, Table2, FileText, FileCode, FileSpreadsheet, History } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
/**
|
| 5 |
+
* Header dropdown that consolidates every "view this conversation as…"
|
| 6 |
+
* and "download this conversation as…" action behind a single Download
|
| 7 |
+
* icon. The same chat-as-.txt / .md / .csv items are *also* listed in
|
| 8 |
+
* the Settings menu's Downloads section (intentional duplication, per
|
| 9 |
+
* UX request); the only item that lives *exclusively* here is
|
| 10 |
+
* "Download full API history".
|
| 11 |
+
*
|
| 12 |
+
* Mirrors the open/close + outside-mousedown pattern used by DevMenu so
|
| 13 |
+
* both dropdowns feel and behave identically.
|
| 14 |
+
*/
|
| 15 |
+
export default function DownloadMenu({
|
| 16 |
+
hasChat,
|
| 17 |
+
hasApiLog,
|
| 18 |
+
onShowTableView,
|
| 19 |
+
onDownloadChatTxt,
|
| 20 |
+
onDownloadChatMd,
|
| 21 |
+
onDownloadCsvTable,
|
| 22 |
+
onDownloadApiLog,
|
| 23 |
+
}) {
|
| 24 |
+
const [open, setOpen] = useState(false);
|
| 25 |
+
const wrapRef = useRef(null);
|
| 26 |
+
|
| 27 |
+
useEffect(() => {
|
| 28 |
+
function handleClickOutside(e) {
|
| 29 |
+
if (wrapRef.current && !wrapRef.current.contains(e.target)) {
|
| 30 |
+
setOpen(false);
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
document.addEventListener('mousedown', handleClickOutside);
|
| 34 |
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
| 35 |
+
}, []);
|
| 36 |
+
|
| 37 |
+
const fire = (fn) => () => { fn?.(); setOpen(false); };
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div className="dev-wrap" ref={wrapRef}>
|
| 41 |
+
<div className="dev-dropdown-header">
|
| 42 |
+
<button
|
| 43 |
+
className="icon-btn"
|
| 44 |
+
onClick={() => setOpen(o => !o)}
|
| 45 |
+
title="Downloads & exports"
|
| 46 |
+
aria-label="Downloads"
|
| 47 |
+
>
|
| 48 |
+
<Download size={16} />
|
| 49 |
+
</button>
|
| 50 |
+
{open && (
|
| 51 |
+
<div className="dev-panel">
|
| 52 |
+
<div className="dev-panel-label">View</div>
|
| 53 |
+
<button
|
| 54 |
+
className="dev-panel-row"
|
| 55 |
+
disabled={!hasChat}
|
| 56 |
+
onClick={fire(onShowTableView)}
|
| 57 |
+
title="Open the conversation summary table"
|
| 58 |
+
>
|
| 59 |
+
<Table2 size={14} className="dev-check-icon" />
|
| 60 |
+
Summary table…
|
| 61 |
+
</button>
|
| 62 |
+
|
| 63 |
+
<div className="dev-panel-divider" />
|
| 64 |
+
<div className="dev-panel-label">Downloads</div>
|
| 65 |
+
<button
|
| 66 |
+
className="dev-panel-row"
|
| 67 |
+
disabled={!hasChat}
|
| 68 |
+
onClick={fire(onDownloadChatTxt)}
|
| 69 |
+
>
|
| 70 |
+
<FileText size={14} className="dev-check-icon" />
|
| 71 |
+
Chat as .txt
|
| 72 |
+
</button>
|
| 73 |
+
<button
|
| 74 |
+
className="dev-panel-row"
|
| 75 |
+
disabled={!hasChat}
|
| 76 |
+
onClick={fire(onDownloadChatMd)}
|
| 77 |
+
>
|
| 78 |
+
<FileCode size={14} className="dev-check-icon" />
|
| 79 |
+
Chat as .md
|
| 80 |
+
</button>
|
| 81 |
+
<button
|
| 82 |
+
className="dev-panel-row"
|
| 83 |
+
disabled={!hasChat}
|
| 84 |
+
onClick={fire(onDownloadCsvTable)}
|
| 85 |
+
title="Download the summary table as CSV"
|
| 86 |
+
>
|
| 87 |
+
<FileSpreadsheet size={14} className="dev-check-icon" />
|
| 88 |
+
Summary table as .csv
|
| 89 |
+
</button>
|
| 90 |
+
<button
|
| 91 |
+
className="dev-panel-row"
|
| 92 |
+
disabled={!hasApiLog}
|
| 93 |
+
onClick={fire(onDownloadApiLog)}
|
| 94 |
+
title="Download the full backend API call history for this session"
|
| 95 |
+
>
|
| 96 |
+
<History size={14} className="dev-check-icon" />
|
| 97 |
+
Full API history
|
| 98 |
+
</button>
|
| 99 |
+
</div>
|
| 100 |
+
)}
|
| 101 |
+
</div>
|
| 102 |
+
</div>
|
| 103 |
+
);
|
| 104 |
+
}
|
|
@@ -1,13 +1,22 @@
|
|
| 1 |
import React from 'react';
|
| 2 |
-
import { Sun, Moon } from 'lucide-react';
|
| 3 |
import AuthBadge from './AuthBadge';
|
| 4 |
import ParticipantDropdown from './ParticipantDropdown';
|
|
|
|
| 5 |
import DevMenu from './DevMenu';
|
| 6 |
|
| 7 |
/**
|
| 8 |
-
* Header bar: brand on the left; on the right,
|
| 9 |
-
*
|
| 10 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
*/
|
| 12 |
export default function Header({
|
| 13 |
theme,
|
|
@@ -24,8 +33,37 @@ export default function Header({
|
|
| 24 |
autoSelectMode,
|
| 25 |
onToggleAutoSelectMode,
|
| 26 |
|
| 27 |
-
//
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}) {
|
| 30 |
return (
|
| 31 |
<header className="app-header">
|
|
@@ -51,16 +89,42 @@ export default function Header({
|
|
| 51 |
autoSelectMode={autoSelectMode}
|
| 52 |
onToggleAutoSelectMode={onToggleAutoSelectMode}
|
| 53 |
/>
|
| 54 |
-
<
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
<DevMenu
|
| 62 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
onOpenExpertModal={onOpenExpertModal}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
/>
|
| 65 |
</div>
|
| 66 |
</header>
|
|
|
|
| 1 |
import React from 'react';
|
|
|
|
| 2 |
import AuthBadge from './AuthBadge';
|
| 3 |
import ParticipantDropdown from './ParticipantDropdown';
|
| 4 |
+
import DownloadMenu from './DownloadMenu';
|
| 5 |
import DevMenu from './DevMenu';
|
| 6 |
|
| 7 |
/**
|
| 8 |
+
* Header bar: brand on the left; on the right, auth badge, participant
|
| 9 |
+
* dropdown, downloads dropdown, settings (gear) dropdown.
|
| 10 |
+
*
|
| 11 |
+
* The standalone Sun/Moon theme toggle that used to live here has moved
|
| 12 |
+
* inside the DevMenu (Theme is the top item in the settings panel).
|
| 13 |
+
*
|
| 14 |
+
* NOTE: every prop that goes to DevMenu is forwarded *explicitly* below
|
| 15 |
+
* (no `...devProps` rest spread). The previous spread pattern hid the
|
| 16 |
+
* `maxParticipants` value because Header destructured it for its own
|
| 17 |
+
* use, which silently stripped it from the spread that fed DevMenu;
|
| 18 |
+
* the Max Participants stepper then received `undefined` and produced
|
| 19 |
+
* NaN on every click, which manifested as "the +/- buttons do nothing".
|
| 20 |
*/
|
| 21 |
export default function Header({
|
| 22 |
theme,
|
|
|
|
| 33 |
autoSelectMode,
|
| 34 |
onToggleAutoSelectMode,
|
| 35 |
|
| 36 |
+
// Models / display
|
| 37 |
+
allModels,
|
| 38 |
+
orchestratorModel,
|
| 39 |
+
onOrchestratorChange,
|
| 40 |
+
summarizerModel,
|
| 41 |
+
onSummarizerChange,
|
| 42 |
+
showResponseTime,
|
| 43 |
+
onShowResponseTimeChange,
|
| 44 |
+
showChatStats,
|
| 45 |
+
onShowChatStatsChange,
|
| 46 |
+
onMaxParticipantsChange,
|
| 47 |
+
|
| 48 |
+
participants,
|
| 49 |
+
modelAssignments,
|
| 50 |
+
onModelAssignmentChange,
|
| 51 |
+
|
| 52 |
+
// Modals / transparency
|
| 53 |
+
onShowTableView,
|
| 54 |
+
onShowCredentials,
|
| 55 |
+
hasCredentials,
|
| 56 |
+
onShowPromptCatalog,
|
| 57 |
+
onShowConversationLimits,
|
| 58 |
+
conversationLimitsOverridden,
|
| 59 |
+
|
| 60 |
+
// Downloads
|
| 61 |
+
onDownloadChatTxt,
|
| 62 |
+
onDownloadChatMd,
|
| 63 |
+
onDownloadCsvTable,
|
| 64 |
+
onDownloadApiLog,
|
| 65 |
+
hasApiLog,
|
| 66 |
+
hasChat,
|
| 67 |
}) {
|
| 68 |
return (
|
| 69 |
<header className="app-header">
|
|
|
|
| 89 |
autoSelectMode={autoSelectMode}
|
| 90 |
onToggleAutoSelectMode={onToggleAutoSelectMode}
|
| 91 |
/>
|
| 92 |
+
<DownloadMenu
|
| 93 |
+
hasChat={hasChat}
|
| 94 |
+
hasApiLog={hasApiLog}
|
| 95 |
+
onShowTableView={onShowTableView}
|
| 96 |
+
onDownloadChatTxt={onDownloadChatTxt}
|
| 97 |
+
onDownloadChatMd={onDownloadChatMd}
|
| 98 |
+
onDownloadCsvTable={onDownloadCsvTable}
|
| 99 |
+
onDownloadApiLog={onDownloadApiLog}
|
| 100 |
+
/>
|
| 101 |
<DevMenu
|
| 102 |
+
theme={theme}
|
| 103 |
+
onToggleTheme={onToggleTheme}
|
| 104 |
+
allModels={allModels}
|
| 105 |
+
orchestratorModel={orchestratorModel}
|
| 106 |
+
onOrchestratorChange={onOrchestratorChange}
|
| 107 |
+
summarizerModel={summarizerModel}
|
| 108 |
+
onSummarizerChange={onSummarizerChange}
|
| 109 |
+
showResponseTime={showResponseTime}
|
| 110 |
+
onShowResponseTimeChange={onShowResponseTimeChange}
|
| 111 |
+
showChatStats={showChatStats}
|
| 112 |
+
onShowChatStatsChange={onShowChatStatsChange}
|
| 113 |
+
maxParticipants={maxParticipants}
|
| 114 |
+
onMaxParticipantsChange={onMaxParticipantsChange}
|
| 115 |
+
participants={participants}
|
| 116 |
+
modelAssignments={modelAssignments}
|
| 117 |
+
onModelAssignmentChange={onModelAssignmentChange}
|
| 118 |
onOpenExpertModal={onOpenExpertModal}
|
| 119 |
+
onShowCredentials={onShowCredentials}
|
| 120 |
+
hasCredentials={hasCredentials}
|
| 121 |
+
onShowPromptCatalog={onShowPromptCatalog}
|
| 122 |
+
onShowConversationLimits={onShowConversationLimits}
|
| 123 |
+
conversationLimitsOverridden={conversationLimitsOverridden}
|
| 124 |
+
onDownloadChatTxt={onDownloadChatTxt}
|
| 125 |
+
onDownloadChatMd={onDownloadChatMd}
|
| 126 |
+
onDownloadCsvTable={onDownloadCsvTable}
|
| 127 |
+
hasChat={hasChat}
|
| 128 |
/>
|
| 129 |
</div>
|
| 130 |
</header>
|
|
@@ -982,7 +982,10 @@
|
|
| 982 |
display: flex;
|
| 983 |
align-items: center;
|
| 984 |
gap: 8px;
|
| 985 |
-
padding
|
|
|
|
|
|
|
|
|
|
| 986 |
}
|
| 987 |
|
| 988 |
.ccai-stepper-btn {
|
|
|
|
| 982 |
display: flex;
|
| 983 |
align-items: center;
|
| 984 |
gap: 8px;
|
| 985 |
+
/* 22px left padding matches .dev-panel-row indentation so the
|
| 986 |
+
stepper hangs under the "Max participants" category label
|
| 987 |
+
consistently with every other category's items. */
|
| 988 |
+
padding: 4px 10px 8px 22px;
|
| 989 |
}
|
| 990 |
|
| 991 |
.ccai-stepper-btn {
|
|
@@ -769,7 +769,10 @@
|
|
| 769 |
padding: 8px 10px;
|
| 770 |
border: none;
|
| 771 |
background: transparent;
|
| 772 |
-
|
|
|
|
|
|
|
|
|
|
| 773 |
border-radius: 6px;
|
| 774 |
cursor: pointer;
|
| 775 |
font-size: 13px;
|
|
@@ -782,13 +785,17 @@
|
|
| 782 |
}
|
| 783 |
|
| 784 |
.dev-panel button:disabled {
|
| 785 |
-
|
|
|
|
|
|
|
| 786 |
cursor: not-allowed;
|
| 787 |
}
|
| 788 |
|
| 789 |
.dev-panel-hint {
|
| 790 |
font-size: 10px;
|
| 791 |
-
|
|
|
|
|
|
|
| 792 |
margin-left: 6px;
|
| 793 |
font-style: italic;
|
| 794 |
}
|
|
@@ -804,10 +811,55 @@
|
|
| 804 |
font-weight: 600;
|
| 805 |
text-transform: uppercase;
|
| 806 |
letter-spacing: 0.05em;
|
| 807 |
-
|
|
|
|
| 808 |
padding: 4px 10px 2px;
|
| 809 |
}
|
| 810 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
.dev-panel-choice {
|
| 812 |
padding-left: 22px !important;
|
| 813 |
display: flex !important;
|
|
@@ -1132,12 +1184,11 @@
|
|
| 1132 |
|
| 1133 |
/* ── Small tablet / large phone (<600px) ──────────────────────── */
|
| 1134 |
@media (max-width: 600px) {
|
| 1135 |
-
.dev-download-btns
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
}
|
| 1141 |
.dev-sub-panel {
|
| 1142 |
right: 0;
|
| 1143 |
top: 100%;
|
|
|
|
| 769 |
padding: 8px 10px;
|
| 770 |
border: none;
|
| 771 |
background: transparent;
|
| 772 |
+
/* Higher-contrast text: was --text-secondary (#4B5563) which read as
|
| 773 |
+
low-contrast against the white panel. --text-primary (#111827) is
|
| 774 |
+
near-black and matches the contrast of the rest of the chat UI. */
|
| 775 |
+
color: var(--text-primary);
|
| 776 |
border-radius: 6px;
|
| 777 |
cursor: pointer;
|
| 778 |
font-size: 13px;
|
|
|
|
| 785 |
}
|
| 786 |
|
| 787 |
.dev-panel button:disabled {
|
| 788 |
+
/* Slightly less aggressive than 0.4 because the base text is now darker;
|
| 789 |
+
0.55 keeps disabled items legible-but-distinguishable. */
|
| 790 |
+
opacity: 0.55;
|
| 791 |
cursor: not-allowed;
|
| 792 |
}
|
| 793 |
|
| 794 |
.dev-panel-hint {
|
| 795 |
font-size: 10px;
|
| 796 |
+
/* Bumped from --text-muted (#9CA3AF) to --text-secondary (#4B5563)
|
| 797 |
+
so inline metadata is actually readable. */
|
| 798 |
+
color: var(--text-secondary);
|
| 799 |
margin-left: 6px;
|
| 800 |
font-style: italic;
|
| 801 |
}
|
|
|
|
| 811 |
font-weight: 600;
|
| 812 |
text-transform: uppercase;
|
| 813 |
letter-spacing: 0.05em;
|
| 814 |
+
/* Bumped from --text-muted to --text-secondary for the same reason. */
|
| 815 |
+
color: var(--text-secondary);
|
| 816 |
padding: 4px 10px 2px;
|
| 817 |
}
|
| 818 |
|
| 819 |
+
/* Indented row used for every actionable item inside a category, so
|
| 820 |
+
labels (left-aligned at 10px) read as clear group headers and the
|
| 821 |
+
items below them sit at a 22px hang. Matches the look of the
|
| 822 |
+
original .dev-panel-choice rows. */
|
| 823 |
+
.dev-panel-row {
|
| 824 |
+
padding-left: 22px !important;
|
| 825 |
+
display: flex !important;
|
| 826 |
+
align-items: center;
|
| 827 |
+
gap: 8px;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
/* Two-line variant: name on top, subtitle (model name) on bottom,
|
| 831 |
+
chevron right-aligned. The middle text column is flex:1, min-width:0
|
| 832 |
+
so the subtitle can ellipsis-truncate when it overflows. */
|
| 833 |
+
.dev-panel-row-stack {
|
| 834 |
+
align-items: stretch;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
.dev-panel-row-text {
|
| 838 |
+
flex: 1;
|
| 839 |
+
min-width: 0;
|
| 840 |
+
display: flex;
|
| 841 |
+
flex-direction: column;
|
| 842 |
+
align-items: flex-start;
|
| 843 |
+
gap: 2px;
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.dev-panel-row-name {
|
| 847 |
+
font-size: 13px;
|
| 848 |
+
color: var(--text-primary);
|
| 849 |
+
line-height: 1.25;
|
| 850 |
+
}
|
| 851 |
+
|
| 852 |
+
.dev-panel-row-sub {
|
| 853 |
+
font-size: 11px;
|
| 854 |
+
color: var(--text-tertiary);
|
| 855 |
+
font-style: italic;
|
| 856 |
+
white-space: nowrap;
|
| 857 |
+
overflow: hidden;
|
| 858 |
+
text-overflow: ellipsis;
|
| 859 |
+
max-width: 100%;
|
| 860 |
+
line-height: 1.2;
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
.dev-panel-choice {
|
| 864 |
padding-left: 22px !important;
|
| 865 |
display: flex !important;
|
|
|
|
| 1184 |
|
| 1185 |
/* ── Small tablet / large phone (<600px) ──────────────────────── */
|
| 1186 |
@media (max-width: 600px) {
|
| 1187 |
+
/* The standalone .dev-download-btns header strip and the
|
| 1188 |
+
.dev-panel-download-item mobile fallback inside the settings menu
|
| 1189 |
+
have been replaced by a dedicated <DownloadMenu> dropdown that
|
| 1190 |
+
works on all viewports, so the responsive fallback logic that used
|
| 1191 |
+
to live here is no longer needed. */
|
|
|
|
| 1192 |
.dev-sub-panel {
|
| 1193 |
right: 0;
|
| 1194 |
top: 100%;
|