Skip to content

Commit

Permalink
539 UI enhancements v2 (#617)
Browse files Browse the repository at this point in the history
- Add chat link to LF logo that links back to last viewed chat thread (breadcrumbs also link back to last thread)
- Update import/export text
- Remove excess padding in sidebar
- A few test typescript cleanups
- Fixes bug where thread messages wouldn't show without a page refresh when using in app navigation
  • Loading branch information
andrewrisse committed Jun 12, 2024
1 parent 4b69d3c commit 3577947
Show file tree
Hide file tree
Showing 19 changed files with 225 additions and 99 deletions.
1 change: 0 additions & 1 deletion src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 28 additions & 14 deletions src/leapfrogai_ui/src/lib/components/ChatSidebar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion src/leapfrogai_ui/src/lib/components/ImportExport.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@
id="export-btn"
kind="ghost"
icon={Export}
size="small"
iconDescription="Export conversations"
on:click={onExport}>Export data</Button
on:click={onExport}>Export chat history</Button
>
</div>

Expand All @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/leapfrogai_ui/src/lib/components/ImportExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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',
Expand All @@ -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');
});
Expand Down
5 changes: 3 additions & 2 deletions src/leapfrogai_ui/src/lib/components/LFFileUploader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<div>
<input
data-testid="import data input"
data-testid="import-chat-history-input"
id="import-conversations"
bind:this={ref}
type="file"
Expand All @@ -40,8 +40,9 @@
id="import-btn"
kind="ghost"
disabled={importing}
size="small"
icon={Download}
iconDescription="Import conversations"
on:click={() => ref?.click()}>Import data</Button
on:click={() => ref?.click()}>Import chat history</Button
>
</div>
10 changes: 9 additions & 1 deletion src/leapfrogai_ui/src/lib/components/LFHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,7 +37,14 @@
persistentHamburgerMenu={innerWidth ? innerWidth < 1056 : false}
bind:isSideNavOpen={$uiStore.isSideNavOpen}
>
<span slot="platform"><img alt="LeapfrogAI Logo" src={logo} class="logo" /></span>
<span slot="platform"
><a
data-testid="header-logo-link"
href={$threadsStore.lastVisitedThreadId
? `/chat/${$threadsStore.lastVisitedThreadId}`
: '/chat'}><img alt="LeapfrogAI Logo" src={logo} class="logo" /></a
></span
>
<HeaderUtilities>
<HeaderAction
data-testid="settings header action button"
Expand Down
12 changes: 12 additions & 0 deletions src/leapfrogai_ui/src/lib/components/LFHeader.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { render, screen } from '@testing-library/svelte';
import { LFHeader } from '$components/index';
import userEvent from '@testing-library/user-event';
import { threadsStore } from '$stores';
import { getFakeThread } from '$testUtils/fakeData';

describe('LFHeader', () => {
it('closes the other header actions when one is opened', async () => {
Expand All @@ -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}`);
});
});
8 changes: 7 additions & 1 deletion src/leapfrogai_ui/src/lib/stores/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -164,6 +169,7 @@ const createThreadsStore = () => {
...old,
threads: old.threads.filter((c) => c.id !== id)
}));
await goto(`/chat`);
} catch {
toastStore.addToast({
kind: 'error',
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LFThread | null> => {
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<Profile[]>()
.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 };
};
Loading

0 comments on commit 3577947

Please sign in to comment.