SarahXia0405 commited on
Commit
a720e8f
·
verified ·
1 Parent(s): 5757bc6

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +217 -194
web/src/App.tsx CHANGED
@@ -7,9 +7,10 @@ import { RightPanel } from './components/RightPanel';
7
  import { FloatingActionButtons } from './components/FloatingActionButtons';
8
  import { LoginScreen } from './components/LoginScreen';
9
  import { ProfileEditor } from './components/ProfileEditor';
10
- import { X, ChevronLeft, ChevronRight } from 'lucide-react';
11
  import { Button } from './components/ui/button';
12
  import { Toaster } from './components/ui/sonner';
 
13
  import { toast } from 'sonner';
14
 
15
  import {
@@ -30,7 +31,7 @@ export interface Message {
30
  content: string;
31
  timestamp: Date;
32
  references?: string[];
33
- sender?: GroupMember;
34
  }
35
 
36
  export interface User {
@@ -82,6 +83,8 @@ function App() {
82
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
83
  const [language, setLanguage] = useState<Language>('auto');
84
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
 
 
85
  const [memoryProgress] = useState(36);
86
 
87
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
@@ -95,6 +98,7 @@ function App() {
95
  const [exportResult, setExportResult] = useState('');
96
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
97
 
 
98
  const [groupMembers] = useState<GroupMember[]>([
99
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
100
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
@@ -109,7 +113,12 @@ function App() {
109
  if (user) {
110
  const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
111
  setWorkspaces([
112
- { id: 'individual', name: 'My Space', type: 'individual', avatar: userAvatar },
 
 
 
 
 
113
  {
114
  id: 'group-1',
115
  name: 'CS 101 Study Group',
@@ -142,7 +151,9 @@ function App() {
142
  if (!content.trim() || !user) return;
143
 
144
  const sender: GroupMember | undefined =
145
- spaceType === 'group' ? { id: user.email, name: user.name, email: user.email } : undefined;
 
 
146
 
147
  const userMessage: Message = {
148
  id: crypto.randomUUID(),
@@ -158,16 +169,14 @@ function App() {
158
  if (!shouldAIRespond) return;
159
 
160
  const assistantId = crypto.randomUUID();
161
- setMessages((prev) => [
162
- ...prev,
163
- {
164
- id: assistantId,
165
- role: 'assistant',
166
- content: 'Thinking...',
167
- timestamp: new Date(),
168
- sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
169
- },
170
- ]);
171
 
172
  try {
173
  const data = await apiChat({
@@ -186,14 +195,20 @@ function App() {
186
  setMessages((prev) =>
187
  prev.map((m) =>
188
  m.id === assistantId
189
- ? { ...m, content: data.reply || '', references: references.length ? references : undefined }
 
 
 
 
190
  : m
191
  )
192
  );
193
  } catch (err: any) {
194
  setMessages((prev) =>
195
  prev.map((m) =>
196
- m.id === assistantId ? { ...m, content: `Sorry — request failed.\n${err?.message ?? String(err)}` } : m
 
 
197
  )
198
  );
199
  }
@@ -202,7 +217,11 @@ function App() {
202
  const handleFileUpload = async (files: File[]) => {
203
  if (!user) return;
204
 
205
- const newFiles: UploadedFile[] = files.map((file) => ({ file, type: 'other' }));
 
 
 
 
206
  setUploadedFiles((prev) => [...prev, ...newFiles]);
207
 
208
  for (const f of files) {
@@ -227,7 +246,11 @@ function App() {
227
  if (!target) return;
228
 
229
  try {
230
- const r = await apiUpload({ user: asApiUser(user), file: target.file, fileType: type });
 
 
 
 
231
  toast.success(r.status_md || `Updated type: ${target.file.name}`);
232
  } catch (e: any) {
233
  toast.error(e?.message ?? `Failed to update type: ${target.file.name}`);
@@ -287,9 +310,8 @@ function App() {
287
  }
288
 
289
  return (
290
- <div className="h-dvh bg-background flex flex-col overflow-hidden">
291
  <Toaster />
292
-
293
  <Header
294
  user={user}
295
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
@@ -307,199 +329,200 @@ function App() {
307
  <ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
308
  )}
309
 
310
- {/* ✅ 关键:这里必须 min-h-0,才能把可用高度传下去 */}
311
- <div className="flex-1 min-h-0 overflow-hidden">
312
- {/* ✅ 三栏用 grid:中间列 minmax(0,1fr) 最稳 */}
313
- <div className="h-full min-h-0 overflow-hidden grid grid-cols-1 lg:grid-cols-[20rem_minmax(0,1fr)_20rem]">
314
- {/* Left overlay (mobile) */}
315
- {leftSidebarOpen && (
316
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
317
- )}
318
-
319
- {/* Left (desktop) */}
320
- {leftPanelVisible ? (
321
- <aside className="hidden lg:flex bg-card border-r border-border min-h-0 overflow-hidden relative">
322
- <Button
323
- variant="secondary"
324
- size="icon"
325
- onClick={() => setLeftPanelVisible(false)}
326
- className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
327
- style={{ right: '-10px' }}
328
- title="Close panel"
329
- >
330
- <ChevronLeft className="h-3 w-3" />
331
- </Button>
332
-
333
- <div className="flex-1 min-h-0 overflow-hidden">
334
- <LeftSidebar
335
- learningMode={learningMode}
336
- language={language}
337
- onLearningModeChange={setLearningMode}
338
- onLanguageChange={setLanguage}
339
- spaceType={spaceType}
340
- groupMembers={groupMembers}
341
- user={user}
342
- onLogin={setUser}
343
- onLogout={() => setUser(null)}
344
- isLoggedIn={!!user}
345
- onEditProfile={() => setShowProfileEditor(true)}
346
- />
347
- </div>
348
- </aside>
349
- ) : (
350
  <Button
351
  variant="secondary"
352
  size="icon"
353
- onClick={() => setLeftPanelVisible(true)}
354
- className="hidden lg:flex fixed top-20 left-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
355
- title="Open panel"
 
356
  >
357
- <ChevronRight className="h-3 w-3" />
358
  </Button>
359
- )}
360
-
361
- {/* Left (mobile) */}
362
- <aside
363
- className={`
364
- fixed lg:hidden top-16 bottom-0 left-0 z-50
365
- w-80 bg-card border-r border-border
366
- transform transition-transform duration-300 ease-in-out
367
- ${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
368
- flex flex-col min-h-0 overflow-hidden
369
- `}
370
- >
371
- <div className="p-4 border-b border-border flex justify-between items-center">
372
- <h3>Settings & Guide</h3>
373
- <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
374
- <X className="h-5 w-5" />
375
- </Button>
376
- </div>
377
- <div className="flex-1 min-h-0 overflow-hidden">
378
- <LeftSidebar
379
- learningMode={learningMode}
380
- language={language}
381
- onLearningModeChange={setLearningMode}
382
- onLanguageChange={setLanguage}
383
- spaceType={spaceType}
384
- groupMembers={groupMembers}
385
- user={user}
386
- onLogin={setUser}
387
- onLogout={() => setUser(null)}
388
- isLoggedIn={!!user}
389
- onEditProfile={() => setShowProfileEditor(true)}
390
- />
391
- </div>
392
- </aside>
393
-
394
- {/* ✅ Center:必须 min-h-0 + overflow-hidden(不滚),滚动交给 ChatArea */}
395
- <main className="min-w-0 min-h-0 overflow-hidden flex">
396
- <ChatArea
397
- user={asApiUser(user)}
398
- messages={messages}
399
- onSendMessage={handleSendMessage}
400
- uploadedFiles={uploadedFiles}
401
- onFileUpload={handleFileUpload}
402
- onRemoveFile={handleRemoveFile}
403
- onFileTypeChange={handleFileTypeChange}
404
- memoryProgress={memoryProgress}
405
- isLoggedIn={!!user}
406
  learningMode={learningMode}
407
- onClearConversation={handleClearConversation}
408
  onLearningModeChange={setLearningMode}
 
409
  spaceType={spaceType}
 
 
 
 
 
 
410
  />
411
- </main>
412
-
413
- {/* Right overlay (mobile) */}
414
- {rightPanelOpen && (
415
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
416
- )}
417
-
418
- {/* Right (desktop) */}
419
- {rightPanelVisible ? (
420
- <aside className="hidden lg:flex bg-card border-l border-border min-h-0 overflow-hidden relative">
421
- <Button
422
- variant="secondary"
423
- size="icon"
424
- onClick={() => setRightPanelVisible(false)}
425
- className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
426
- style={{ left: '-10px' }}
427
- title="Close panel"
428
- >
429
- <ChevronRight className="h-3 w-3" />
430
- </Button>
431
-
432
- <div className="flex-1 min-h-0 overflow-hidden">
433
- <RightPanel
434
- user={user}
435
- onLogin={setUser}
436
- onLogout={() => setUser(null)}
437
- isLoggedIn={!!user}
438
- onClose={() => setRightPanelVisible(false)}
439
- exportResult={exportResult}
440
- setExportResult={setExportResult}
441
- resultType={resultType}
442
- setResultType={setResultType}
443
- onExport={handleExport}
444
- onSummary={handleSummary}
445
- />
446
- </div>
447
- </aside>
448
- ) : (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
  <Button
450
  variant="secondary"
451
  size="icon"
452
- onClick={() => setRightPanelVisible(true)}
453
- className="hidden lg:flex fixed top-20 right-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
454
- title="Open panel"
 
455
  >
456
- <ChevronLeft className="h-3 w-3" />
457
  </Button>
458
- )}
459
-
460
- {/* Right (mobile) */}
461
- <aside
462
- className={`
463
- fixed lg:hidden top-16 bottom-0 right-0 z-50
464
- w-80 bg-card border-l border-border
465
- transform transition-transform duration-300 ease-in-out
466
- ${rightPanelOpen ? 'translate-x-0' : 'translate-x-full'}
467
- flex flex-col min-h-0 overflow-hidden
468
- `}
469
- >
470
- <div className="p-4 border-b border-border flex justify-between items-center">
471
- <h3>Account & Actions</h3>
472
- <Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
473
- <X className="h-5 w-5" />
474
- </Button>
475
- </div>
476
- <div className="flex-1 min-h-0 overflow-hidden">
477
- <RightPanel
478
- user={user}
479
- onLogin={setUser}
480
- onLogout={() => setUser(null)}
481
- isLoggedIn={!!user}
482
- onClose={() => setRightPanelVisible(false)}
483
- exportResult={exportResult}
484
- setExportResult={setExportResult}
485
- resultType={resultType}
486
- setResultType={setResultType}
487
- onExport={handleExport}
488
- onSummary={handleSummary}
489
- />
490
- </div>
491
- </aside>
492
-
493
- {!rightPanelVisible && (
494
- <FloatingActionButtons
495
  user={user}
 
 
496
  isLoggedIn={!!user}
497
- onOpenPanel={() => setRightPanelVisible(true)}
 
 
 
 
498
  onExport={handleExport}
499
  onSummary={handleSummary}
500
  />
501
- )}
502
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  </div>
504
  </div>
505
  );
 
7
  import { FloatingActionButtons } from './components/FloatingActionButtons';
8
  import { LoginScreen } from './components/LoginScreen';
9
  import { ProfileEditor } from './components/ProfileEditor';
10
+ import { X } from 'lucide-react';
11
  import { Button } from './components/ui/button';
12
  import { Toaster } from './components/ui/sonner';
13
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
14
  import { toast } from 'sonner';
15
 
16
  import {
 
31
  content: string;
32
  timestamp: Date;
33
  references?: string[];
34
+ sender?: GroupMember; // For group chat
35
  }
36
 
37
  export interface User {
 
83
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
84
  const [language, setLanguage] = useState<Language>('auto');
85
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
86
+
87
+ // You can later wire this to /api/memoryline
88
  const [memoryProgress] = useState(36);
89
 
90
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
 
98
  const [exportResult, setExportResult] = useState('');
99
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
100
 
101
+ // Mock group members (still fine; AI responder uses backend now)
102
  const [groupMembers] = useState<GroupMember[]>([
103
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
104
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
 
113
  if (user) {
114
  const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
115
  setWorkspaces([
116
+ {
117
+ id: 'individual',
118
+ name: 'My Space',
119
+ type: 'individual',
120
+ avatar: userAvatar,
121
+ },
122
  {
123
  id: 'group-1',
124
  name: 'CS 101 Study Group',
 
151
  if (!content.trim() || !user) return;
152
 
153
  const sender: GroupMember | undefined =
154
+ spaceType === 'group'
155
+ ? { id: user.email, name: user.name, email: user.email }
156
+ : undefined;
157
 
158
  const userMessage: Message = {
159
  id: crypto.randomUUID(),
 
169
  if (!shouldAIRespond) return;
170
 
171
  const assistantId = crypto.randomUUID();
172
+ const assistantPlaceholder: Message = {
173
+ id: assistantId,
174
+ role: 'assistant',
175
+ content: 'Thinking...',
176
+ timestamp: new Date(),
177
+ sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
178
+ };
179
+ setMessages((prev) => [...prev, assistantPlaceholder]);
 
 
180
 
181
  try {
182
  const data = await apiChat({
 
195
  setMessages((prev) =>
196
  prev.map((m) =>
197
  m.id === assistantId
198
+ ? {
199
+ ...m,
200
+ content: data.reply || '',
201
+ references: references.length ? references : undefined,
202
+ }
203
  : m
204
  )
205
  );
206
  } catch (err: any) {
207
  setMessages((prev) =>
208
  prev.map((m) =>
209
+ m.id === assistantId
210
+ ? { ...m, content: `Sorry — request failed.\n${err?.message ?? String(err)}` }
211
+ : m
212
  )
213
  );
214
  }
 
217
  const handleFileUpload = async (files: File[]) => {
218
  if (!user) return;
219
 
220
+ const newFiles: UploadedFile[] = files.map((file) => ({
221
+ file,
222
+ type: 'other',
223
+ }));
224
+
225
  setUploadedFiles((prev) => [...prev, ...newFiles]);
226
 
227
  for (const f of files) {
 
246
  if (!target) return;
247
 
248
  try {
249
+ const r = await apiUpload({
250
+ user: asApiUser(user),
251
+ file: target.file,
252
+ fileType: type,
253
+ });
254
  toast.success(r.status_md || `Updated type: ${target.file.name}`);
255
  } catch (e: any) {
256
  toast.error(e?.message ?? `Failed to update type: ${target.file.name}`);
 
310
  }
311
 
312
  return (
313
+ <div className="min-h-screen bg-background flex flex-col">
314
  <Toaster />
 
315
  <Header
316
  user={user}
317
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
 
329
  <ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
330
  )}
331
 
332
+ <div
333
+ className="flex-1 flex overflow-hidden"
334
+ onWheel={(e) => e.stopPropagation()}
335
+ style={{ overscrollBehavior: 'none' }}
336
+ >
337
+ {/* Mobile Sidebar Toggle - Left */}
338
+ {leftSidebarOpen && (
339
+ <div
340
+ className="fixed inset-0 bg-black/50 z-40 lg:hidden"
341
+ onClick={() => setLeftSidebarOpen(false)}
342
+ />
343
+ )}
344
+
345
+ {/* Left Sidebar */}
346
+ {leftPanelVisible ? (
347
+ <aside className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  <Button
349
  variant="secondary"
350
  size="icon"
351
+ onClick={() => setLeftPanelVisible(false)}
352
+ className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
353
+ style={{ right: '-10px' }}
354
+ title="Close panel"
355
  >
356
+ <ChevronLeft className="h-3 w-3" />
357
  </Button>
358
+ <LeftSidebar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  learningMode={learningMode}
360
+ language={language}
361
  onLearningModeChange={setLearningMode}
362
+ onLanguageChange={setLanguage}
363
  spaceType={spaceType}
364
+ groupMembers={groupMembers}
365
+ user={user}
366
+ onLogin={setUser}
367
+ onLogout={() => setUser(null)}
368
+ isLoggedIn={!!user}
369
+ onEditProfile={() => setShowProfileEditor(true)}
370
  />
371
+ </aside>
372
+ ) : (
373
+ <Button
374
+ variant="secondary"
375
+ size="icon"
376
+ onClick={() => setLeftPanelVisible(true)}
377
+ className="hidden lg:flex fixed top-20 left-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
378
+ title="Open panel"
379
+ >
380
+ <ChevronRight className="h-3 w-3" />
381
+ </Button>
382
+ )}
383
+
384
+ {/* Left Sidebar - Mobile */}
385
+ <aside
386
+ className={`
387
+ fixed lg:hidden inset-y-0 left-0 z-50
388
+ w-80 bg-card border-r border-border
389
+ transform transition-transform duration-300 ease-in-out
390
+ ${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
391
+ flex flex-col
392
+ mt-16
393
+ h-[calc(100vh-4rem)]
394
+ min-h-0
395
+ `}
396
+ >
397
+ <div className="p-4 border-b border-border flex justify-between items-center">
398
+ <h3>Settings & Guide</h3>
399
+ <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
400
+ <X className="h-5 w-5" />
401
+ </Button>
402
+ </div>
403
+ <LeftSidebar
404
+ learningMode={learningMode}
405
+ language={language}
406
+ onLearningModeChange={setLearningMode}
407
+ onLanguageChange={setLanguage}
408
+ spaceType={spaceType}
409
+ groupMembers={groupMembers}
410
+ user={user}
411
+ onLogin={setUser}
412
+ onLogout={() => setUser(null)}
413
+ isLoggedIn={!!user}
414
+ onEditProfile={() => setShowProfileEditor(true)}
415
+ />
416
+ </aside>
417
+
418
+ {/* Main Chat Area */}
419
+ <main className="flex-1 flex flex-col min-w-0 min-h-0 h-full">
420
+ <ChatArea
421
+ // ✅ NEW: pass ApiUser down so Message can submit feedback
422
+ user={asApiUser(user)}
423
+ messages={messages}
424
+ onSendMessage={handleSendMessage}
425
+ uploadedFiles={uploadedFiles}
426
+ onFileUpload={handleFileUpload}
427
+ onRemoveFile={handleRemoveFile}
428
+ onFileTypeChange={handleFileTypeChange}
429
+ memoryProgress={memoryProgress}
430
+ isLoggedIn={!!user}
431
+ learningMode={learningMode}
432
+ onClearConversation={handleClearConversation}
433
+ onLearningModeChange={setLearningMode}
434
+ spaceType={spaceType}
435
+ />
436
+ </main>
437
+
438
+ {/* Mobile Sidebar Toggle - Right */}
439
+ {rightPanelOpen && (
440
+ <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
441
+ )}
442
+
443
+ {/* Right Panel */}
444
+ {rightPanelVisible ? (
445
+ <aside className="hidden lg:flex w-80 bg-card border-l border-border flex-col min-h-0 relative" style={{ height: 'calc(100vh - 4rem)' }}>
446
  <Button
447
  variant="secondary"
448
  size="icon"
449
+ onClick={() => setRightPanelVisible(false)}
450
+ className="absolute top-4 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
451
+ style={{ left: '-10px' }}
452
+ title="Close panel"
453
  >
454
+ <ChevronRight className="h-3 w-3" />
455
  </Button>
456
+ <RightPanel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  user={user}
458
+ onLogin={setUser}
459
+ onLogout={() => setUser(null)}
460
  isLoggedIn={!!user}
461
+ onClose={() => setRightPanelVisible(false)}
462
+ exportResult={exportResult}
463
+ setExportResult={setExportResult}
464
+ resultType={resultType}
465
+ setResultType={setResultType}
466
  onExport={handleExport}
467
  onSummary={handleSummary}
468
  />
469
+ </aside>
470
+ ) : (
471
+ <Button
472
+ variant="secondary"
473
+ size="icon"
474
+ onClick={() => setRightPanelVisible(true)}
475
+ className="hidden lg:flex fixed top-20 right-0 z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
476
+ title="Open panel"
477
+ >
478
+ <ChevronLeft className="h-3 w-3" />
479
+ </Button>
480
+ )}
481
+
482
+ {/* Right Panel - Mobile */}
483
+ <aside
484
+ className={`
485
+ fixed lg:hidden inset-y-0 right-0 z-50
486
+ w-80 bg-card border-l border-border
487
+ transform transition-transform duration-300 ease-in-out
488
+ ${rightPanelOpen ? 'translate-x-0' : 'translate-x-full'}
489
+ flex flex-col
490
+ mt-16
491
+ h-[calc(100vh-4rem)]
492
+ min-h-0
493
+ `}
494
+ >
495
+ <div className="p-4 border-b border-border flex justify-between items-center">
496
+ <h3>Account & Actions</h3>
497
+ <Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
498
+ <X className="h-5 w-5" />
499
+ </Button>
500
+ </div>
501
+ <RightPanel
502
+ user={user}
503
+ onLogin={setUser}
504
+ onLogout={() => setUser(null)}
505
+ isLoggedIn={!!user}
506
+ onClose={() => setRightPanelVisible(false)}
507
+ exportResult={exportResult}
508
+ setExportResult={setExportResult}
509
+ resultType={resultType}
510
+ setResultType={setResultType}
511
+ onExport={handleExport}
512
+ onSummary={handleSummary}
513
+ />
514
+ </aside>
515
+
516
+ {/* Floating Action Buttons - Desktop only, when panel is closed */}
517
+ {!rightPanelVisible && (
518
+ <FloatingActionButtons
519
+ user={user}
520
+ isLoggedIn={!!user}
521
+ onOpenPanel={() => setRightPanelVisible(true)}
522
+ onExport={handleExport}
523
+ onSummary={handleSummary}
524
+ />
525
+ )}
526
  </div>
527
  </div>
528
  );