Skip to content

Commit 2283eb1

Browse files
authored
Merge pull request #73 from UBCMint/frontend/new-session
New session
2 parents 16478c0 + 33f140c commit 2283eb1

File tree

4 files changed

+107
-95
lines changed

4 files changed

+107
-95
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ export default function AppHeader() {
2222
<Image
2323
src="/mint-logo.png"
2424
alt="Logo"
25-
width={56}
26-
height={50}
27-
className="h-14 w-auto object-contain pt-2 ml-2"
25+
width={52}
26+
height={46}
27+
className="h-14 w-auto object-contain ml-2"
2828
/>
2929
</div>
3030

3131
{/* update, issues */}
32-
<div className="flex h-full items-center pt-2 space-x-4">
32+
<div className="flex h-full items-center space-x-4">
3333
<Button
3434
variant="link"
3535
className="flex items-center space-x-1 px-3 py-2"

frontend/components/ui-header/settings-bar.tsx

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

3-
import { Menubar, MenubarMenu, MenubarTrigger } from '@/components/ui/menubar';
3+
import { Menubar } from '@/components/ui/menubar';
4+
import { Plus } from 'lucide-react';
45
import { ProgressBar } from '@/components/ui/progressbar';
56
import { Button } from '@/components/ui/button';
67
import { useGlobalContext } from '@/context/GlobalContext';
@@ -40,6 +41,7 @@ export default function SettingsBar() {
4041
const [leftTimerSeconds, setLeftTimerSeconds] = useState(0);
4142
const intervalRef = useRef<NodeJS.Timeout | null>(null);
4243
const [isResetDialogOpen, setIsResetDialogOpen] = useState(false);
44+
const [isNewDialogOpen, setIsNewDialogOpen] = useState(false);
4345
const [isSessionModalOpen, setIsSessionModalOpen] = useState(false);
4446
const [sessionModalMode, setSessionModalMode] = useState<'save' | 'load'>(
4547
'save'
@@ -49,37 +51,19 @@ export default function SettingsBar() {
4951
const [fetchingFor, setFetchingFor] = useState<'save' | 'load' | null>(null);
5052
const [isSaving, setIsSaving] = useState(false);
5153
const [isLoading, setIsLoading] = useState(false);
54+
const [isDirty, setIsDirty] = useState(false);
5255

53-
const [sessionId, setSessionId] = useState<number | null>(null);
54-
56+
// Track unsaved canvas changes
5557
useEffect(() => {
56-
async function fetchOrCreateSession() {
57-
try {
58-
const res = await fetch('/api/sessions');
59-
const sessions = await res.json();
60-
61-
if (sessions.length > 0) {
62-
setSessionId(sessions[0].id);
63-
} else {
64-
const created = await fetch('/api/sessions', {
65-
method: 'POST',
66-
headers: { 'Content-Type': 'application/json' },
67-
body: JSON.stringify('New Session'),
68-
});
69-
const session = await created.json();
70-
setSessionId(session.id);
71-
}
72-
} catch (err) {
73-
console.error('Failed to fetch or create session', err);
74-
}
75-
}
76-
77-
fetchOrCreateSession();
58+
const handler = () => setIsDirty(true);
59+
window.addEventListener('canvas-changed', handler);
60+
window.addEventListener('reactflow-edges-changed', handler);
61+
return () => {
62+
window.removeEventListener('canvas-changed', handler);
63+
window.removeEventListener('reactflow-edges-changed', handler);
64+
};
7865
}, []);
79-
80-
// useEffect(() => {
81-
// console.log('dataStreaming:', dataStreaming);
82-
// });
66+
8367
// Timer effect - starts/stops based on dataStreaming state
8468
useEffect(() => {
8569
if (dataStreaming) {
@@ -171,6 +155,7 @@ export default function SettingsBar() {
171155
setIsSaving(true);
172156
try {
173157
await handleSaveToExistingSession(activeSessionId);
158+
setIsDirty(false);
174159
notifications.success({ title: 'Session saved successfully' });
175160
} catch (error) {
176161
notifications.error({
@@ -224,13 +209,35 @@ export default function SettingsBar() {
224209
}
225210
};
226211

212+
const handleNewClick = () => {
213+
if (isSaving || isLoading || isFetchingSessions) {
214+
return;
215+
}
216+
if (isDirty) {
217+
setIsNewDialogOpen(true);
218+
} else {
219+
handleConfirmNew();
220+
}
221+
};
222+
223+
const handleConfirmNew = () => {
224+
setActiveSessionId(null);
225+
setIsDirty(false);
226+
setDataStreaming(false);
227+
setLeftTimerSeconds(0);
228+
window.dispatchEvent(new Event('pipeline-reset'));
229+
setIsNewDialogOpen(false);
230+
notifications.success({ title: 'New session started' });
231+
};
232+
227233
const handleCreateAndSaveSession = async (sessionName: string) => {
228234
setIsSaving(true);
229235
try {
230236
const state = await requestFrontendState();
231237
const createdSession = await createSession(sessionName);
232238
await saveFrontendState(createdSession.id, state);
233239
setActiveSessionId(createdSession.id);
240+
setIsDirty(false);
234241
setIsSessionModalOpen(false);
235242
notifications.success({ title: 'Session saved successfully' });
236243
} catch (error) {
@@ -278,16 +285,27 @@ export default function SettingsBar() {
278285

279286
return (
280287
<div className="flex justify-between items-center p-4 bg-white border-b">
281-
{/* Session ID, Tutorial */}
288+
{/* Session ID, Tutorials */}
282289
<Menubar>
283290
<span className="px-3 py-1 text-sm">
284-
Session {sessionId ?? 'ID'}
291+
Session {activeSessionId ?? 'ID'}
285292
</span>
286-
<MenubarMenu>
287-
<MenubarTrigger className="hover:cursor-pointer hover:underline">Tutorials</MenubarTrigger>
288-
</MenubarMenu>
293+
<button className="px-3 py-1 text-sm rounded-sm hover:bg-accent hover:text-accent-foreground hover:underline">
294+
Tutorials
295+
</button>
289296
</Menubar>
290297

298+
{/* New session button */}
299+
<Button
300+
variant="outline"
301+
onClick={handleNewClick}
302+
disabled={isSaving || isLoading || isFetchingSessions}
303+
className="ml-2 flex items-center gap-1"
304+
>
305+
<Plus size={14} />
306+
New
307+
</Button>
308+
291309
{/* slider */}
292310
<div className="flex-1 mx-4">
293311
<ProgressBar value={(leftTimerSeconds / 300) * 100} />
@@ -353,6 +371,23 @@ export default function SettingsBar() {
353371
</Button>
354372
</div>
355373

374+
<Dialog open={isNewDialogOpen} onOpenChange={setIsNewDialogOpen}>
375+
<DialogContent>
376+
<DialogHeader>
377+
<DialogTitle>Start a new session?</DialogTitle>
378+
<DialogDescription>
379+
Your current session is unsaved. Hitting confirm will clear the current pipeline. Any unsaved changes will be lost.
380+
</DialogDescription>
381+
</DialogHeader>
382+
<div className="flex justify-end gap-2 mt-4">
383+
<DialogClose asChild>
384+
<Button variant="outline">Cancel</Button>
385+
</DialogClose>
386+
<Button className="bg-red-500" onClick={handleConfirmNew}>Confirm</Button>
387+
</div>
388+
</DialogContent>
389+
</Dialog>
390+
356391
<SessionModal
357392
open={isSessionModalOpen}
358393
mode={sessionModalMode}

frontend/components/ui-react-flow/react-flow-view.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ const ReactFlowInterface = () => {
158158
);
159159

160160
const onNodesChange: OnNodesChange = useCallback(
161-
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
161+
(changes) => {
162+
setNodes((nds) => applyNodeChanges(changes, nds));
163+
window.dispatchEvent(new Event('canvas-changed'));
164+
},
162165
[setNodes]
163166
);
164167

frontend/components/ui-sessions/session-modal.tsx

Lines changed: 30 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22

33
import { useEffect, useMemo, useState } from 'react';
4-
import { Check, ChevronsUpDown } from 'lucide-react';
4+
import { Check } from 'lucide-react';
55

66
import { Button } from '@/components/ui/button';
77
import {
@@ -20,7 +20,6 @@ import {
2020
CommandItem,
2121
CommandList,
2222
} from '@/components/ui/command';
23-
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
2423
import { cn } from '@/lib/utils';
2524
import { SessionSummary } from '@/lib/session-api';
2625

@@ -49,15 +48,13 @@ export default function SessionModal({
4948
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(
5049
null
5150
);
52-
const [isSessionPickerOpen, setIsSessionPickerOpen] = useState(false);
5351
const [validationError, setValidationError] = useState<string | null>(null);
5452

5553
useEffect(() => {
5654
if (!open) {
5755
setSessionName('');
5856
setSelectedSessionId(null);
5957
setValidationError(null);
60-
setIsSessionPickerOpen(false);
6158
}
6259
}, [open, mode]);
6360

@@ -129,58 +126,35 @@ export default function SessionModal({
129126
</div>
130127
) : (
131128
<div className="space-y-2">
132-
<Popover
133-
open={isSessionPickerOpen}
134-
onOpenChange={setIsSessionPickerOpen}
135-
>
136-
<PopoverTrigger asChild>
137-
<Button
138-
variant="outline"
139-
role="combobox"
140-
aria-expanded={isSessionPickerOpen}
141-
className="w-full justify-between"
142-
disabled={isSubmitting || sessions.length === 0}
143-
>
144-
{selectedSession
145-
? `${selectedSession.name} (ID ${selectedSession.id})`
146-
: 'Select session'}
147-
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
148-
</Button>
149-
</PopoverTrigger>
150-
<PopoverContent className="w-[420px] p-0">
151-
<Command>
152-
<CommandInput placeholder="Search by name or session ID..." />
153-
<CommandList>
154-
<CommandEmpty>No sessions found.</CommandEmpty>
155-
<CommandGroup>
156-
{sessions.map((session) => (
157-
<CommandItem
158-
key={session.id}
159-
value={`${session.name} ${session.id}`}
160-
onSelect={() => {
161-
setSelectedSessionId(session.id);
162-
setIsSessionPickerOpen(false);
163-
}}
164-
>
165-
<Check
166-
className={cn(
167-
'mr-2 h-4 w-4',
168-
selectedSessionId === session.id
169-
? 'opacity-100'
170-
: 'opacity-0'
171-
)}
172-
/>
173-
<span>{session.name}</span>
174-
<span className="ml-auto text-xs text-muted-foreground">
175-
ID {session.id}
176-
</span>
177-
</CommandItem>
178-
))}
179-
</CommandGroup>
180-
</CommandList>
181-
</Command>
182-
</PopoverContent>
183-
</Popover>
129+
<Command className="border rounded-md">
130+
<CommandInput placeholder="Search by name or session ID..." />
131+
<CommandList>
132+
<CommandEmpty>No sessions found.</CommandEmpty>
133+
<CommandGroup>
134+
{sessions.map((session) => (
135+
<CommandItem
136+
key={session.id}
137+
value={`${session.name} ${session.id}`}
138+
onSelect={() => setSelectedSessionId(session.id)}
139+
disabled={isSubmitting}
140+
>
141+
<Check
142+
className={cn(
143+
'mr-2 h-4 w-4',
144+
selectedSessionId === session.id
145+
? 'opacity-100'
146+
: 'opacity-0'
147+
)}
148+
/>
149+
<span>{session.name}</span>
150+
<span className="ml-auto text-xs text-muted-foreground">
151+
ID {session.id}
152+
</span>
153+
</CommandItem>
154+
))}
155+
</CommandGroup>
156+
</CommandList>
157+
</Command>
184158
{validationError ? (
185159
<p className="text-sm text-red-600">{validationError}</p>
186160
) : null}

0 commit comments

Comments
 (0)