Skip to content

Commit

Permalink
Fetch individual threads (#679)
Browse files Browse the repository at this point in the history
* Refactor to fetch a thread when the thread is clicked on in the sidenav.
* Fixes some typing issues, as well as sets a better pattern for keeping threads up to date.
* Also fixes some edge cases where messages are not ordered correctly.
* Included some test cleanup
  • Loading branch information
andrewrisse authored Jul 8, 2024
1 parent 80adda2 commit 7b28de8
Show file tree
Hide file tree
Showing 25 changed files with 388 additions and 479 deletions.
5 changes: 5 additions & 0 deletions src/leapfrogai_ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ Stop Supabase:

`npm run supabase:stop`

_Warning - if switching the application from utilizing Leapfrog API to OpenAI or vice versa,
and you encounter this error:_
`Server responded with status code 431. See https://vitejs.dev/guide/troubleshooting.html#_431-request-header-fields-too-large.`
_you need to clear your browser cookies_

### Building

To create a production version of the app:
Expand Down
114 changes: 41 additions & 73 deletions src/leapfrogai_ui/src/lib/components/ChatSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
import {
Button,
Modal,
OverflowMenu,
OverflowMenuItem,
SideNav,
SideNavItems,
SideNavMenu,
Expand All @@ -15,50 +13,46 @@
import { MAX_LABEL_SIZE } from '$lib/constants';
import { threadsStore, uiStore } from '$stores';
import { page } from '$app/stores';
import { browser } from '$app/environment';
import ImportExport from '$components/ImportExport.svelte';
import Fuse, { type FuseResult, type IFuseOptions } from 'fuse.js';
import { onMount } from 'svelte';
import type { LFThread } from '$lib/types/threads';
import { getMessageText } from '$helpers/threads';
import { goto } from '$app/navigation';
import ThreadOverflowMenu from '$components/ThreadOverflowMenu.svelte';
let deleteModalOpen = false;
let editMode = false;
let editThreadId: string | null = null;
let editLabelText: string | undefined = undefined;
let editLabelInputDisabled = false;
let disableScroll = false;
let overflowMenuOpen = false;
let menuOffset = 0;
let scrollOffset = 0;
let activeThreadRef: HTMLElement | null;
let scrollBoxRef: HTMLElement;
let searchText = '';
let searchResults: FuseResult<LFThread>[];
let filteredThreads: LFThread[] = [];
let editLabelText: string | undefined;
let sideNavItemRefs: { [id: string]: HTMLAnchorElement } = {};
let editMode = false;
$: activeThread = $threadsStore.threads.find((thread) => thread.id === $page.params.thread_id);
$: editMode = !!activeThread?.id && editThreadId === activeThread.id;
$: activeThread = $page.data.thread;
$: organizedThreads = dates.organizeThreadsByDate(
searchText !== '' ? filteredThreads : $threadsStore.threads
);
$: selectedThread = $threadsStore.threads.find(
(thread) => thread.id === $uiStore.selectedThreadOverflowMenuId
);
const resetEditMode = () => {
disableScroll = false;
editThreadId = null;
uiStore.setSelectedThreadOverflowMenuId('');
editLabelText = undefined;
editLabelInputDisabled = false;
editMode = false;
};
const saveNewLabel = async () => {
if (editThreadId && editLabelText) {
if ($uiStore.selectedThreadOverflowMenuId && editLabelText) {
editLabelInputDisabled = true;
await threadsStore.updateThreadLabel(editThreadId, editLabelText);
resetEditMode();
await threadsStore.updateThreadLabel($uiStore.selectedThreadOverflowMenuId, editLabelText);
}
resetEditMode();
};
const handleEdit = async (e: KeyboardEvent | FocusEvent) => {
Expand All @@ -79,30 +73,18 @@
};
const handleDelete = async () => {
delete sideNavItemRefs[$uiStore.selectedThreadOverflowMenuId];
deleteModalOpen = false;
if (activeThread?.id) {
await threadsStore.deleteThread(activeThread.id);
if ($uiStore.selectedThreadOverflowMenuId) {
await threadsStore.deleteThread($uiStore.selectedThreadOverflowMenuId);
}
await goto('/chat');
};
const handleActiveThreadChange = (id: string) => {
threadsStore.changeThread(id);
activeThreadRef = document.getElementById(`side-nav-menu-item-${id}`);
};
// To properly display the overflow menu items for each thread, we have to calculate the height they
// should be displayed at due to the carbon override for allowing overflow
$: if (browser && activeThreadRef) {
menuOffset = activeThreadRef?.offsetTop;
scrollOffset = scrollBoxRef?.scrollTop;
} else {
if (!activeThreadRef) {
menuOffset = 0;
scrollOffset = 0;
}
}
const options: IFuseOptions<unknown> = {
keys: ['metadata.label', 'messages.content'],
minMatchCharLength: 3,
Expand Down Expand Up @@ -166,10 +148,11 @@
</div>

<div
class:noScroll={disableScroll || editMode}
class:noScroll={$uiStore.selectedThreadOverflowMenuId !== '' || editMode}
bind:this={scrollBoxRef}
class="threads"
data-testid="threads"
on:scroll={() => (scrollOffset = scrollBoxRef.scrollTop)}
>
{#each organizedThreads as category}
{#if category.threads.length > 0}
Expand All @@ -178,19 +161,30 @@
<SideNavMenuItem
data-testid="side-nav-menu-item-{thread.metadata.label}"
id="side-nav-menu-item-{thread.id}"
bind:ref={sideNavItemRefs[thread.id]}
isSelected={activeThread?.id === thread.id}
on:click={() => handleActiveThreadChange(thread.id)}
on:click={() => {
uiStore.setSelectedThreadOverflowMenuId('');
handleActiveThreadChange(thread.id);
}}
>
<div class="menu-content">
{#if editMode && activeThread?.id === thread.id}
{#if editMode && $uiStore.selectedThreadOverflowMenuId === thread.id}
<TextInput
bind:value={editLabelText}
size="sm"
class="edit-thread"
on:keydown={(e) => handleEdit(e)}
on:keydown={(e) => {
e.stopPropagation();
handleEdit(e);
}}
on:blur={(e) => {
e.stopPropagation();
handleEdit(e);
}}
on:click={(e) => {
e.stopPropagation();
}}
autofocus
maxlength={MAX_LABEL_SIZE}
readonly={editLabelInputDisabled}
Expand All @@ -201,39 +195,14 @@
{thread.metadata.label}
</div>
<div>
<OverflowMenu
id={`overflow-menu-${thread.id}`}
on:close={() => {
overflowMenuOpen = false;
disableScroll = false;
}}
on:click={(e) => {
e.stopPropagation();
overflowMenuOpen = true;
handleActiveThreadChange(thread.id);
disableScroll = true;
}}
data-testid="overflow-menu-{thread.metadata.label}"
style={overflowMenuOpen && activeThread?.id === thread.id
? `position: fixed; top: 0; left: 0; transform: translate(224px, ${menuOffset - scrollOffset + 48}px)`
: ''}
>
<OverflowMenuItem
text="Edit"
on:click={() => {
editThreadId = thread.id;
editLabelText = thread.metadata.label;
}}
/>

<OverflowMenuItem
data-testid="overflow-menu-delete-{thread.metadata.label}"
text="Delete"
on:click={() => {
deleteModalOpen = true;
}}
/>
</OverflowMenu>
<ThreadOverflowMenu
{thread}
{scrollOffset}
parentSideNavRef={sideNavItemRefs[thread.id]}
bind:editLabelText
bind:editMode
bind:deleteModalOpen
/>
</div>
{/if}
</div>
Expand Down Expand Up @@ -263,7 +232,7 @@
on:close
on:submit={handleDelete}
>Are you sure you want to delete your <strong
>{activeThread?.metadata.label.substring(0, MAX_LABEL_SIZE)}</strong
>{selectedThread?.metadata.label.substring(0, MAX_LABEL_SIZE)}</strong
> chat?</Modal
>
</div></SideNav
Expand Down Expand Up @@ -325,7 +294,6 @@ https://github.com/carbon-design-system/carbon-components-svelte/issues/892
display: flex;
align-items: center;
justify-content: space-between;
width: 208px;
.menu-text {
width: 192px;
Expand Down
47 changes: 15 additions & 32 deletions src/leapfrogai_ui/src/lib/components/ChatSidebar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import { fireEvent, render, screen, within } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { fakeThreads, getFakeThread } from '$testUtils/fakeData';
import { vi } from 'vitest';
import stores from '$app/stores';

import { getUnixSeconds, monthNames } from '$helpers/dates';
import * as navigation from '$app/navigation';
import { getMessageText } from '$helpers/threads';
import { NO_SELECTED_ASSISTANT_ID } from '$constants';

const { getStores } = await vi.hoisted(() => import('../../lib/mocks/svelte'));

const editThreadsLabel = async (oldLabel: string, newLabel: string, keyToPress = '{enter}') => {
const overflowMenu = within(screen.getByTestId(`side-nav-menu-item-${oldLabel}`)).getByRole(
'button',
Expand All @@ -33,39 +31,11 @@ const editThreadsLabel = async (oldLabel: string, newLabel: string, keyToPress =
await userEvent.keyboard(keyToPress);
};

vi.mock('$app/stores', (): typeof stores => {
const page: typeof stores.page = {
subscribe(fn) {
return getStores({
url: `http://localhost/chat/${fakeThreads[0].id}`,
params: { thread_id: fakeThreads[0].id }
}).page.subscribe(fn);
}
};
const navigating: typeof stores.navigating = {
subscribe(fn) {
return getStores().navigating.subscribe(fn);
}
};
const updated: typeof stores.updated = {
subscribe(fn) {
return getStores().updated.subscribe(fn);
},
check: () => Promise.resolve(false)
};

return {
getStores,
navigating,
page,
updated
};
});

describe('ChatSidebar', () => {
it('renders threads', async () => {
threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -90,6 +60,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: [fakeTodayThread, fakeYesterdayThread], // uses date override starting in March
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: ''
});
Expand All @@ -114,6 +85,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand Down Expand Up @@ -151,6 +123,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand Down Expand Up @@ -184,6 +157,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -204,6 +178,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -224,6 +199,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand Down Expand Up @@ -255,6 +231,7 @@ describe('ChatSidebar', () => {
const newLabelText = 'new label';
threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -275,6 +252,7 @@ describe('ChatSidebar', () => {
const newLabelText = 'new label';
threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -297,6 +275,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -321,6 +300,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: fakeThreads,
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -338,6 +318,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: [fakeThread],
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: fakeThreads[0].id
});
Expand All @@ -358,6 +339,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: [fakeThread1, fakeThread2, fakeThread3],
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: ''
});
Expand All @@ -383,6 +365,7 @@ describe('ChatSidebar', () => {

threadsStore.set({
threads: [fakeThread1, fakeThread2, fakeThread3],
sendingBlocked: false,
selectedAssistantId: NO_SELECTED_ASSISTANT_ID,
lastVisitedThreadId: ''
});
Expand Down
Loading

0 comments on commit 7b28de8

Please sign in to comment.