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
8 changes: 4 additions & 4 deletions frontend/components/ui-header/app-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export default function AppHeader() {
<Image
src="/mint-logo.png"
alt="Logo"
width={56}
height={50}
className="h-14 w-auto object-contain pt-2 ml-2"
width={52}
height={46}
className="h-14 w-auto object-contain ml-2"
/>
</div>

{/* update, issues */}
<div className="flex h-full items-center pt-2 space-x-4">
<div className="flex h-full items-center space-x-4">
<Button
variant="link"
className="flex items-center space-x-1 px-3 py-2"
Expand Down
103 changes: 69 additions & 34 deletions frontend/components/ui-header/settings-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { Menubar, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar';
import { Menubar } from '@/components/ui/menubar';
import { Plus } from 'lucide-react';
import { ProgressBar } from '@/components/ui/progressbar';
import { Button } from '@/components/ui/button';
import { useGlobalContext } from '@/context/GlobalContext';
Expand Down Expand Up @@ -40,6 +41,7 @@ export default function SettingsBar() {
const [leftTimerSeconds, setLeftTimerSeconds] = useState(0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
const [isNewDialogOpen, setIsNewDialogOpen] = useState(false);
const [isSessionModalOpen, setIsSessionModalOpen] = useState(false);
const [sessionModalMode, setSessionModalMode] = useState<'save' | 'load'>(
'save'
Expand All @@ -49,37 +51,19 @@ export default function SettingsBar() {
const [fetchingFor, setFetchingFor] = useState<'save' | 'load' | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isDirty, setIsDirty] = useState(false);

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

// Track unsaved canvas changes
useEffect(() => {
async function fetchOrCreateSession() {
try {
const res = await fetch('/api/sessions');
const sessions = await res.json();

if (sessions.length > 0) {
setSessionId(sessions[0].id);
} else {
const created = await fetch('/api/sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify('New Session'),
});
const session = await created.json();
setSessionId(session.id);
}
} catch (err) {
console.error('Failed to fetch or create session', err);
}
}

fetchOrCreateSession();
const handler = () => setIsDirty(true);
window.addEventListener('canvas-changed', handler);
window.addEventListener('reactflow-edges-changed', handler);
return () => {
window.removeEventListener('canvas-changed', handler);
window.removeEventListener('reactflow-edges-changed', handler);
};
}, []);

// useEffect(() => {
// console.log('dataStreaming:', dataStreaming);
// });

// Timer effect - starts/stops based on dataStreaming state
useEffect(() => {
if (dataStreaming) {
Expand Down Expand Up @@ -171,6 +155,7 @@ export default function SettingsBar() {
setIsSaving(true);
try {
await handleSaveToExistingSession(activeSessionId);
setIsDirty(false);
notifications.success({ title: 'Session saved successfully' });
} catch (error) {
notifications.error({
Expand Down Expand Up @@ -224,13 +209,35 @@ export default function SettingsBar() {
}
};

const handleNewClick = () => {
if (isSaving || isLoading || isFetchingSessions) {
return;
}
if (isDirty) {
setIsNewDialogOpen(true);
} else {
handleConfirmNew();
}
};

const handleConfirmNew = () => {
setActiveSessionId(null);
setIsDirty(false);
setDataStreaming(false);
setLeftTimerSeconds(0);
window.dispatchEvent(new Event('pipeline-reset'));
setIsNewDialogOpen(false);
notifications.success({ title: 'New session started' });
};

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);
setIsDirty(false);
setIsSessionModalOpen(false);
notifications.success({ title: 'Session saved successfully' });
} catch (error) {
Expand Down Expand Up @@ -278,16 +285,27 @@ export default function SettingsBar() {

return (
<div className="flex justify-between items-center p-4 bg-white border-b">
{/* Session ID, Tutorial */}
{/* Session ID, Tutorials */}
<Menubar>
<span className="px-3 py-1 text-sm">
Session {sessionId ?? 'ID'}
Session {activeSessionId ?? 'ID'}
</span>
<MenubarMenu>
<MenubarTrigger className="hover:cursor-pointer hover:underline">Tutorials</MenubarTrigger>
</MenubarMenu>
<button className="px-3 py-1 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground hover:underline">
Tutorials
</button>
</Menubar>

{/* New session button */}
<Button
variant="outline"
onClick={handleNewClick}
disabled={isSaving || isLoading || isFetchingSessions}
className="ml-2 flex items-center gap-1"
>
<Plus size={14} />
New
</Button>

{/* slider */}
<div className="flex-1 mx-4">
<ProgressBar value={(leftTimerSeconds / 300) * 100} />
Expand Down Expand Up @@ -353,6 +371,23 @@ export default function SettingsBar() {
</Button>
</div>

<Dialog open={isNewDialogOpen} onOpenChange={setIsNewDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Start a new session?</DialogTitle>
<DialogDescription>
Your current session is unsaved. Hitting confirm will clear the current pipeline. Any unsaved changes will be lost.
</DialogDescription>
</DialogHeader>
<div className="flex justify-end gap-2 mt-4">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button className="bg-red-500" onClick={handleConfirmNew}>Confirm</Button>
</div>
</DialogContent>
</Dialog>

<SessionModal
open={isSessionModalOpen}
mode={sessionModalMode}
Expand Down
5 changes: 4 additions & 1 deletion frontend/components/ui-react-flow/react-flow-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ const ReactFlowInterface = () => {
);

const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
(changes) => {
setNodes((nds) => applyNodeChanges(changes, nds));
window.dispatchEvent(new Event('canvas-changed'));
},
[setNodes]
);

Expand Down
86 changes: 30 additions & 56 deletions frontend/components/ui-sessions/session-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useEffect, useMemo, useState } from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { Check } from 'lucide-react';

import { Button } from '@/components/ui/button';
import {
Expand All @@ -20,7 +20,6 @@ import {
CommandItem,
CommandList,
} from '@/components/ui/command';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { SessionSummary } from '@/lib/session-api';

Expand Down Expand Up @@ -49,15 +48,13 @@ export default function SessionModal({
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(
null
);
const [isSessionPickerOpen, setIsSessionPickerOpen] = useState(false);
const [validationError, setValidationError] = useState<string | null>(null);

useEffect(() => {
if (!open) {
setSessionName('');
setSelectedSessionId(null);
setValidationError(null);
setIsSessionPickerOpen(false);
}
}, [open, mode]);

Expand Down Expand Up @@ -129,58 +126,35 @@ export default function SessionModal({
</div>
) : (
<div className="space-y-2">
<Popover
open={isSessionPickerOpen}
onOpenChange={setIsSessionPickerOpen}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={isSessionPickerOpen}
className="w-full justify-between"
disabled={isSubmitting || sessions.length === 0}
>
{selectedSession
? `${selectedSession.name} (ID ${selectedSession.id})`
: 'Select session'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[420px] p-0">
<Command>
<CommandInput placeholder="Search by name or session ID..." />
<CommandList>
<CommandEmpty>No sessions found.</CommandEmpty>
<CommandGroup>
{sessions.map((session) => (
<CommandItem
key={session.id}
value={`${session.name} ${session.id}`}
onSelect={() => {
setSelectedSessionId(session.id);
setIsSessionPickerOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedSessionId === session.id
? 'opacity-100'
: 'opacity-0'
)}
/>
<span>{session.name}</span>
<span className="ml-auto text-xs text-muted-foreground">
ID {session.id}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Command className="border rounded-md">
<CommandInput placeholder="Search by name or session ID..." />
<CommandList>
<CommandEmpty>No sessions found.</CommandEmpty>
<CommandGroup>
{sessions.map((session) => (
<CommandItem
key={session.id}
value={`${session.name} ${session.id}`}
onSelect={() => setSelectedSessionId(session.id)}
disabled={isSubmitting}
>
<Check
className={cn(
'mr-2 h-4 w-4',
selectedSessionId === session.id
? 'opacity-100'
: 'opacity-0'
)}
/>
<span>{session.name}</span>
<span className="ml-auto text-xs text-muted-foreground">
ID {session.id}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
{validationError ? (
<p className="text-sm text-red-600">{validationError}</p>
) : null}
Expand Down
Loading