Jordan Miller Cursor commited on
Commit
958ebfa
·
1 Parent(s): 4944128

Improve mobile header and participants dropdown UX.

Browse files

Add a header overflow menu on narrow screens, show the participants picker as a bottom sheet, and surface Create Expert Persona at the top of the dropdown with updated helper copy.

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

frontend/src/components/Header.js CHANGED
@@ -4,6 +4,56 @@ import AuthBadge from './AuthBadge';
4
  import ParticipantDropdown from './ParticipantDropdown';
5
  import DownloadMenu from './DownloadMenu';
6
  import DevMenu from './DevMenu';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  /**
9
  * Header bar: brand on the left; on the right, auth badge, participant
@@ -97,41 +147,20 @@ export default function Header({
97
  autoSelectMode={autoSelectMode}
98
  onToggleAutoSelectMode={onToggleAutoSelectMode}
99
  />
100
- <button
101
- type="button"
102
- className={
103
- 'btn-sm btn-outline ccai-human-add-btn'
104
- + (humanParticipant ? ' ccai-human-add-btn-active' : '')
105
- }
106
- onClick={onOpenHumanModal}
107
- title={humanParticipant
108
- ? `Edit ${humanParticipant.name}'s credential summary`
109
- : 'Add a human participant to the conversation'}
110
- >
111
- {humanParticipant ? (
112
- <>
113
- <UserCheck size={14} style={{ marginRight: 4 }} />
114
- {humanParticipant.name}
115
- </>
116
- ) : (
117
- <>
118
- <UserPlus size={14} style={{ marginRight: 4 }} />
119
- Add a Human Participant
120
- </>
121
- )}
122
- </button>
123
- <button
124
- type="button"
125
- className="btn-sm btn-outline ccai-table-view-btn"
126
- onClick={onShowTableView}
127
- disabled={!hasChat}
128
- title={hasChat
129
- ? 'Open the conversation summary table'
130
- : 'Start a chat to view the summary table'}
131
- >
132
- <Table2 size={14} style={{ marginRight: 4 }} />
133
- Table View
134
- </button>
135
  <DownloadMenu
136
  hasChat={hasChat}
137
  hasApiLog={hasApiLog}
 
4
  import ParticipantDropdown from './ParticipantDropdown';
5
  import DownloadMenu from './DownloadMenu';
6
  import DevMenu from './DevMenu';
7
+ import HeaderMoreMenu from './HeaderMoreMenu';
8
+
9
+ function HumanParticipantButton({ humanParticipant, onOpenHumanModal, className }) {
10
+ return (
11
+ <button
12
+ type="button"
13
+ className={
14
+ 'btn-sm btn-outline ccai-human-add-btn header-actions-desktop'
15
+ + (className ? ` ${className}` : '')
16
+ + (humanParticipant ? ' ccai-human-add-btn-active' : '')
17
+ }
18
+ onClick={onOpenHumanModal}
19
+ title={humanParticipant
20
+ ? `Edit ${humanParticipant.name}'s credential summary`
21
+ : 'Add a human participant to the conversation'}
22
+ >
23
+ {humanParticipant ? (
24
+ <>
25
+ <UserCheck size={14} style={{ marginRight: 4 }} />
26
+ {humanParticipant.name}
27
+ </>
28
+ ) : (
29
+ <>
30
+ <UserPlus size={14} style={{ marginRight: 4 }} />
31
+ Add a Human Participant
32
+ </>
33
+ )}
34
+ </button>
35
+ );
36
+ }
37
+
38
+ function TableViewButton({ hasChat, onShowTableView, className }) {
39
+ return (
40
+ <button
41
+ type="button"
42
+ className={
43
+ 'btn-sm btn-outline ccai-table-view-btn header-actions-desktop'
44
+ + (className ? ` ${className}` : '')
45
+ }
46
+ onClick={onShowTableView}
47
+ disabled={!hasChat}
48
+ title={hasChat
49
+ ? 'Open the conversation summary table'
50
+ : 'Start a chat to view the summary table'}
51
+ >
52
+ <Table2 size={14} style={{ marginRight: 4 }} />
53
+ Table View
54
+ </button>
55
+ );
56
+ }
57
 
58
  /**
59
  * Header bar: brand on the left; on the right, auth badge, participant
 
147
  autoSelectMode={autoSelectMode}
148
  onToggleAutoSelectMode={onToggleAutoSelectMode}
149
  />
150
+ <HumanParticipantButton
151
+ humanParticipant={humanParticipant}
152
+ onOpenHumanModal={onOpenHumanModal}
153
+ />
154
+ <TableViewButton
155
+ hasChat={hasChat}
156
+ onShowTableView={onShowTableView}
157
+ />
158
+ <HeaderMoreMenu
159
+ humanParticipant={humanParticipant}
160
+ onOpenHumanModal={onOpenHumanModal}
161
+ hasChat={hasChat}
162
+ onShowTableView={onShowTableView}
163
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  <DownloadMenu
165
  hasChat={hasChat}
166
  hasApiLog={hasApiLog}
frontend/src/components/HeaderMoreMenu.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { MoreHorizontal, UserPlus, UserCheck, Table2 } from 'lucide-react';
3
+
4
+ /**
5
+ * Mobile header overflow for wide labeled actions (human participant,
6
+ * table view). Mirrors DownloadMenu's open/close pattern.
7
+ */
8
+ export default function HeaderMoreMenu({
9
+ humanParticipant,
10
+ onOpenHumanModal,
11
+ hasChat,
12
+ onShowTableView,
13
+ }) {
14
+ const [open, setOpen] = useState(false);
15
+ const wrapRef = useRef(null);
16
+
17
+ useEffect(() => {
18
+ function handleClickOutside(e) {
19
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) {
20
+ setOpen(false);
21
+ }
22
+ }
23
+ document.addEventListener('mousedown', handleClickOutside);
24
+ return () => document.removeEventListener('mousedown', handleClickOutside);
25
+ }, []);
26
+
27
+ const fire = (fn) => () => { fn?.(); setOpen(false); };
28
+
29
+ return (
30
+ <div className="dev-wrap header-actions-mobile" ref={wrapRef}>
31
+ <div className="dev-dropdown-header">
32
+ <button
33
+ type="button"
34
+ className="icon-btn"
35
+ onClick={() => setOpen(o => !o)}
36
+ title="More actions"
37
+ aria-label="More actions"
38
+ aria-expanded={open}
39
+ >
40
+ <MoreHorizontal size={16} />
41
+ </button>
42
+ {open && (
43
+ <div className="dev-panel">
44
+ <button
45
+ type="button"
46
+ className={
47
+ 'dev-panel-row'
48
+ + (humanParticipant ? ' ccai-human-add-btn-active' : '')
49
+ }
50
+ onClick={fire(onOpenHumanModal)}
51
+ title={humanParticipant
52
+ ? `Edit ${humanParticipant.name}'s credential summary`
53
+ : 'Add a human participant to the conversation'}
54
+ >
55
+ {humanParticipant ? (
56
+ <>
57
+ <UserCheck size={14} className="dev-check-icon" />
58
+ {humanParticipant.name}
59
+ </>
60
+ ) : (
61
+ <>
62
+ <UserPlus size={14} className="dev-check-icon" />
63
+ Add a Human Participant
64
+ </>
65
+ )}
66
+ </button>
67
+ <button
68
+ type="button"
69
+ className="dev-panel-row"
70
+ disabled={!hasChat}
71
+ onClick={fire(onShowTableView)}
72
+ title={hasChat
73
+ ? 'Open the conversation summary table'
74
+ : 'Start a chat to view the summary table'}
75
+ >
76
+ <Table2 size={14} className="dev-check-icon" />
77
+ Table View
78
+ </button>
79
+ </div>
80
+ )}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
frontend/src/components/ParticipantDropdown.js CHANGED
@@ -45,6 +45,11 @@ export default function ParticipantDropdown({
45
  // orchestrator picks at start time, so user picks are ignored.
46
  const checkboxDisabledForAuto = !!autoSelectMode;
47
 
 
 
 
 
 
48
  return (
49
  <div className="ccai-dropdown-wrap" ref={ref}>
50
  <button
@@ -65,7 +70,14 @@ export default function ParticipantDropdown({
65
  <ChevronDown size={12} />
66
  </button>
67
  {open && (
68
- <div className="ccai-dropdown-panel">
 
 
 
 
 
 
 
69
  <div className="ccai-dropdown-section">
70
  <button
71
  type="button"
@@ -86,8 +98,9 @@ export default function ParticipantDropdown({
86
  <div className="ccai-dropdown-autoselect-help">
87
  {autoSelectMode
88
  ? 'The orchestrator will pick the most relevant participants for your question when you start the chat.'
89
- : 'Or pick manually from the lists below.'}
90
  </div>
 
91
  </div>
92
  <div className="ccai-dropdown-divider" />
93
  <div
@@ -164,20 +177,31 @@ export default function ParticipantDropdown({
164
  onToggle={() => onToggleParticipant(p, 'expert')}
165
  />
166
  ))}
167
- <button
168
- className="ccai-dropdown-create-btn"
169
- onClick={() => { setOpen(false); onOpenExpertModal(null); }}
170
- >
171
- <Plus size={12} />
172
- Create Expert Persona...
173
- </button>
174
  </div>
175
- </div>
 
176
  )}
177
  </div>
178
  );
179
  }
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  function DropdownItem({ participant, checked, disabledForAdd, autoSelectActive, onToggle }) {
182
  return (
183
  <label
 
45
  // orchestrator picks at start time, so user picks are ignored.
46
  const checkboxDisabledForAuto = !!autoSelectMode;
47
 
48
+ const openCreateExpertModal = () => {
49
+ setOpen(false);
50
+ onOpenExpertModal(null);
51
+ };
52
+
53
  return (
54
  <div className="ccai-dropdown-wrap" ref={ref}>
55
  <button
 
70
  <ChevronDown size={12} />
71
  </button>
72
  {open && (
73
+ <>
74
+ <button
75
+ type="button"
76
+ className="ccai-dropdown-backdrop"
77
+ aria-label="Close participants menu"
78
+ onClick={() => setOpen(false)}
79
+ />
80
+ <div className="ccai-dropdown-panel">
81
  <div className="ccai-dropdown-section">
82
  <button
83
  type="button"
 
98
  <div className="ccai-dropdown-autoselect-help">
99
  {autoSelectMode
100
  ? 'The orchestrator will pick the most relevant participants for your question when you start the chat.'
101
+ : 'Create or pick manually from the lists below.'}
102
  </div>
103
+ <CreateExpertPersonaButton onClick={openCreateExpertModal} />
104
  </div>
105
  <div className="ccai-dropdown-divider" />
106
  <div
 
177
  onToggle={() => onToggleParticipant(p, 'expert')}
178
  />
179
  ))}
180
+ <CreateExpertPersonaButton onClick={openCreateExpertModal} />
 
 
 
 
 
 
181
  </div>
182
+ </div>
183
+ </>
184
  )}
185
  </div>
186
  );
187
  }
188
 
189
+ function CreateExpertPersonaButton({ onClick, className }) {
190
+ return (
191
+ <button
192
+ type="button"
193
+ className={
194
+ 'ccai-dropdown-create-btn'
195
+ + (className ? ` ${className}` : '')
196
+ }
197
+ onClick={onClick}
198
+ >
199
+ <Plus size={12} />
200
+ Create Expert Persona...
201
+ </button>
202
+ );
203
+ }
204
+
205
  function DropdownItem({ participant, checked, disabledForAdd, autoSelectActive, onToggle }) {
206
  return (
207
  <label
frontend/src/styles/ccai.css CHANGED
@@ -10,6 +10,10 @@
10
 
11
  .ccai-dropdown-wrap { position: relative; }
12
 
 
 
 
 
13
  .ccai-dropdown-trigger {
14
  display: inline-flex;
15
  align-items: center;
@@ -1193,6 +1197,33 @@
1193
  }
1194
 
1195
  @media (max-width: 600px) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1196
  .ccai-table-card {
1197
  max-height: 100vh;
1198
  border-radius: 0;
 
10
 
11
  .ccai-dropdown-wrap { position: relative; }
12
 
13
+ .ccai-dropdown-backdrop {
14
+ display: none;
15
+ }
16
+
17
  .ccai-dropdown-trigger {
18
  display: inline-flex;
19
  align-items: center;
 
1197
  }
1198
 
1199
  @media (max-width: 600px) {
1200
+ .ccai-dropdown-backdrop {
1201
+ display: block;
1202
+ position: fixed;
1203
+ inset: 0;
1204
+ border: none;
1205
+ padding: 0;
1206
+ margin: 0;
1207
+ background: rgba(0, 0, 0, 0.45);
1208
+ z-index: 59;
1209
+ cursor: default;
1210
+ }
1211
+
1212
+ .ccai-dropdown-panel {
1213
+ position: fixed;
1214
+ left: 0;
1215
+ right: 0;
1216
+ bottom: 0;
1217
+ top: auto;
1218
+ width: 100%;
1219
+ max-width: none;
1220
+ max-height: min(85vh, calc(100dvh - 64px));
1221
+ border-radius: 16px 16px 0 0;
1222
+ border-bottom: none;
1223
+ padding-bottom: max(10px, env(safe-area-inset-bottom));
1224
+ box-shadow: var(--shadow-lg);
1225
+ }
1226
+
1227
  .ccai-table-card {
1228
  max-height: 100vh;
1229
  border-radius: 0;
frontend/src/styles/layout.css CHANGED
@@ -72,6 +72,14 @@
72
  flex-shrink: 0;
73
  }
74
 
 
 
 
 
 
 
 
 
75
  .icon-btn {
76
  display: flex;
77
  align-items: center;
@@ -144,6 +152,32 @@
144
  }
145
  }
146
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  /* ── Phone ────────────────────────────────────────────────────── */
148
  @media (max-width: 480px) {
149
  .app-header {
 
72
  flex-shrink: 0;
73
  }
74
 
75
+ .header-actions-mobile {
76
+ display: none;
77
+ }
78
+
79
+ .header-actions-desktop {
80
+ display: inline-flex;
81
+ }
82
+
83
  .icon-btn {
84
  display: flex;
85
  align-items: center;
 
152
  }
153
  }
154
 
155
+ /* ── Small tablet / large phone (<600px) ──────────────────────── */
156
+ @media (max-width: 600px) {
157
+ .app-header {
158
+ flex-wrap: wrap;
159
+ gap: 8px;
160
+ }
161
+ .header-left {
162
+ flex: 1 1 auto;
163
+ min-width: 0;
164
+ }
165
+ .header-right {
166
+ flex: 1 1 100%;
167
+ flex-wrap: wrap;
168
+ justify-content: flex-end;
169
+ row-gap: 6px;
170
+ max-width: 100%;
171
+ min-width: 0;
172
+ }
173
+ .header-actions-desktop {
174
+ display: none !important;
175
+ }
176
+ .header-actions-mobile {
177
+ display: block;
178
+ }
179
+ }
180
+
181
  /* ── Phone ────────────────────────────────────────────────────── */
182
  @media (max-width: 480px) {
183
  .app-header {