Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions frontend/app/api/sessions/[session_id]/eeg_data/export/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { forwardToBackend } from '@/lib/backend-proxy';
import { NextRequest } from 'next/server';

export async function POST(
req: NextRequest,
{ params }: { params: { session_id: string } }
) {
const body = await req.text();

const response = await forwardToBackend({
method: 'POST',
path: `/api/sessions/${params.session_id}/eeg_data/export`,
body,
contentType: 'application/json',
});

// Return the CSV as plain text
const text = await response.text();
return new Response(text, {
status: response.status,
headers: {
'Content-Type': response.headers.get('Content-Type') ?? 'text/csv',
},
});
}
55 changes: 55 additions & 0 deletions frontend/app/api/sessions/[session_id]/eeg_data/import/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextRequest } from 'next/server';

const DEFAULT_API_BASES = [
process.env.SESSION_API_BASE_URL,
process.env.API_BASE_URL,
process.env.VITE_API_URL,
'http://api-server:9000',
'http://127.0.0.1:9000',
'http://localhost:9000',
].filter((v): v is string => Boolean(v));

export async function POST(
req: NextRequest,
{ params }: { params: { session_id: string } }
) {
const csvBody = await req.text();
const path = `/api/sessions/${params.session_id}/eeg_data/import`;

let lastError: unknown = null;
for (const baseUrl of DEFAULT_API_BASES) {
const url = `${baseUrl.replace(/\/$/, '')}${path}`;
try {
const backendResp = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'text/csv' },
body: csvBody,
cache: 'no-store',
});
const text = await backendResp.text();
return new Response(text, {
status: backendResp.status,
headers: {
'Content-Type':
backendResp.headers.get('Content-Type') ??
'application/json',
},
});
} catch (error) {
lastError = error;
}
}

const fallbackMessage =
lastError instanceof Error ? lastError.message : 'Unknown error';

return new Response(
JSON.stringify({
message: `Could not reach API backend: ${fallbackMessage}`,
}),
{
status: 503,
headers: { 'Content-Type': 'application/json' },
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { Card } from '@/components/ui/card';
import { Handle, Position, useReactFlow } from '@xyflow/react';
import { useGlobalContext } from '@/context/GlobalContext';
import useNodeData from '@/hooks/useNodeData';
import { ArrowUpRight } from 'lucide-react';
import React from 'react';
import { ArrowUpRight, Download } from 'lucide-react';
import React, { useState } from 'react';

import {
Dialog,
Expand All @@ -14,6 +14,7 @@ import {
DialogTrigger,
} from '@/components/ui/dialog';
import SignalGraphView from './signal-graph-full';
import ExportDialog from '@/components/ui/export-dialog';

export default function SignalGraphNode({ id }: { id?: string }) {
const { dataStreaming } = useGlobalContext();
Expand All @@ -22,6 +23,8 @@ export default function SignalGraphNode({ id }: { id?: string }) {
const processedData = renderData;
const reactFlowInstance = useReactFlow();
const [isConnected, setIsConnected] = React.useState(false);
const { activeSessionId } = useGlobalContext();
const [isExportOpen, setIsExportOpen] = useState(false);

// Determine if this Chart View node has an upstream path from a Source
const checkConnectionStatus = React.useCallback(() => {
Expand Down Expand Up @@ -98,14 +101,23 @@ export default function SignalGraphNode({ id }: { id?: string }) {
Chart View
</span>
{isConnected && (
<div className="w-full mt-[50px] transition-all duration-300 ease-in-out">
<div className="w-full mt-[50px] transition-all duration-300 ease-in-out flex items-center gap-3">
<DialogTrigger asChild>
<button
className="font-geist text-[14px] font-normal leading-tight text-black flex items-center gap-1 hover:opacity-80 transition"
onClick={(e) => e.stopPropagation()}>
Preview <ArrowUpRight size={14} className="transition-transform duration-200 hover:scale-110" />
</button>
</DialogTrigger>
<button
className="font-geist text-[14px] font-normal leading-tight text-black flex items-center gap-1 hover:opacity-80 transition"
onClick={(e) => {
e.stopPropagation();
setIsExportOpen(true);
}}
>
Export <Download size={14} className="transition-transform duration-200 hover:scale-110" />
</button>
</div>
)}
</div>
Expand All @@ -122,9 +134,9 @@ export default function SignalGraphNode({ id }: { id?: string }) {
</div>


<DialogContent
className="items-center justify-center w-screen h-screen max-w-none max-h-none"
style={{ backgroundColor : '#EAF1F0'}}
<DialogContent
className="items-center justify-center w-screen h-screen max-w-none max-h-none"
style={{ backgroundColor: '#EAF1F0' }}
>
<DialogHeader>
<DialogTitle></DialogTitle>
Expand All @@ -135,6 +147,13 @@ export default function SignalGraphNode({ id }: { id?: string }) {
</div>
</DialogContent>
</Card>

{/* Export dialog — outside the ReactFlow Dialog to avoid nesting */}
<ExportDialog
open={isExportOpen}
sessionId={activeSessionId}
onOpenChange={setIsExportOpen}
/>
</Dialog>
);
}
49 changes: 43 additions & 6 deletions frontend/components/ui-header/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import {
} from '@/components/ui/dropdown-menu';
import { useState } from 'react';
import Image from 'next/image';
import { useGlobalContext } from '@/context/GlobalContext';
import ExportDialog from '@/components/ui/export-dialog';
import ImportDialog from '@/components/ui/import-dialog';

export default function AppHeader() {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isExportOpen, setIsExportOpen] = useState(false);
const [isImportOpen, setIsImportOpen] = useState(false);
const { activeSessionId } = useGlobalContext();

return (
<header className="flex justify-between items-center p-2 border-b h-25">
Expand All @@ -28,21 +34,41 @@ export default function AppHeader() {
/>
</div>

{/* update, issues */}
{/* update, issues, import, export, help */}
<div className="flex h-full items-center space-x-4">
<Button
variant="link"
className="flex items-center space-x-1 px-3 py-2"
>
<span>Update</span>
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px'}} />
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
</Button>
<Button
variant="link"
className="flex items-center space-x-1 px-3 py-2"
>
<span>Issues</span>
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px'}} />
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
</Button>

{/* Export Data */}
<Button
variant="link"
className="flex items-center space-x-1 px-3 py-2"
onClick={() => setIsExportOpen(true)}
>
<span>Export Data</span>
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
</Button>

{/* Import Data */}
<Button
variant="link"
className="flex items-center space-x-1 px-3 py-2"
onClick={() => setIsImportOpen(true)}
>
<span>Import Data</span>
<MoveUpRight style={{ height: '10px', width: '10px', marginLeft: '-5px' }} />
</Button>

{/* help */}
Expand All @@ -53,9 +79,8 @@ export default function AppHeader() {
Help
</span>
<ChevronUpIcon
className={`h-4 w-4 transform transition-transform duration-300 ${
isOpen ? 'rotate-180' : 'rotate-0'
}`}
className={`h-4 w-4 transform transition-transform duration-300 ${isOpen ? 'rotate-180' : 'rotate-0'
}`}
/>
</div>
</DropdownMenuTrigger>
Expand All @@ -69,6 +94,18 @@ export default function AppHeader() {
</DropdownMenuContent>
</DropdownMenu>
</div>

{/* Dialogs */}
<ExportDialog
open={isExportOpen}
sessionId={activeSessionId}
onOpenChange={setIsExportOpen}
/>
<ImportDialog
open={isImportOpen}
sessionId={activeSessionId}
onOpenChange={setIsImportOpen}
/>
</header>
);
}
11 changes: 6 additions & 5 deletions frontend/components/ui-header/settings-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ export default function SettingsBar() {
</div>


{/* start/stop, reset, save, load */}
{/* start/stop, reset, import, export, save, load */}
<div className="flex space-x-2">
<Button
onClick={handleStartStop}
Expand Down Expand Up @@ -349,14 +349,15 @@ export default function SettingsBar() {
</Dialog>
<Button
variant="outline"

onClick={handleSaveClick}
disabled={isSaving || isLoading || isFetchingSessions}
>
{isSaving
? 'Saving...'
: fetchingFor === 'save'
? 'Preparing...'
: 'Save'}
? 'Preparing...'
: 'Save'}
</Button>
<Button
variant="outline"
Expand All @@ -366,8 +367,8 @@ export default function SettingsBar() {
{isLoading
? 'Loading...'
: fetchingFor === 'load'
? 'Preparing...'
: 'Load'}
? 'Preparing...'
: 'Load'}
</Button>
</div>

Expand Down
Loading
Loading