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
36 changes: 36 additions & 0 deletions frontend/app/api/sessions/[session_id]/frontend-state/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { forwardToBackend, passthroughJsonResponse } from '@/lib/backend-proxy';

export async function GET(
_req: Request,
context: {
params: { session_id: string } | Promise<{ session_id: string }>;
}
) {
const params = await Promise.resolve(context.params);

const response = await forwardToBackend({
method: 'GET',
path: `/api/sessions/${params.session_id}/frontend-state`,
});

return passthroughJsonResponse(response);
}

export async function POST(
req: Request,
context: {
params: { session_id: string } | Promise<{ session_id: string }>;
}
) {
const params = await Promise.resolve(context.params);
const body = await req.text();

const response = await forwardToBackend({
method: 'POST',
path: `/api/sessions/${params.session_id}/frontend-state`,
body,
});

return passthroughJsonResponse(response);
}

23 changes: 23 additions & 0 deletions frontend/app/api/sessions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { forwardToBackend, passthroughJsonResponse } from '@/lib/backend-proxy';

export async function GET() {
const response = await forwardToBackend({
method: 'GET',
path: '/api/sessions',
});

return passthroughJsonResponse(response);
}

export async function POST(req: Request) {
const body = await req.text();

const response = await forwardToBackend({
method: 'POST',
path: '/api/sessions',
body,
});

return passthroughJsonResponse(response);
}

67 changes: 12 additions & 55 deletions frontend/app/home/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,25 @@ import ReactFlowView from '@/components/ui-react-flow/react-flow-view';
import { GlobalProvider } from '@/context/GlobalContext';
import AppHeader from '@/components/ui-header/app-header';
import SettingsBar from '@/components/ui-header/settings-bar';

import { ErrorDialog, PermissionDialog } from '@/components/ui/custom-dialog';
import { useState } from 'react';
import { NotificationsProvider } from '@/components/notifications';

export default function Home() {

// REMOVE LATER: consts for testing pop-up notifications
// const [showErrorDialog, setShowErrorDialog] = useState(false);
// const [showPermissionDialog, setShowPermissionDialog] = useState(false);

return (
<GlobalProvider>
<div className="h-screen flex flex-col">
{/* Top section for header and settings bar */}
<div className="flex flex-col">
<AppHeader />
<SettingsBar />
</div>

{/* Bottom section for workspace and sidebar */}
<div className="flex-1 flex relative">
<ReactFlowView />
<NotificationsProvider>
<div className="h-screen flex flex-col">
{/* Top section for header and settings bar */}
<div className="flex flex-col">
<AppHeader />
<SettingsBar />
</div>

{/* REMOVE LATER: Test pop-up buttons for notifications (positioned top-right corner)
<div className="absolute top-4 right-20 flex gap-2">
<button
onClick={() => setShowErrorDialog(true)}
className="bg-[#EB0000] text-white px-3 py-1 rounded-md"
>
Show Error
</button>
<button
onClick={() => setShowPermissionDialog(true)}
className="bg-[#2C7778] text-white px-3 py-1 rounded-md"
>
Request Permission
</button>
{/* Bottom section for workspace and sidebar */}
<div className="flex-1 flex relative">
<ReactFlowView />
</div>
*/}
</div>

{/* REMOVE LATER: Test Dialogs for pop-up notifications
<ErrorDialog
open={showErrorDialog}
onOpenChange={setShowErrorDialog}
title="Save Failed: Memory Limit Exceeded"
description="This action has been terminated because it exceeds the configured maximum memory limit."
/>

<PermissionDialog
open={showPermissionDialog}
onOpenChange={setShowPermissionDialog}
title="[Thing] Permissions"
description="We need access to ................."
onAllow={() => {
console.log('Permission granted');
setShowPermissionDialog(false);
}}
/>
*/}

</div>
</NotificationsProvider>
</GlobalProvider>
);
}
200 changes: 196 additions & 4 deletions frontend/components/ui-header/settings-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { useGlobalContext } from '@/context/GlobalContext';
import { Timer } from '@/components/ui/timer';
import { useEffect, useRef, useState } from 'react';
import SessionModal from '@/components/ui-sessions/session-modal';
import {
Dialog,
DialogContent,
Expand All @@ -15,12 +16,39 @@ import {
DialogTrigger,
DialogClose,
} from '@/components/ui/dialog';
import { useNotifications } from '@/components/notifications';
import {
createSession,
getSessions,
loadFrontendState,
saveFrontendState,
SessionSummary,
} from '@/lib/session-api';
import {
FrontendWorkspaceState,
isFrontendWorkspaceState,
} from '@/lib/frontend-state';

export default function SettingsBar() {
const { dataStreaming, setDataStreaming } = useGlobalContext();
const {
dataStreaming,
setDataStreaming,
activeSessionId,
setActiveSessionId,
} = useGlobalContext();
const notifications = useNotifications();
const [leftTimerSeconds, setLeftTimerSeconds] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const [isSessionModalOpen, setIsSessionModalOpen] = useState(false);
const [sessionModalMode, setSessionModalMode] = useState<'save' | 'load'>(
'save'
);
const [sessions, setSessions] = useState<SessionSummary[]>([]);
const [isFetchingSessions, setIsFetchingSessions] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(false);

const [sessionId, setSessionId] = useState<number | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -81,7 +109,7 @@ export default function SettingsBar() {
if (leftTimerSeconds >= 300 && dataStreaming) {
setDataStreaming(false);
}
}, [leftTimerSeconds, dataStreaming]);
}, [leftTimerSeconds, dataStreaming, setDataStreaming]);


const handleStartStop = () => {
Expand All @@ -103,6 +131,139 @@ export default function SettingsBar() {
setIsResetDialogOpen(false);
};

const requestFrontendState = async (): Promise<FrontendWorkspaceState> =>
new Promise((resolve, reject) => {
const timeout = window.setTimeout(() => {
reject(new Error('Timed out while collecting frontend state.'));
}, 4000);

const responseListener = (event: Event) => {
window.clearTimeout(timeout);
const customEvent = event as CustomEvent<unknown>;
if (!isFrontendWorkspaceState(customEvent.detail)) {
reject(new Error('Frontend state payload is invalid.'));
return;
}
resolve(customEvent.detail);
};

window.addEventListener('frontend-state-response', responseListener, {
once: true,
});
window.dispatchEvent(new Event('request-frontend-state'));
});

const getErrorMessage = (error: unknown) =>
error instanceof Error ? error.message : 'Unexpected error';

const handleSaveToExistingSession = async (sessionId: number) => {
const state = await requestFrontendState();
await saveFrontendState(sessionId, state);
};

const handleSaveClick = async () => {
if (isSaving || isLoading || isFetchingSessions) {
return;
}

if (activeSessionId !== null) {
setIsSaving(true);
try {
await handleSaveToExistingSession(activeSessionId);
notifications.success({ title: 'Session saved successfully' });
} catch (error) {
notifications.error({
title: 'Save failed',
description: getErrorMessage(error),
});
} finally {
setIsSaving(false);
}
return;
}

setIsFetchingSessions(true);
try {
const fetchedSessions = await getSessions();
setSessions(fetchedSessions);
setSessionModalMode('save');
setIsSessionModalOpen(true);
} catch (error) {
notifications.error({
title: 'Save failed',
description: getErrorMessage(error),
});
} finally {
setIsFetchingSessions(false);
}
};

const handleLoadClick = async () => {
if (isSaving || isLoading || isFetchingSessions) {
return;
}

setIsFetchingSessions(true);
try {
const fetchedSessions = await getSessions();
setSessions(fetchedSessions);
setSessionModalMode('load');
setIsSessionModalOpen(true);
} catch (error) {
notifications.error({
title: 'Load failed',
description: getErrorMessage(error),
});
} finally {
setIsFetchingSessions(false);
}
};

const handleCreateAndSaveSession = async (sessionName: string) => {
setIsSaving(true);
try {
const state = await requestFrontendState();
const createdSession = await createSession(sessionName);
await saveFrontendState(createdSession.id, state);
setActiveSessionId(createdSession.id);
setIsSessionModalOpen(false);
notifications.success({ title: 'Session saved successfully' });
} catch (error) {
notifications.error({
title: 'Save failed',
description: getErrorMessage(error),
});
} finally {
setIsSaving(false);
}
};

const handleLoadSession = async (sessionId: number) => {
setIsLoading(true);
try {
const loadedPayload = await loadFrontendState(sessionId);
if (!isFrontendWorkspaceState(loadedPayload)) {
throw new Error('Loaded session payload has an invalid format.');
}

window.dispatchEvent(
new CustomEvent('restore-frontend-state', {
detail: loadedPayload,
})
);
setActiveSessionId(sessionId);
setIsSessionModalOpen(false);
notifications.success({ title: 'Session loaded successfully' });
} catch (error) {
notifications.error({
title: 'Load failed',
description: getErrorMessage(error),
});
} finally {
setIsLoading(false);
}
};

// Format seconds to MM:SS
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
Expand Down Expand Up @@ -136,7 +297,7 @@ export default function SettingsBar() {
</div>


{/* start/stop, load, save */}
{/* start/stop, reset, save, load */}
<div className="flex space-x-2">
<Button
onClick={handleStartStop}
Expand All @@ -163,8 +324,39 @@ export default function SettingsBar() {
</div>
</DialogContent>
</Dialog>
<Button variant="outline">Save</Button>
<Button
variant="outline"
onClick={handleLoadClick}
disabled={isSaving || isLoading || isFetchingSessions}
>
{isLoading
? 'Loading...'
: isFetchingSessions && sessionModalMode === 'load'
? 'Preparing...'
: 'Load'}
</Button>
<Button
variant="outline"
onClick={handleSaveClick}
disabled={isSaving || isLoading || isFetchingSessions}
>
{isSaving
? 'Saving...'
: isFetchingSessions && sessionModalMode === 'save'
? 'Preparing...'
: 'Save'}
</Button>
</div>

<SessionModal
open={isSessionModalOpen}
mode={sessionModalMode}
sessions={sessions}
isSubmitting={isSaving || isLoading}
onOpenChange={setIsSessionModalOpen}
onSave={handleCreateAndSaveSession}
onLoad={handleLoadSession}
/>
</div>
);
}
Loading
Loading