Skip to content

Commit 96ba133

Browse files
committed
ui changes, add popover for import assumptions, add export all option
1 parent 5cf7713 commit 96ba133

File tree

3 files changed

+114
-85
lines changed

3 files changed

+114
-85
lines changed

frontend/components/ui-header/app-header.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import { Button } from '@/components/ui/button';
33
import { ChevronUpIcon } from '@radix-ui/react-icons';
4-
import { MoveUpRight } from 'lucide-react';
4+
import { ArrowUpRight } from 'lucide-react';
55

66
import {
77
DropdownMenu,
@@ -22,7 +22,7 @@ export default function AppHeader() {
2222
const { activeSessionId } = useGlobalContext();
2323

2424
return (
25-
<header className="flex justify-between items-center p-2 border-b h-25">
25+
<header className="flex justify-between items-center px-2 py-3 border-b h-25">
2626
{/* logo */}
2727
<div className="flex items-center">
2828
<Image
@@ -34,41 +34,27 @@ export default function AppHeader() {
3434
/>
3535
</div>
3636

37-
{/* update, issues, import, export, help */}
37+
{/* import, export, help */}
3838
<div className="flex h-full items-center space-x-4">
39-
<Button
40-
variant="link"
41-
className="flex items-center space-x-1 px-3 py-2"
42-
>
43-
<span>Update</span>
44-
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
45-
</Button>
46-
<Button
47-
variant="link"
48-
className="flex items-center space-x-1 px-3 py-2"
49-
>
50-
<span>Issues</span>
51-
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
52-
</Button>
5339

5440
{/* Export Data */}
5541
<Button
5642
variant="link"
57-
className="flex items-center space-x-1 px-3 py-2"
43+
className="flex items-center gap-1 px-3 py-2"
5844
onClick={() => setIsExportOpen(true)}
5945
>
6046
<span>Export Data</span>
61-
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
47+
<ArrowUpRight size={14} className="transition-transform duration-200 hover:scale-110" />
6248
</Button>
6349

6450
{/* Import Data */}
6551
<Button
6652
variant="link"
67-
className="flex items-center space-x-1 px-3 py-2"
53+
className="flex items-center gap-1 px-3 py-2"
6854
onClick={() => setIsImportOpen(true)}
6955
>
7056
<span>Import Data</span>
71-
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
57+
<ArrowUpRight size={14} className="transition-transform duration-200 hover:scale-110" />
7258
</Button>
7359

7460
{/* help */}

frontend/components/ui/export-dialog.tsx

Lines changed: 88 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,21 @@ import {
1010
import { Button } from '@/components/ui/button';
1111
import { useNotifications } from '@/components/notifications';
1212
import { exportEEGData, downloadCSV } from '@/lib/eeg-api';
13+
import { ExitIcon } from '@radix-ui/react-icons';
1314

1415
type ExportDialogProps = {
1516
open: boolean;
1617
sessionId: number | null;
1718
onOpenChange: (open: boolean) => void;
1819
};
1920

20-
const ExportIcon = () => (
21-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="none" className="mr-3 h-6 w-6">
22-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" />
23-
<path fill="#fff" stroke="#fff" strokeWidth="2" strokeLinecap="square" strokeLinejoin="miter" d="M8 12h8M12 8l4 4-4 4" />
24-
</svg>
25-
);
26-
2721
export default function ExportDialog({
2822
open,
2923
sessionId,
3024
onOpenChange,
3125
}: ExportDialogProps) {
3226
const notifications = useNotifications();
27+
const [exportMode, setExportMode] = useState<'range' | 'all'>('range');
3328
const [durationValue, setDurationValue] = useState('30');
3429
const [durationUnit, setDurationUnit] = useState('Minutes');
3530
const [isExporting, setIsExporting] = useState(false);
@@ -49,29 +44,30 @@ export default function ExportDialog({
4944
return;
5045
}
5146

52-
const value = parseFloat(durationValue);
53-
if (isNaN(value) || value <= 0) {
54-
notifications.error({
55-
title: 'Invalid duration',
56-
description: 'Please enter a valid number greater than 0.',
57-
});
58-
return;
59-
}
47+
const options: Record<string, string> = {};
6048

61-
setIsExporting(true);
62-
try {
63-
const options: Record<string, string> = {};
49+
if (exportMode === 'range') {
50+
const value = parseFloat(durationValue);
51+
if (isNaN(value) || value <= 0) {
52+
notifications.error({
53+
title: 'Invalid duration',
54+
description: 'Please enter a valid number greater than 0.',
55+
});
56+
return;
57+
}
6458

65-
let multiplier = 1000; // default to seconds
59+
let multiplier = 1000;
6660
if (durationUnit === 'Minutes') multiplier = 60 * 1000;
6761
if (durationUnit === 'Hours') multiplier = 60 * 60 * 1000;
6862
if (durationUnit === 'Days') multiplier = 24 * 60 * 60 * 1000;
6963

70-
const durationMs = value * multiplier;
7164
const now = new Date();
72-
73-
options.start_time = new Date(now.getTime() - durationMs).toISOString();
65+
options.start_time = new Date(now.getTime() - value * multiplier).toISOString();
7466
options.end_time = now.toISOString();
67+
}
68+
69+
setIsExporting(true);
70+
try {
7571

7672
const csvContent = await exportEEGData(sessionId, options);
7773
downloadCSV(csvContent, sessionId);
@@ -92,47 +88,84 @@ export default function ExportDialog({
9288
<Dialog open={open} onOpenChange={handleClose}>
9389
<DialogContent className="sm:max-w-[400px] p-8">
9490
<DialogHeader className="mb-4 text-left">
95-
<DialogTitle className="flex items-center text-2xl font-bold mb-0">
96-
<ExportIcon />
91+
<DialogTitle className="flex items-center text-2xl font-bold mb-2">
92+
<ExitIcon className="mr-2" width={24} height={24} />
9793
Export Data
9894
</DialogTitle>
9995
</DialogHeader>
10096

10197
<div className="py-2">
102-
<p className="text-sm font-medium text-gray-900 mb-2">
103-
Export data from the last:
104-
</p>
105-
106-
<div className="flex gap-3 mb-4">
107-
<input
108-
type="number"
109-
min="1"
110-
value={durationValue}
111-
onChange={(e) => setDurationValue(e.target.value)}
98+
{/* Mode toggle */}
99+
<div className="flex rounded-lg border border-gray-200 overflow-hidden mb-4">
100+
<button
101+
type="button"
102+
onClick={() => setExportMode('range')}
112103
disabled={isExporting}
113-
className="flex h-10 w-24 rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
114-
/>
115-
<div className="relative flex-1">
116-
<select
117-
value={durationUnit}
118-
onChange={(e) => setDurationUnit(e.target.value)}
119-
disabled={isExporting}
120-
className="appearance-none flex h-10 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 pr-8 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
121-
>
122-
<option value="Seconds">Seconds</option>
123-
<option value="Minutes">Minutes</option>
124-
<option value="Hours">Hours</option>
125-
<option value="Days">Days</option>
126-
</select>
127-
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-500">
128-
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
129-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
130-
</svg>
131-
</div>
132-
</div>
104+
className={`flex-1 px-3 py-2 text-sm font-medium transition-colors ${exportMode === 'range' ? 'bg-gray-900 text-white' : 'bg-transparent text-gray-600 hover:bg-gray-50'} disabled:cursor-not-allowed disabled:opacity-50`}
105+
>
106+
Last duration
107+
</button>
108+
<button
109+
type="button"
110+
onClick={() => setExportMode('all')}
111+
disabled={isExporting}
112+
className={`flex-1 px-3 py-2 text-sm font-medium transition-colors ${exportMode === 'all' ? 'bg-gray-900 text-white' : 'bg-transparent text-gray-600 hover:bg-gray-50'} disabled:cursor-not-allowed disabled:opacity-50`}
113+
>
114+
All data
115+
</button>
116+
</div>
117+
118+
<div className="h-20">
119+
{exportMode === 'range' && (
120+
<>
121+
<p className="text-sm font-medium text-gray-900 mb-2">
122+
Export data from the last:
123+
</p>
124+
<div className="flex gap-3">
125+
<input
126+
type="number"
127+
min="1"
128+
value={durationValue}
129+
onChange={(e) => setDurationValue(e.target.value)}
130+
disabled={isExporting}
131+
className="flex h-10 w-24 rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
132+
/>
133+
<div className="relative flex-1">
134+
<select
135+
value={durationUnit}
136+
onChange={(e) => setDurationUnit(e.target.value)}
137+
disabled={isExporting}
138+
className="appearance-none flex h-10 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 pr-8 text-sm shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
139+
>
140+
<option value="Seconds">Seconds</option>
141+
<option value="Minutes">Minutes</option>
142+
<option value="Hours">Hours</option>
143+
<option value="Days">Days</option>
144+
</select>
145+
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-500">
146+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
147+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
148+
</svg>
149+
</div>
150+
</div>
151+
</div>
152+
</>
153+
)}
154+
155+
{exportMode === 'all' && (
156+
<p className="text-sm text-gray-500">
157+
Exports all recorded data for this session, from the earliest timestamp to now.
158+
</p>
159+
)}
133160
</div>
134161

135-
<hr className="my-6 border-gray-100" />
162+
<hr className="my-4 border-gray-100" />
163+
164+
{sessionId === null && (
165+
<p className="text-sm text-red-600 font-medium mb-3">
166+
No active session - please start or load a session before exporting.
167+
</p>
168+
)}
136169

137170
<p className="text-sm text-gray-400 font-medium mb-3">
138171
Data will be exported as CSV format.

frontend/components/ui/import-dialog.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
import { useNotifications } from '@/components/notifications';
1212
import { importEEGData } from '@/lib/eeg-api';
1313
import { Folder } from 'lucide-react';
14+
import { EnterIcon, InfoCircledIcon } from '@radix-ui/react-icons';
15+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
1416

1517
type ImportDialogProps = {
1618
open: boolean;
@@ -20,13 +22,6 @@ type ImportDialogProps = {
2022
onImportSuccess?: () => void;
2123
};
2224

23-
const ImportIcon = () => (
24-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="none" className="mr-3 h-6 w-6">
25-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z" />
26-
<path fill="#fff" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M12 12H4M8 8l-4 4 4 4" />
27-
</svg>
28-
);
29-
3025
export default function ImportDialog({
3126
open,
3227
sessionId,
@@ -119,11 +114,26 @@ export default function ImportDialog({
119114
<DialogContent className="sm:max-w-[425px] p-8">
120115
<DialogHeader className="mb-4 text-left">
121116
<DialogTitle className="flex items-center text-2xl font-bold mb-2">
122-
<ImportIcon />
117+
<EnterIcon className="mr-2" width={24} height={24} />
123118
Import Data
124119
</DialogTitle>
125-
<DialogDescription className="text-gray-500 text-sm">
120+
<DialogDescription className="text-gray-500 text-sm flex items-center gap-1.5">
126121
Only CSV files are accepted.
122+
<Popover>
123+
<PopoverTrigger asChild>
124+
<button type="button" className="inline-flex items-center text-gray-400 hover:text-gray-600 transition-colors">
125+
<InfoCircledIcon width={14} height={14} />
126+
</button>
127+
</PopoverTrigger>
128+
<PopoverContent className="w-90 text-sm" side="bottom" align="start">
129+
<p className="font-semibold mb-2">Expected CSV format</p>
130+
<p className="text-gray-500 mb-2">The file must have a header row followed by data rows in this shape:</p>
131+
<code className="block bg-gray-100 rounded px-2 py-1.5 text-xs font-mono mb-2">
132+
Time,Channel1,Channel2,Channel3,Channel4
133+
</code>
134+
<p className="text-gray-500">The <span className="font-medium text-gray-700">Time</span> column must be in <span className="font-medium text-gray-700">RFC 3339</span> format (e.g. <code className="text-xs font-mono">2024-01-15T13:45:00Z</code>).</p>
135+
</PopoverContent>
136+
</Popover>
127137
</DialogDescription>
128138
</DialogHeader>
129139

0 commit comments

Comments
 (0)