diff --git a/src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte b/src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte index 8bd34a7ba..32cee7b96 100644 --- a/src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte +++ b/src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte @@ -301,7 +301,6 @@ https://github.com/carbon-design-system/carbon-components-svelte/issues/892 display: flex; flex-direction: column; justify-content: space-between; - padding: 0 0 layout.$spacing-05 0; :global(.bx--side-nav__divider) { margin: layout.$spacing-03 0 0 0; diff --git a/src/leapfrogai_ui/src/lib/components/ChatSidebar.test.ts b/src/leapfrogai_ui/src/lib/components/ChatSidebar.test.ts index 9fd799c54..715dd11b7 100644 --- a/src/leapfrogai_ui/src/lib/components/ChatSidebar.test.ts +++ b/src/leapfrogai_ui/src/lib/components/ChatSidebar.test.ts @@ -66,7 +66,8 @@ describe('ChatSidebar', () => { it('renders threads', async () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -89,7 +90,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: [fakeTodayThread, fakeYesterdayThread], // uses date override starting in March - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: '' }); render(ChatSidebar); @@ -112,7 +114,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -148,7 +151,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -180,7 +184,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -199,7 +204,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -218,7 +224,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -248,7 +255,8 @@ describe('ChatSidebar', () => { const newLabelText = 'new label'; threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -267,7 +275,8 @@ describe('ChatSidebar', () => { const newLabelText = 'new label'; threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -288,7 +297,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -311,7 +321,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: fakeThreads, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -327,7 +338,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: [fakeThread], - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: fakeThreads[0].id }); render(ChatSidebar); @@ -346,7 +358,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: [fakeThread1, fakeThread2, fakeThread3], - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: '' }); render(ChatSidebar); @@ -370,7 +383,8 @@ describe('ChatSidebar', () => { threadsStore.set({ threads: [fakeThread1, fakeThread2, fakeThread3], - selectedAssistantId: NO_SELECTED_ASSISTANT_ID + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + lastVisitedThreadId: '' }); render(ChatSidebar); diff --git a/src/leapfrogai_ui/src/lib/components/ImportExport.svelte b/src/leapfrogai_ui/src/lib/components/ImportExport.svelte index 8cc7abb55..0512865af 100644 --- a/src/leapfrogai_ui/src/lib/components/ImportExport.svelte +++ b/src/leapfrogai_ui/src/lib/components/ImportExport.svelte @@ -80,8 +80,9 @@ id="export-btn" kind="ghost" icon={Export} + size="small" iconDescription="Export conversations" - on:click={onExport}>Export dataExport chat history @@ -90,6 +91,7 @@ display: flex; flex-direction: column; justify-content: space-between; + padding: layout.$spacing-03 0 layout.$spacing-03 0; :global(.bx--btn) { width: 100%; color: themes.$text-secondary; diff --git a/src/leapfrogai_ui/src/lib/components/ImportExport.test.ts b/src/leapfrogai_ui/src/lib/components/ImportExport.test.ts index eceb524db..efde4bc1a 100644 --- a/src/leapfrogai_ui/src/lib/components/ImportExport.test.ts +++ b/src/leapfrogai_ui/src/lib/components/ImportExport.test.ts @@ -12,12 +12,12 @@ const uploadJSONFile = async (obj: object) => { const blob = new Blob([dataStr]); const file = new File([blob], 'badData.json', { type: 'application/JSON' }); File.prototype.text = vi.fn().mockResolvedValueOnce(dataStr); - const uploadBtn = screen.getByTestId('import data input'); + const uploadBtn = screen.getByTestId('import-chat-history-input'); await userEvent.upload(uploadBtn, file); }; -describe('Import and Export data', () => { +describe('Import and Export chat history', () => { // Note - actual exporting and importing of data tested with E2E test afterEach(() => { @@ -67,7 +67,7 @@ describe('Import and Export data', () => { render(ImportExport); - await userEvent.click(screen.getByText('Export data')); + await userEvent.click(screen.getByText('Export chat history')); expect(toastSpy).toHaveBeenCalledTimes(1); expect(toastSpy).toHaveBeenCalledWith({ kind: 'error', @@ -78,7 +78,7 @@ describe('Import and Export data', () => { it('only allows uploading of JSON files', async () => { render(ImportExport); - const uploadBtn = screen.getByTestId('import data input'); + const uploadBtn = screen.getByTestId('import-chat-history-input'); expect(uploadBtn).toHaveAttribute('accept', 'application/json'); }); diff --git a/src/leapfrogai_ui/src/lib/components/LFFileUploader.svelte b/src/leapfrogai_ui/src/lib/components/LFFileUploader.svelte index eb881d29f..70e952329 100644 --- a/src/leapfrogai_ui/src/lib/components/LFFileUploader.svelte +++ b/src/leapfrogai_ui/src/lib/components/LFFileUploader.svelte @@ -20,7 +20,7 @@
ref?.click()}>Import data ref?.click()}>Import chat history
diff --git a/src/leapfrogai_ui/src/lib/components/LFHeader.svelte b/src/leapfrogai_ui/src/lib/components/LFHeader.svelte index c3153eb7a..220d16426 100644 --- a/src/leapfrogai_ui/src/lib/components/LFHeader.svelte +++ b/src/leapfrogai_ui/src/lib/components/LFHeader.svelte @@ -3,6 +3,7 @@ import logo from '$assets/LeapfrogAI.png'; import { Settings, UserAvatar } from 'carbon-icons-svelte'; import { Header, HeaderAction, HeaderUtilities } from 'carbon-components-svelte'; + import { threadsStore } from '$stores'; let loading = false; let signOutForm: HTMLFormElement; @@ -36,7 +37,14 @@ persistentHamburgerMenu={innerWidth ? innerWidth < 1056 : false} bind:isSideNavOpen={$uiStore.isSideNavOpen} > - + { it('closes the other header actions when one is opened', async () => { @@ -20,4 +22,14 @@ describe('LFHeader', () => { expect(settingsActionBtn).toHaveClass('bx--header__action--active'); expect(userActionBtn).not.toHaveClass('bx--header__action--active'); }); + it('has a link on the logo that navigates to the last visited thread id', () => { + const thread = getFakeThread(); + + threadsStore.set({ + threads: [thread], + lastVisitedThreadId: thread.id + }); + render(LFHeader); + expect(screen.getByTestId('header-logo-link')).toHaveAttribute('href', `/chat/${thread.id}`); + }); }); diff --git a/src/leapfrogai_ui/src/lib/stores/threads.ts b/src/leapfrogai_ui/src/lib/stores/threads.ts index 9e6f05ba3..8656c06fd 100644 --- a/src/leapfrogai_ui/src/lib/stores/threads.ts +++ b/src/leapfrogai_ui/src/lib/stores/threads.ts @@ -12,12 +12,14 @@ type ThreadsStore = { threads: LFThread[]; selectedAssistantId: string; sendingBlocked: boolean; + lastVisitedThreadId: string; }; const defaultValues: ThreadsStore = { threads: [], selectedAssistantId: NO_SELECTED_ASSISTANT_ID, - sendingBlocked: false + sendingBlocked: false, + lastVisitedThreadId: '' }; const createThread = async (input: NewThreadInput) => { @@ -85,6 +87,9 @@ const createThreadsStore = () => { setThreads: (threads: LFThread[]) => { update((old) => ({ ...old, threads })); }, + setLastVisitedThreadId: (id: string) => { + update((old) => ({ ...old, lastVisitedThreadId: id })); + }, setSelectedAssistantId: (selectedAssistantId: string) => { update((old) => { return { ...old, selectedAssistantId }; @@ -164,6 +169,7 @@ const createThreadsStore = () => { ...old, threads: old.threads.filter((c) => c.id !== id) })); + await goto(`/chat`); } catch { toastStore.addToast({ kind: 'error', diff --git a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.server.ts b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.server.ts index 0158fc17f..dac755b10 100644 --- a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.server.ts +++ b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.server.ts @@ -1,71 +1,17 @@ import { redirect } from '@sveltejs/kit'; -import type { Profile } from '$lib/types/profile'; -import type { LFThread } from '$lib/types/threads'; -import { openai } from '$lib/server/constants'; -import type { LFMessage } from '$lib/types/messages'; -const getThreadWithMessages = async (thread_id: string): Promise => { - try { - const thread = (await openai.beta.threads.retrieve(thread_id)) as LFThread; - if (!thread) { - return null; - } - const messagesPage = await openai.beta.threads.messages.list(thread.id); - const messages = messagesPage.data as LFMessage[]; - messages.sort((a, b) => a.created_at - b.created_at); - return { ...thread, messages: messages }; - } catch (e) { - console.error(`Error fetching thread or messages: ${e}`); - return null; - } -}; - -export const load = async ({ fetch, locals: { supabase, safeGetSession } }) => { +export const load = async ({ fetch, locals: { safeGetSession } }) => { const { session } = await safeGetSession(); if (!session) { throw redirect(303, '/'); } - const { data: profile, error: profileError } = await supabase - .from('profiles') - .select(`*`) - .eq('id', session.user?.id) - .returns() - .single(); - - if (profileError) { - console.error( - `error getting user profile for user_id: ${session.user?.id}. ${JSON.stringify(profileError)}` - ); - throw redirect(303, '/'); - } - - const threads: LFThread[] = []; - if (profile?.thread_ids && profile?.thread_ids.length > 0) { - try { - const threadPromises = profile.thread_ids.map((thread_id) => - getThreadWithMessages(thread_id) - ); - const results = await Promise.allSettled(threadPromises); - - results.forEach((result) => { - if (result.status === 'fulfilled' && result.value) { - threads.push(result.value); - } - }); - } catch (e) { - console.error(`Error fetching threads: ${e}`); - // fail silently - return null; - } - } - const promises = [fetch('/api/assistants'), fetch('/api/files')]; const [assistantsRes, filesRes] = await Promise.all(promises); const assistants = await assistantsRes.json(); const files = await filesRes.json(); - return { threads, assistants, files }; + return { assistants, files }; }; diff --git a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.svelte b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.svelte index f2fca5518..4ee648194 100644 --- a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.svelte +++ b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/+page.svelte @@ -26,11 +26,10 @@ ERROR_GETTING_ASSISTANT_MSG_TEXT, ERROR_SAVING_MSG_TEXT } from '$constants/errorMessages'; - import type { PageServerLoad } from './$types'; + import { convertMessageToAiMessage } from '$helpers/threads.js'; - // TODO - this data is not receiving correct type inference, see issue: (https://github.com/defenseunicorns/leapfrogai/issues/587) - export let data: PageServerLoad; + export let data; /** LOCAL VARS **/ let messageThreadDiv: HTMLDivElement; @@ -42,8 +41,9 @@ /** REACTIVE STATE **/ $: activeThread = $threadsStore.threads.find((t) => t.id === $page.params.thread_id); + $: $page.params.thread_id, threadsStore.setLastVisitedThreadId($page.params.thread_id); - $: assistantsList = [...(data.assistants || [])].map((assistant) => ({ + $: assistantsList = [...(data?.assistants || [])].map((assistant) => ({ id: assistant.id, text: assistant.name || 'unknown' })); @@ -86,8 +86,8 @@ ...$chatMessages, ...assistantMessagesCopy ]); - } catch (error) { - console.log('error fetching message', error); + } catch { + // Fail Silently - error notification would not be useful to user, on failure, just show unparsed message } }; @@ -253,7 +253,6 @@ }; onMount(async () => { - threadsStore.setThreads(data.threads || []); await tick(); resetMessages({ activeThread, diff --git a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage.test.ts b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage.test.ts index 463e3668f..434223b6d 100644 --- a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage.test.ts +++ b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage.test.ts @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/svelte'; - import { fakeThreads, getFakeAssistant, getFakeFiles, getFakeProfile } from '$testUtils/fakeData'; import ChatPage from './+page.svelte'; import ChatPageWithToast from './ChatPageWithToast.test.svelte'; @@ -31,15 +30,17 @@ import type { LFThread } from '$lib/types/threads'; import type { LFAssistant } from '$lib/types/assistants'; import { delay } from '$helpers/chatHelpers'; import { mockGetFiles } from '$lib/mocks/file-mocks'; +import { threadsStore } from '$stores'; +import { NO_SELECTED_ASSISTANT_ID } from '$constants'; //Calls to vi.mock are hoisted to the top of the file, so you don't have access to variables declared in the global file scope unless they are defined with vi.hoisted before the call. const { getStores } = await vi.hoisted(() => import('$lib/mocks/svelte')); -type PageServerLoad = { +type LayoutServerLoad = { threads: LFThread[]; assistants: LFAssistant[]; } | null; -let data: PageServerLoad; +let data: LayoutServerLoad; const question = 'What is AI?'; const assistant1 = getFakeAssistant(); @@ -104,6 +105,13 @@ describe('when there is an active thread selected', () => { safeGetSession: sessionMock } }); + + threadsStore.set({ + threads: fakeThreads, + lastVisitedThreadId: fakeThreads[0].id, + selectedAssistantId: NO_SELECTED_ASSISTANT_ID, + sendingBlocked: false + }); }); afterAll(() => { diff --git a/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/+layout.svelte b/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/+layout.svelte index e572e3635..6c633b221 100644 --- a/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/+layout.svelte +++ b/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/+layout.svelte @@ -2,6 +2,7 @@ import { Breadcrumb, BreadcrumbItem, Content } from 'carbon-components-svelte'; import { page } from '$app/stores'; import { PoweredByDU } from '$components'; + import { threadsStore } from '$stores'; const paths = [ { @@ -27,6 +28,14 @@ // Handle edit route with assistant id as path parameter ($page.url.pathname.startsWith('/chat/assistants-management/edit/') && path === '/chat/assistants-management/edit'); + + const getPath = (path: string) => { + if (path === '/chat') + return $threadsStore.lastVisitedThreadId + ? `/chat/${$threadsStore.lastVisitedThreadId}` + : '/chat'; + return path; + }; @@ -36,7 +45,7 @@ {#each paths as { path, name } (path)} {#if $page.url.pathname.includes(path)} {name} {/if} diff --git a/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/assistants-management-page.test.ts b/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/assistants-management-page.test.ts index 2c99ef84e..0dff6ef23 100644 --- a/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/assistants-management-page.test.ts +++ b/src/leapfrogai_ui/src/routes/chat/(settings)/assistants-management/assistants-management-page.test.ts @@ -3,6 +3,8 @@ import AssistantsManagementPage from './+page.svelte'; import { getFakeAssistant } from '$testUtils/fakeData'; describe('Assistants management page', () => { + // Assistant search tested with e2e + it('displays all the assistants', async () => { const assistant1 = getFakeAssistant(); const assistant2 = getFakeAssistant(); @@ -12,5 +14,4 @@ describe('Assistants management page', () => { screen.getByTestId(`assistant-tile-${assistant1.name!}`); screen.getByTestId(`assistant-tile-${assistant2.name!}`); }); - // Assistant search tested with e2e }); diff --git a/src/leapfrogai_ui/src/routes/chat/(settings)/file-management/+layout.svelte b/src/leapfrogai_ui/src/routes/chat/(settings)/file-management/+layout.svelte index 04bf14bee..13d1a4069 100644 --- a/src/leapfrogai_ui/src/routes/chat/(settings)/file-management/+layout.svelte +++ b/src/leapfrogai_ui/src/routes/chat/(settings)/file-management/+layout.svelte @@ -2,6 +2,7 @@ import { Breadcrumb, BreadcrumbItem, Content } from 'carbon-components-svelte'; import { page } from '$app/stores'; import { PoweredByDU } from '$components'; + import { threadsStore } from '$stores'; const paths = [ { @@ -14,6 +15,14 @@ } ]; $: isCurrentPage = (path: string) => $page.url.pathname === path; + + const getPath = (path: string) => { + if (path === '/chat') + return $threadsStore.lastVisitedThreadId + ? `/chat/${$threadsStore.lastVisitedThreadId}` + : '/chat'; + return path; + }; @@ -23,7 +32,7 @@ {#each paths as { path, name } (path)} {#if $page.url.pathname.includes(path)} {name} {/if} diff --git a/src/leapfrogai_ui/src/routes/chat/+layout.server.ts b/src/leapfrogai_ui/src/routes/chat/+layout.server.ts new file mode 100644 index 000000000..7a1f1d0b0 --- /dev/null +++ b/src/leapfrogai_ui/src/routes/chat/+layout.server.ts @@ -0,0 +1,65 @@ +import { redirect } from '@sveltejs/kit'; +import type { Profile } from '$lib/types/profile'; +import type { LFThread } from '$lib/types/threads'; +import { openai } from '$lib/server/constants'; +import type { LFMessage } from '$lib/types/messages'; + +const getThreadWithMessages = async (thread_id: string): Promise => { + try { + const thread = (await openai.beta.threads.retrieve(thread_id)) as LFThread; + if (!thread) { + return null; + } + const messagesPage = await openai.beta.threads.messages.list(thread.id); + const messages = messagesPage.data as LFMessage[]; + messages.sort((a, b) => a.created_at - b.created_at); + return { ...thread, messages: messages }; + } catch (e) { + console.error(`Error fetching thread or messages: ${e}`); + return null; + } +}; + +export const load = async ({ locals: { supabase, safeGetSession } }) => { + const { session } = await safeGetSession(); + + if (!session) { + throw redirect(303, '/'); + } + + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select(`*`) + .eq('id', session.user?.id) + .returns() + .single(); + + if (profileError) { + console.error( + `error getting user profile for user_id: ${session.user?.id}. ${JSON.stringify(profileError)}` + ); + throw redirect(303, '/'); + } + + const threads: LFThread[] = []; + if (profile?.thread_ids && profile?.thread_ids.length > 0) { + try { + const threadPromises = profile.thread_ids.map((thread_id) => + getThreadWithMessages(thread_id) + ); + const results = await Promise.allSettled(threadPromises); + + results.forEach((result) => { + if (result.status === 'fulfilled' && result.value) { + threads.push(result.value); + } + }); + } catch (e) { + console.error(`Error fetching threads: ${e}`); + // fail silently + return null; + } + } + + return { threads }; +}; diff --git a/src/leapfrogai_ui/src/routes/chat/+layout.ts b/src/leapfrogai_ui/src/routes/chat/+layout.ts new file mode 100644 index 000000000..b72a9dc09 --- /dev/null +++ b/src/leapfrogai_ui/src/routes/chat/+layout.ts @@ -0,0 +1,12 @@ +import { browser } from '$app/environment'; +import { threadsStore } from '$stores'; + +// Load the store with the threads fetched by the +layout.server.ts (set store on the client side only) +// This only runs when the app is first loaded (because it's a higher level layout) +// After this load, the app keeps the store in sync with data changes and we don't +// re-fetch all that data from the server +export const load = async ({ data }) => { + if (browser) { + threadsStore.setThreads(data?.threads || []); + } +}; diff --git a/src/leapfrogai_ui/tests/assistants.test.ts b/src/leapfrogai_ui/tests/assistants.test.ts index 9ee81cd01..ef336c7ce 100644 --- a/src/leapfrogai_ui/tests/assistants.test.ts +++ b/src/leapfrogai_ui/tests/assistants.test.ts @@ -1,5 +1,12 @@ import { expect, test } from './fixtures'; -import { createAssistant, deleteActiveThread, deleteAllAssistants, loadChatPage } from './helpers'; +import { + createAssistant, + deleteActiveThread, + deleteAllAssistants, + getSimpleMathQuestion, + loadChatPage, + sendMessage +} from './helpers'; import { getFakeAssistantInput } from '../testUtils/fakeData'; import type { ActionResult } from '@sveltejs/kit'; @@ -133,14 +140,27 @@ test('it can search for assistants', async ({ page, browserName }) => { } }); -test('it can navigate with breadcrumbs', async ({ page }) => { - await page.goto('/chat/assistants-management'); +test('it can navigate to the last visited thread with breadcrumbs', async ({ page }) => { + const newMessage = getSimpleMathQuestion(); + await page.goto('/chat'); + await sendMessage(page, newMessage); + const messages = page.getByTestId('message'); + await expect(messages).toHaveCount(2); + + const urlParts = new URL(page.url()).pathname.split('/'); + const threadId = urlParts[urlParts.length - 1]; + + await page.getByLabel('Settings').click(); + await page.getByText('Assistants Management').click(); await page.getByRole('button', { name: 'New Assistant' }).click(); await page.waitForURL('/chat/assistants-management/new'); await page.getByRole('link', { name: 'Assistants Management' }).click(); await page.waitForURL('/chat/assistants-management'); await page.getByRole('link', { name: 'Chat' }).click(); - await page.waitForURL('/chat'); + await page.waitForURL(`/chat/${threadId}`); + + // Cleanup + await deleteActiveThread(page); }); test('it validates input', async ({ page }) => { diff --git a/src/leapfrogai_ui/tests/file-management.test.ts b/src/leapfrogai_ui/tests/file-management.test.ts index 18697b823..6ad1f08d4 100644 --- a/src/leapfrogai_ui/tests/file-management.test.ts +++ b/src/leapfrogai_ui/tests/file-management.test.ts @@ -1,5 +1,5 @@ import { expect, test } from './fixtures'; -import { loadChatPage } from './helpers'; +import { getSimpleMathQuestion, loadChatPage, sendMessage } from './helpers'; import type { Page } from '@playwright/test'; const loadFileManagementPage = async (page: Page) => { @@ -42,6 +42,21 @@ test.beforeEach(async ({ page }) => { console.log('After Each completed'); }); +test('it can navigate to the last visited thread with breadcrumbs', async ({ page }) => { + const newMessage = getSimpleMathQuestion(); + await page.goto('/chat'); + await sendMessage(page, newMessage); + const messages = page.getByTestId('message'); + await expect(messages).toHaveCount(2); + + const urlParts = new URL(page.url()).pathname.split('/'); + const threadId = urlParts[urlParts.length - 1]; + + await page.goto('/chat/file-management'); + await page.getByRole('link', { name: 'Chat' }).click(); + await page.waitForURL(`/chat/${threadId}`); +}); + test('it can navigate to the file management page', async ({ page }) => { await loadChatPage(page); diff --git a/src/leapfrogai_ui/tests/import_export.test.ts b/src/leapfrogai_ui/tests/import_export.test.ts index 325347a4a..c26b62425 100644 --- a/src/leapfrogai_ui/tests/import_export.test.ts +++ b/src/leapfrogai_ui/tests/import_export.test.ts @@ -17,7 +17,7 @@ test('it can import and exports threads', async ({ page }) => { await expect(page.getByText(thread.metadata.label)).toHaveCount(1); const downloadPromise = page.waitForEvent('download'); - await page.getByText('Export data').click(); + await page.getByText('Export chat history').click(); const download = await downloadPromise;