diff --git a/src/leapfrogai_ui/src/app.d.ts b/src/leapfrogai_ui/src/app.d.ts index d493910cc..f19b0b155 100644 --- a/src/leapfrogai_ui/src/app.d.ts +++ b/src/leapfrogai_ui/src/app.d.ts @@ -23,7 +23,6 @@ declare global { profile?: Profile; threads?: LFThread[]; assistants?: LFAssistant[]; - assistant?: LFAssistant; files?: FileObject[]; keys?: APIKeyRow[]; } diff --git a/src/leapfrogai_ui/src/lib/components/AssistantAvatar.svelte b/src/leapfrogai_ui/src/lib/components/AssistantAvatar.svelte index ceca70148..a5e6d8105 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantAvatar.svelte +++ b/src/leapfrogai_ui/src/lib/components/AssistantAvatar.svelte @@ -33,8 +33,7 @@ ignoreLocation: true }; - $: fileNotUploaded = !$form.avatarFile; // if on upload tab, you must upload a file to enable save - + $: fileNotUploaded = !$form.avatar && !$form.avatarFile; // if on upload tab, you must upload a file to enable save $: avatarToShow = $form.avatarFile ? URL.createObjectURL($form.avatarFile) : $form.avatar; $: fileTooBig = $form.avatarFile?.size > MAX_AVATAR_SIZE; @@ -66,9 +65,7 @@ modalOpen = false; $form.avatar = originalAvatar; tempPictogram = selectedPictogramName; // reset to original pictogram - if ($form.avatar) { - $form.avatarFile = $form.avatar; // reset to original file - } else { + if (!$form.avatar) { clearFileInput(); } fileUploaderRef.value = ''; // Reset the file input value to ensure input event detection @@ -102,7 +99,7 @@ } } else { // pictogram tab - selectedPictogramName = tempPictogram; // TODO - can we remove this line + selectedPictogramName = tempPictogram; $form.pictogram = tempPictogram; $form.avatar = ''; // remove saved avatar clearFileInput(); @@ -197,8 +194,6 @@ > Upload from computer - - {#if hideUploader} @@ -222,7 +217,9 @@ - + { @@ -236,5 +233,6 @@ name="avatarFile" class="sr-only" /> - + + diff --git a/src/leapfrogai_ui/src/lib/components/AssistantCard.svelte b/src/leapfrogai_ui/src/lib/components/AssistantCard.svelte index ceabb4098..dfa88a3e4 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantCard.svelte +++ b/src/leapfrogai_ui/src/lib/components/AssistantCard.svelte @@ -1,10 +1,10 @@ diff --git a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte index 74d0f8ba9..6cf1ab3e5 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte +++ b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte @@ -2,7 +2,7 @@ import { fade } from 'svelte/transition'; import { filesStore } from '$stores'; import type { FilesForm } from '$lib/types/files'; - import { ACCEPTED_FILE_TYPES, STANDARD_FADE_DURATION } from '$constants'; + import { ACCEPTED_DOC_TYPES, STANDARD_FADE_DURATION } from '$constants'; import AssistantFileDropdown from '$components/AssistantFileDropdown.svelte'; import FileUploaderItem from '$components/FileUploaderItem.svelte'; @@ -17,7 +17,7 @@ .filter((id) => $filesStore.selectedAssistantFileIds.includes(id)); - +
{#each filteredStoreFiles as file} diff --git a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts index 6bb15f2ae..61c3efed9 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts +++ b/src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts @@ -4,14 +4,14 @@ import AssistantFileSelect from '$components/AssistantFileSelect.svelte'; import { superValidate } from 'sveltekit-superforms'; import { yup } from 'sveltekit-superforms/adapters'; import { filesSchema } from '$schemas/files'; -import type { FileRow } from '$lib/types/files'; +import type { LFFileObject } from '$lib/types/files'; import { getUnixSeconds } from '$helpers/dates'; import userEvent from '@testing-library/user-event'; const filesForm = await superValidate({}, yup(filesSchema), { errors: false }); describe('AssistantFileSelect', () => { - const mockFiles: FileRow[] = [ + const mockFiles: LFFileObject[] = [ { id: '1', filename: 'file1.pdf', status: 'complete', created_at: getUnixSeconds(new Date()) }, { id: '2', filename: 'file2.pdf', status: 'error', created_at: getUnixSeconds(new Date()) }, { id: '3', filename: 'file3.txt', status: 'uploading', created_at: getUnixSeconds(new Date()) } diff --git a/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte b/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte index 815e009b2..8e7c97a5a 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte +++ b/src/leapfrogai_ui/src/lib/components/AssistantForm.svelte @@ -6,11 +6,11 @@ } from '$lib/constants'; import { superForm } from 'sveltekit-superforms'; import { page } from '$app/stores'; - import { beforeNavigate, goto, invalidate } from '$app/navigation'; + import { beforeNavigate, goto } from '$app/navigation'; import { Button, Modal, P } from 'flowbite-svelte'; import Slider from '$components/Slider.svelte'; import { yup } from 'sveltekit-superforms/adapters'; - import { filesStore, toastStore, uiStore } from '$stores'; + import { assistantsStore, filesStore, toastStore, uiStore } from '$stores'; import { assistantInputSchema, editAssistantInputSchema } from '$lib/schemas/assistants'; import type { NavigationTarget } from '@sveltejs/kit'; import { onMount } from 'svelte'; @@ -25,6 +25,10 @@ let bypassCancelWarning = false; + $: assistant = $assistantsStore.assistants.find( + (assistant) => assistant.id === $page.params.assistantId + ); + const { form, errors, enhance, submitting, isTainted, delayed } = superForm(data.form, { invalidateAll: false, validators: yup(isEditMode ? editAssistantInputSchema : assistantInputSchema), @@ -55,8 +59,12 @@ } bypassCancelWarning = true; - await invalidate('lf:assistants'); - goto(result.data.redirectUrl); + if (isEditMode) { + assistantsStore.updateAssistant(result.data.assistant); + } else { + assistantsStore.addAssistant(result.data.assistant); + } + await goto(result.data.redirectUrl); } else if (result.type === 'failure') { // 400 errors will show errors for the respective fields, do not show toast if (result.status !== 400) { @@ -174,7 +182,7 @@
diff --git a/src/leapfrogai_ui/src/lib/components/AssistantProgressToast.test.ts b/src/leapfrogai_ui/src/lib/components/AssistantProgressToast.test.ts index fb21bd849..fc1d5c5e4 100644 --- a/src/leapfrogai_ui/src/lib/components/AssistantProgressToast.test.ts +++ b/src/leapfrogai_ui/src/lib/components/AssistantProgressToast.test.ts @@ -10,7 +10,7 @@ import AssistantProgressToast from '$components/AssistantProgressToast.svelte'; import { render, screen } from '@testing-library/svelte'; import filesStore from '$stores/filesStore'; import { getFakeFiles } from '$testUtils/fakeData'; -import { convertFileObjectToFileRows } from '$helpers/fileHelpers'; +import { convertFileObjectToLFFileObject } from '$helpers/fileHelpers'; import { delay } from 'msw'; import { vi } from 'vitest'; import { toastStore } from '$stores'; @@ -27,7 +27,7 @@ describe('AssistantProgressToast', () => { fileIds: files.map((file) => file.id), vectorStoreId: '123' }; - filesStore.setFiles(convertFileObjectToFileRows(files)); + filesStore.setFiles(convertFileObjectToLFFileObject(files)); const timeout = 10; //10ms render(AssistantProgressToast, { timeout, toast }); //10ms timeout diff --git a/src/leapfrogai_ui/src/lib/components/ChatFileUpload.svelte b/src/leapfrogai_ui/src/lib/components/ChatFileUpload.svelte index e01575ce2..73356ee1a 100644 --- a/src/leapfrogai_ui/src/lib/components/ChatFileUpload.svelte +++ b/src/leapfrogai_ui/src/lib/components/ChatFileUpload.svelte @@ -1,7 +1,7 @@
diff --git a/src/leapfrogai_ui/src/lib/components/modals/ConfirmFilesDeleteModal.svelte b/src/leapfrogai_ui/src/lib/components/modals/ConfirmFilesDeleteModal.svelte index d80d93147..d581f83cd 100644 --- a/src/leapfrogai_ui/src/lib/components/modals/ConfirmFilesDeleteModal.svelte +++ b/src/leapfrogai_ui/src/lib/components/modals/ConfirmFilesDeleteModal.svelte @@ -3,7 +3,6 @@ import type { Assistant } from 'openai/resources/beta/assistants'; import { filesStore, toastStore } from '$stores'; import { ExclamationCircleOutline } from 'flowbite-svelte-icons'; - import { invalidate } from '$app/navigation'; import { createEventDispatcher } from 'svelte'; import vectorStatusStore from '$stores/vectorStatusStore'; @@ -12,6 +11,8 @@ export let deleting: boolean; export let affectedAssistants: Assistant[]; + $: isMultipleFiles = $filesStore.selectedFileManagementFileIds.length > 1; + const dispatch = createEventDispatcher(); const handleCancel = () => { @@ -20,34 +21,43 @@ affectedAssistantsLoading = false; }; + const handleDeleteError = () => { + toastStore.addToast({ + kind: 'error', + title: `Error Deleting ${isMultipleFiles ? 'Files' : 'File'}` + }); + }; + const handleConfirmedDelete = async () => { - const isMultipleFiles = $filesStore.selectedFileManagementFileIds.length > 1; deleting = true; - const res = await fetch('/api/files/delete', { - method: 'DELETE', - body: JSON.stringify({ ids: $filesStore.selectedFileManagementFileIds }), - headers: { - 'Content-Type': 'application/json' - } - }); - open = false; - await invalidate('lf:files'); - if (res.ok) { - toastStore.addToast({ - kind: 'success', - title: `${isMultipleFiles ? 'Files' : 'File'} Deleted` - }); - } else { - toastStore.addToast({ - kind: 'error', - title: `Error Deleting ${isMultipleFiles ? 'Files' : 'File'}` + try { + const res = await fetch('/api/files/delete', { + method: 'DELETE', + body: JSON.stringify({ ids: $filesStore.selectedFileManagementFileIds }), + headers: { + 'Content-Type': 'application/json' + } }); - } - vectorStatusStore.removeFiles($filesStore.selectedFileManagementFileIds); - filesStore.setSelectedFileManagementFileIds([]); + if (res.ok) { + open = false; + for (const id of $filesStore.selectedFileManagementFileIds) { + filesStore.removeFile(id); + } + vectorStatusStore.removeFiles($filesStore.selectedFileManagementFileIds); + filesStore.setSelectedFileManagementFileIds([]); + toastStore.addToast({ + kind: 'success', + title: `${isMultipleFiles ? 'Files' : 'File'} Deleted` + }); + dispatch('delete'); + } else { + handleDeleteError(); + } + } catch { + handleDeleteError(); + } deleting = false; - dispatch('delete'); }; $: fileNames = $filesStore.files diff --git a/src/leapfrogai_ui/src/lib/components/modals/DeleteApiKeyModal.svelte b/src/leapfrogai_ui/src/lib/components/modals/DeleteApiKeyModal.svelte index 58b0d9d58..c0c7083a8 100644 --- a/src/leapfrogai_ui/src/lib/components/modals/DeleteApiKeyModal.svelte +++ b/src/leapfrogai_ui/src/lib/components/modals/DeleteApiKeyModal.svelte @@ -10,10 +10,12 @@ export let selectedRowIds: string[]; export let deleting: boolean; + $: isMultiple = selectedRowIds.length > 1; + const dispatch = createEventDispatcher(); - $: keyNames = $page.data.keys - ? $page.data.keys + $: keyNames = $page.data.apiKeys + ? $page.data.apiKeys .map((key) => { if (selectedRowIds.includes(key.id)) return key.name; }) @@ -25,27 +27,35 @@ confirmDeleteModalOpen = false; }; + const handleDeleteError = () => { + toastStore.addToast({ + kind: 'error', + title: `Error Deleting ${isMultiple ? 'Keys' : 'Key'}` + }); + }; + const handleDelete = async () => { deleting = true; - const isMultiple = selectedRowIds.length > 1; - const res = await fetch('/api/api-keys/delete', { - body: JSON.stringify({ ids: selectedRowIds }), - method: 'DELETE' - }); - dispatch('delete', selectedRowIds); - deleting = false; - if (res.ok) { - toastStore.addToast({ - kind: 'success', - title: `${isMultiple ? 'Keys' : 'Key'} Deleted` - }); - } else { - toastStore.addToast({ - kind: 'error', - title: `Error Deleting ${isMultiple ? 'Keys' : 'Key'}` + try { + const res = await fetch('/api/api-keys/delete', { + body: JSON.stringify({ ids: selectedRowIds }), + method: 'DELETE' }); + if (res.ok) { + dispatch('delete', selectedRowIds); + toastStore.addToast({ + kind: 'success', + title: `${isMultiple ? 'Keys' : 'Key'} Deleted` + }); + await invalidate('lf:api-keys'); + } else { + handleDeleteError(); + } + } catch { + handleDeleteError(); } - await invalidate('lf:api-keys'); + + deleting = false; }; diff --git a/src/leapfrogai_ui/src/lib/constants/index.ts b/src/leapfrogai_ui/src/lib/constants/index.ts index 5ad6cac6d..08e813bf0 100644 --- a/src/leapfrogai_ui/src/lib/constants/index.ts +++ b/src/leapfrogai_ui/src/lib/constants/index.ts @@ -52,7 +52,7 @@ export const ACCEPTED_AUDIO_FILE_TYPES = [ '.webm' ]; -export const ACCEPTED_FILE_TYPES = [ +export const ACCEPTED_DOC_TYPES = [ '.pdf', '.txt', '.text', @@ -62,7 +62,10 @@ export const ACCEPTED_FILE_TYPES = [ '.pptx', '.doc', '.docx', - '.csv', + '.csv' +]; +export const ACCEPTED_DOC_AND_AUDIO_FILE_TYPES = [ + ...ACCEPTED_DOC_TYPES, ...ACCEPTED_AUDIO_FILE_TYPES ]; @@ -108,7 +111,7 @@ export const NO_FILE_ERROR_TEXT = 'Please upload an image or select a pictogram' export const AVATAR_FILE_SIZE_ERROR_TEXT = `File must be less than ${MAX_AVATAR_SIZE / 1000000} MB`; export const FILE_SIZE_ERROR_TEXT = `File must be less than ${MAX_FILE_SIZE / 1000000} MB`; export const AUDIO_FILE_SIZE_ERROR_TEXT = `Audio file must be less than ${MAX_AUDIO_FILE_SIZE / 1000000} MB`; -export const INVALID_FILE_TYPE_ERROR_TEXT = `Invalid file type, accepted types are: ${ACCEPTED_FILE_TYPES.join(', ')}`; +export const INVALID_FILE_TYPE_ERROR_TEXT = `Invalid file type, accepted types are: ${ACCEPTED_DOC_AND_AUDIO_FILE_TYPES.join(', ')}`; export const INVALID_AUDIO_FILE_TYPE_ERROR_TEXT = `Invalid file type, accepted types are: ${ACCEPTED_AUDIO_FILE_TYPES.join(', ')}`; export const NO_SELECTED_ASSISTANT_ID = 'noSelectedAssistantId'; diff --git a/src/leapfrogai_ui/src/lib/helpers/fileHelpers.ts b/src/leapfrogai_ui/src/lib/helpers/fileHelpers.ts index a0cd0fc5b..b6d229336 100644 --- a/src/leapfrogai_ui/src/lib/helpers/fileHelpers.ts +++ b/src/leapfrogai_ui/src/lib/helpers/fileHelpers.ts @@ -1,11 +1,10 @@ -import type { FileMetadata, FileRow } from '$lib/types/files'; +import type { FileMetadata, LFFileObject } from '$lib/types/files'; import type { FileObject } from 'openai/resources/files'; import { FILE_CONTEXT_TOO_LARGE_ERROR_MSG } from '$constants/errors'; -export const convertFileObjectToFileRows = (files: FileObject[]): FileRow[] => +export const convertFileObjectToLFFileObject = (files: FileObject[]): LFFileObject[] => files.map((file) => ({ - id: file.id, - filename: file.filename, + ...file, created_at: file.created_at * 1000, status: 'hide' })); diff --git a/src/leapfrogai_ui/src/lib/mocks/file-mocks.ts b/src/leapfrogai_ui/src/lib/mocks/file-mocks.ts index f4ff4460f..88fa6d566 100644 --- a/src/leapfrogai_ui/src/lib/mocks/file-mocks.ts +++ b/src/leapfrogai_ui/src/lib/mocks/file-mocks.ts @@ -78,7 +78,7 @@ export const mockConvertFileErrorNoId = () => { export const mockDeleteCheck = (assistantsToReturn: LFAssistant[]) => { server.use( - http.post('/api/files/delete-check', async () => { + http.post('/api/files/delete/check', async () => { await delay(100); return HttpResponse.json(assistantsToReturn); }) diff --git a/src/leapfrogai_ui/src/lib/stores/assistantsStore.ts b/src/leapfrogai_ui/src/lib/stores/assistantsStore.ts new file mode 100644 index 000000000..b0356c576 --- /dev/null +++ b/src/leapfrogai_ui/src/lib/stores/assistantsStore.ts @@ -0,0 +1,57 @@ +import { writable } from 'svelte/store'; +import type { LFAssistant } from '$lib/types/assistants'; +import { NO_SELECTED_ASSISTANT_ID } from '$constants'; + +type AssistantsStore = { + assistants: LFAssistant[]; + selectedAssistantId?: string; +}; + +const defaultValues: AssistantsStore = { + assistants: [], + selectedAssistantId: NO_SELECTED_ASSISTANT_ID +}; +const createAssistantsStore = () => { + const { subscribe, set, update } = writable({ ...defaultValues }); + + return { + subscribe, + set, + update, + setAssistants: (newAssistants: LFAssistant[]) => { + update((old) => ({ ...old, assistants: newAssistants })); + }, + setSelectedAssistantId: (selectedAssistantId: string) => { + update((old) => { + return { ...old, selectedAssistantId }; + }); + }, + addAssistant: (newAssistant: LFAssistant) => { + update((old) => ({ ...old, assistants: [...old.assistants, newAssistant] })); + }, + removeAssistant: (id: string) => { + update((old) => { + const updatedAssistants = [...old.assistants]; + const assistantIndex = updatedAssistants.findIndex((assistant) => assistant.id === id); + if (assistantIndex > -1) { + updatedAssistants.splice(assistantIndex, 1); + } + return { ...old, assistants: updatedAssistants }; + }); + }, + updateAssistant: (newAssistant: LFAssistant) => { + update((old) => { + const updatedAssistants = [...old.assistants]; + const assistantIndex = updatedAssistants.findIndex( + (assistant) => assistant.id === newAssistant.id + ); + if (assistantIndex > -1) { + updatedAssistants[assistantIndex] = newAssistant; + } + return { ...old, assistants: updatedAssistants }; + }); + } + }; +}; +const assistantsStore = createAssistantsStore(); +export default assistantsStore; diff --git a/src/leapfrogai_ui/src/lib/stores/filesStore.ts b/src/leapfrogai_ui/src/lib/stores/filesStore.ts index c6ba33db8..5e0eeea19 100644 --- a/src/leapfrogai_ui/src/lib/stores/filesStore.ts +++ b/src/leapfrogai_ui/src/lib/stores/filesStore.ts @@ -1,14 +1,16 @@ import { derived, writable } from 'svelte/store'; import type { FileObject } from 'openai/resources/files'; -import type { FileRow } from '$lib/types/files'; +import type { LFFileObject, PendingOrErrorFile } from '$lib/types/files'; import { toastStore } from '$stores/index'; +import { getUnixSeconds } from '$helpers/dates'; type FilesStore = { - files: FileRow[]; + files: LFFileObject[]; selectedFileManagementFileIds: string[]; selectedAssistantFileIds: string[]; uploading: boolean; - pendingUploads: FileRow[]; + pendingUploads: PendingOrErrorFile[]; + needsUpdate?: boolean; }; const defaultValues: FilesStore = { @@ -16,7 +18,8 @@ const defaultValues: FilesStore = { selectedFileManagementFileIds: [], selectedAssistantFileIds: [], uploading: false, - pendingUploads: [] + pendingUploads: [], + needsUpdate: false }; const createFilesStore = () => { @@ -27,16 +30,32 @@ const createFilesStore = () => { set, update, setUploading: (status: boolean) => update((old) => ({ ...old, uploading: status })), - - setFiles: (newFiles: FileRow[]) => { + removeFile: (id: string) => { + update((old) => { + const updatedFiles = [...old.files]; + const fileIndex = updatedFiles.findIndex((file) => file.id === id); + if (fileIndex > -1) { + updatedFiles.splice(fileIndex, 1); + } + return { ...old, files: updatedFiles }; + }); + }, + setFiles: (newFiles: LFFileObject[]) => { update((old) => ({ ...old, files: [...newFiles] })); }, - setPendingUploads: (newFiles: FileRow[]) => { + setPendingUploads: (newFiles: LFFileObject[]) => { update((old) => ({ ...old, pendingUploads: [...newFiles] })); }, setSelectedFileManagementFileIds: (newIds: string[]) => { update((old) => ({ ...old, selectedFileManagementFileIds: newIds })); }, + setNeedsUpdate: (status: boolean) => { + update((old) => ({ ...old, needsUpdate: status })); + }, + fetchFiles: async () => { + const files = await fetch('/api/files').then((res) => res.json()); + update((old) => ({ ...old, files, needsUpdate: false })); + }, addSelectedFileManagementFileId: (id: string) => { update((old) => ({ ...old, @@ -66,7 +85,7 @@ const createFilesStore = () => { }, addUploadingFiles: (files: File[], { autoSelectUploadedFiles = false } = {}) => { update((old) => { - const newFiles: FileRow[] = []; + const newFiles: Pick[] = []; const newFileIds: string[] = []; for (const file of files) { const id = `${file.name}-${new Date()}`; // temp id @@ -74,7 +93,7 @@ const createFilesStore = () => { id, filename: file.name, status: 'uploading', - created_at: null + created_at: getUnixSeconds(new Date()) }); newFileIds.push(id); } @@ -87,16 +106,14 @@ const createFilesStore = () => { }; }); }, - updateWithUploadErrors: (newFiles: Array) => { + updateWithUploadErrors: (newFiles: Array) => { update((old) => { - const failedRows: FileRow[] = []; + const failedRows: LFFileObject[] = []; for (const file of newFiles) { if (file.status === 'error') { - const row: FileRow = { - id: file.id, - filename: file.filename, - created_at: file.created_at, + const row: LFFileObject = { + ...file, status: 'error' }; @@ -126,15 +143,13 @@ const createFilesStore = () => { }; }); }, - updateWithUploadSuccess: (newFiles: Array) => { + updateWithUploadSuccess: (newFiles: Array) => { update((old) => { const successRows = [...old.files]; for (const file of newFiles) { - const row: FileRow = { - id: file.id, - filename: file.filename, - created_at: file.created_at, + const row: LFFileObject = { + ...file, status: 'complete' }; diff --git a/src/leapfrogai_ui/src/lib/stores/index.ts b/src/leapfrogai_ui/src/lib/stores/index.ts index 90cac2ebd..66da975b0 100644 --- a/src/leapfrogai_ui/src/lib/stores/index.ts +++ b/src/leapfrogai_ui/src/lib/stores/index.ts @@ -2,3 +2,4 @@ export { default as threadsStore } from './threads'; export { default as toastStore } from './toast'; export { default as uiStore } from './ui'; export { default as filesStore } from './filesStore'; +export { default as assistantsStore } from './assistantsStore'; diff --git a/src/leapfrogai_ui/src/lib/stores/threads.ts b/src/leapfrogai_ui/src/lib/stores/threads.ts index 0b9738fbb..a79c66f1a 100644 --- a/src/leapfrogai_ui/src/lib/stores/threads.ts +++ b/src/leapfrogai_ui/src/lib/stores/threads.ts @@ -1,6 +1,6 @@ import { writable } from 'svelte/store'; -import { MAX_LABEL_SIZE, NO_SELECTED_ASSISTANT_ID } from '$lib/constants'; -import { goto, invalidate } from '$app/navigation'; +import { MAX_LABEL_SIZE } from '$lib/constants'; +import { goto } from '$app/navigation'; import { error } from '@sveltejs/kit'; import { type Message as VercelAIMessage } from '@ai-sdk/svelte'; import { toastStore } from '$stores'; @@ -12,7 +12,6 @@ import type { Message } from 'ai'; type ThreadsStore = { threads: LFThread[]; - selectedAssistantId: string; sendingBlocked: boolean; lastVisitedThreadId: string; streamingMessage: VercelAIMessage | null; @@ -20,7 +19,6 @@ type ThreadsStore = { const defaultValues: ThreadsStore = { threads: [], - selectedAssistantId: NO_SELECTED_ASSISTANT_ID, sendingBlocked: false, lastVisitedThreadId: '', streamingMessage: null @@ -97,11 +95,6 @@ const createThreadsStore = () => { setLastVisitedThreadId: (id: string) => { update((old) => ({ ...old, lastVisitedThreadId: id })); }, - setSelectedAssistantId: (selectedAssistantId: string) => { - update((old) => { - return { ...old, selectedAssistantId }; - }); - }, // Important - this method has a built in delay to ensure next user message has a different timestamp when setting to false (unblocking) setSendingBlocked: async (status: boolean) => { if (!status && process.env.NODE_ENV !== 'test') { @@ -303,7 +296,6 @@ const createThreadsStore = () => { title: 'Error', subtitle: `Error deleting message.` }); - await invalidate('lf:threads'); } }, updateThreadLabel: async (id: string, newLabel: string) => { diff --git a/src/leapfrogai_ui/src/lib/types/files.d.ts b/src/leapfrogai_ui/src/lib/types/files.d.ts index 599260041..17355cd32 100644 --- a/src/leapfrogai_ui/src/lib/types/files.d.ts +++ b/src/leapfrogai_ui/src/lib/types/files.d.ts @@ -1,16 +1,16 @@ import type { SuperValidated } from 'sveltekit-superforms'; +import type { FileObject } from 'openai/resources/files'; export type FileUploadStatus = 'uploading' | 'complete' | 'error' | 'hide'; export type VectorStatus = 'in_progress' | 'completed' | 'cancelled' | 'failed'; -export type FileRow = { - id: string; - filename: string; - created_at: number | null; +export type LFFileObject = Omit & { status: FileUploadStatus; }; +export type PendingOrErrorFile = Pick; + // This type is taken from SuperValidated, leaving the any export type FilesForm = SuperValidated< { files?: (File | null | undefined)[] | undefined }, diff --git a/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts b/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts index 785c289ac..eacdd3b2d 100644 --- a/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts +++ b/src/leapfrogai_ui/src/routes/api/api-keys/delete/+server.ts @@ -10,7 +10,6 @@ export const DELETE: RequestHandler = async ({ request, locals: { session } }) = if (!session) { error(401, 'Unauthorized'); } - let requestData: { ids: string }; // Validate request body diff --git a/src/leapfrogai_ui/src/routes/api/files/delete/+server.ts b/src/leapfrogai_ui/src/routes/api/files/delete/+server.ts index 935195842..e8942d8da 100644 --- a/src/leapfrogai_ui/src/routes/api/files/delete/+server.ts +++ b/src/leapfrogai_ui/src/routes/api/files/delete/+server.ts @@ -8,7 +8,6 @@ export const DELETE: RequestHandler = async ({ request, locals: { session } }) = error(401, 'Unauthorized'); } let requestData: { ids: string[] }; - // Validate request body try { requestData = await request.json(); diff --git a/src/leapfrogai_ui/src/routes/api/files/delete-check/+server.ts b/src/leapfrogai_ui/src/routes/api/files/delete/check/+server.ts similarity index 100% rename from src/leapfrogai_ui/src/routes/api/files/delete-check/+server.ts rename to src/leapfrogai_ui/src/routes/api/files/delete/check/+server.ts diff --git a/src/leapfrogai_ui/src/routes/api/files/delete-check/server.test.ts b/src/leapfrogai_ui/src/routes/api/files/delete/check/server.test.ts similarity index 86% rename from src/leapfrogai_ui/src/routes/api/files/delete-check/server.test.ts rename to src/leapfrogai_ui/src/routes/api/files/delete/check/server.test.ts index 1f6bb19bc..f78b142e9 100644 --- a/src/leapfrogai_ui/src/routes/api/files/delete-check/server.test.ts +++ b/src/leapfrogai_ui/src/routes/api/files/delete/check/server.test.ts @@ -1,5 +1,5 @@ import { POST } from './+server'; -import { mockOpenAI } from '../../../../../vitest-setup'; +import { mockOpenAI } from '../../../../../../vitest-setup'; import { getFakeAssistant, getFakeFiles, @@ -7,11 +7,11 @@ import { getFakeVectorStoreFile } from '$testUtils/fakeData'; import type { RequestEvent } from '@sveltejs/kit'; -import type { RouteParams } from '../../../../../.svelte-kit/types/src/routes/api/messages/new/$types'; +import type { RouteParams } from './$types'; import { getLocalsMock } from '$lib/mocks/misc'; const validMessageBody = { fileIds: ['file1', 'file2'] }; -describe('/api/files/delete-check', () => { +describe('/api/files/delete/check', () => { it('returns a 401 when there is no session', async () => { const request = new Request('http://thisurlhasnoeffect', { method: 'POST', @@ -22,7 +22,7 @@ describe('/api/files/delete-check', () => { POST({ request, locals: getLocalsMock({ nullSession: true }) - } as RequestEvent) + } as RequestEvent) ).rejects.toMatchObject({ status: 401 }); @@ -39,7 +39,7 @@ describe('/api/files/delete-check', () => { POST({ request, locals: getLocalsMock() - } as RequestEvent) + } as RequestEvent) ).rejects.toMatchObject({ status: 400 }); @@ -54,7 +54,7 @@ describe('/api/files/delete-check', () => { POST({ request, locals: getLocalsMock() - } as RequestEvent) + } as RequestEvent) ).rejects.toMatchObject({ status: 400 }); @@ -69,7 +69,7 @@ describe('/api/files/delete-check', () => { POST({ request, locals: getLocalsMock() - } as RequestEvent) + } as RequestEvent) ).rejects.toMatchObject({ status: 400 }); @@ -84,7 +84,7 @@ describe('/api/files/delete-check', () => { POST({ request, locals: getLocalsMock() - } as RequestEvent) + } as RequestEvent) ).rejects.toMatchObject({ status: 400 }); @@ -137,7 +137,7 @@ describe('/api/files/delete-check', () => { const res = await POST({ request, locals: getLocalsMock() - } as RequestEvent); + } as RequestEvent); const resData = await res.json(); expect(res.status).toEqual(200); @@ -153,7 +153,7 @@ describe('/api/files/delete-check', () => { const res2 = await POST({ request: request2, locals: getLocalsMock() - } as RequestEvent); + } as RequestEvent); const resData2 = await res2.json(); expect(res2.status).toEqual(200); @@ -173,7 +173,7 @@ describe('/api/files/delete-check', () => { POST({ request, locals: getLocalsMock() - } as RequestEvent) + } as RequestEvent) ).rejects.toMatchObject({ status: 500 }); diff --git a/src/leapfrogai_ui/src/routes/api/helpers.ts b/src/leapfrogai_ui/src/routes/api/helpers.ts new file mode 100644 index 000000000..c64bfe611 --- /dev/null +++ b/src/leapfrogai_ui/src/routes/api/helpers.ts @@ -0,0 +1,18 @@ +import type { LFThread } from '$lib/types/threads'; +import { getOpenAiClient } from '$lib/server/constants'; +import type { LFMessage } from '$lib/types/messages'; + +export const getThreadWithMessages = async ( + thread_id: string, + access_token: string +): Promise => { + const openai = getOpenAiClient(access_token); + 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 }; +}; diff --git a/src/leapfrogai_ui/src/routes/api/threads/+server.ts b/src/leapfrogai_ui/src/routes/api/threads/+server.ts new file mode 100644 index 000000000..8158bab7a --- /dev/null +++ b/src/leapfrogai_ui/src/routes/api/threads/+server.ts @@ -0,0 +1,45 @@ +import type { RequestHandler } from './$types'; +import { error, json } from '@sveltejs/kit'; +import type { Profile } from '$lib/types/profile'; +import type { LFThread } from '$lib/types/threads'; +import { getThreadWithMessages } from '../helpers'; + +export const GET: RequestHandler = async ({ locals: { session, supabase, user } }) => { + if (!session) { + error(401, 'Unauthorized'); + } + + const { data: profile, error: profileError } = await supabase + .from('profiles') + .select(`*`) + .eq('id', user?.id) + .returns() + .single(); + + if (profileError) { + console.error( + `error getting user profile for user_id: ${user?.id}. ${JSON.stringify(profileError)}` + ); + error(500, 'Internal Error'); + } + + const threads: LFThread[] = []; + if (profile?.thread_ids && profile?.thread_ids.length > 0) { + try { + const threadPromises = profile.thread_ids.map((thread_id) => + getThreadWithMessages(thread_id, session.access_token) + ); + 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}`); + return json([]); + } + } + + return json(threads); +}; diff --git a/src/leapfrogai_ui/src/routes/api/threads/[thread_id]/+server.ts b/src/leapfrogai_ui/src/routes/api/threads/[thread_id]/+server.ts index 0a4a29f76..5c0c9f769 100644 --- a/src/leapfrogai_ui/src/routes/api/threads/[thread_id]/+server.ts +++ b/src/leapfrogai_ui/src/routes/api/threads/[thread_id]/+server.ts @@ -1,23 +1,6 @@ import type { RequestHandler } from './$types'; import { error, json } from '@sveltejs/kit'; -import { getOpenAiClient } from '$lib/server/constants'; -import type { LFThread } from '$lib/types/threads'; -import type { LFMessage } from '$lib/types/messages'; - -const getThreadWithMessages = async ( - thread_id: string, - access_token: string -): Promise => { - const openai = getOpenAiClient(access_token); - 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 }; -}; +import { getThreadWithMessages } from '../../helpers'; export const GET: RequestHandler = async ({ params, locals: { session } }) => { if (!session) { diff --git a/src/leapfrogai_ui/src/routes/api/threads/server.test.ts b/src/leapfrogai_ui/src/routes/api/threads/server.test.ts new file mode 100644 index 000000000..34c7dade9 --- /dev/null +++ b/src/leapfrogai_ui/src/routes/api/threads/server.test.ts @@ -0,0 +1,125 @@ +import { GET } from './+server'; +import { getLocalsMock } from '$lib/mocks/misc'; +import type { RequestEvent } from '@sveltejs/kit'; +import type { RouteParams } from './$types'; +import { + selectSingleReturnsMockError, + supabaseFromMockWrapper, + supabaseSelectSingleByIdMock +} from '$lib/mocks/supabase-mocks'; +import { getFakeThread } from '$testUtils/fakeData'; +import { mockOpenAI } from '../../../../vitest-setup'; +import * as apiHelpers from '../helpers'; + +const request = new Request('http://thisurlhasnoeffect', { + method: 'GET' +}); + +const thread1 = getFakeThread({ numMessages: 1 }); +const thread2 = getFakeThread({ numMessages: 2 }); +const fakeProfile = { thread_ids: [thread1.id, thread2.id] }; + +describe('/api/threads', () => { + it('returns a 401 when there is no session', async () => { + await expect( + GET({ + request, + locals: getLocalsMock({ nullSession: true }) + } as RequestEvent) + ).rejects.toMatchObject({ + status: 401 + }); + }); + it("returns a user's threads", async () => { + const thread1WithoutMessages = { ...thread1, messages: undefined }; + const thread2WithoutMessages = { ...thread2, messages: undefined }; + + mockOpenAI.setThreads([thread1WithoutMessages, thread2WithoutMessages]); + mockOpenAI.setMessages([...(thread1.messages || []), ...(thread2.messages || [])]); + + const res = await GET({ + request, + locals: getLocalsMock({ + supabase: supabaseFromMockWrapper({ + ...supabaseSelectSingleByIdMock(fakeProfile) + }) + }) + } as RequestEvent); + + expect(res.status).toEqual(200); + const resJson = await res.json(); + // Note - our fake threads already have messages attached, we are checking here that the + // API fetched the messages and added them to the threads since real threads don't have messages + expect(resJson[0].id).toEqual(thread1.id); + expect(resJson[0].messages).toEqual(thread1.messages); + expect(resJson[1].id).toEqual(thread2.id); + expect(resJson[1].messages).toEqual(thread2.messages); + }); + it('still returns threads that were successfully retrieved when there is an error getting a thread', async () => { + mockOpenAI.setThreads([thread2]); + mockOpenAI.setError('retrieveThread'); // fail the first thread fetching + const res = await GET({ + request, + locals: getLocalsMock({ + supabase: supabaseFromMockWrapper({ + ...supabaseSelectSingleByIdMock(fakeProfile) + }) + }) + } as RequestEvent); + + expect(res.status).toEqual(200); + const resJson = await res.json(); + expect(resJson[0].id).toEqual(thread2.id); + }); + it('still returns threads that were successfully retrieved when there is an error getting messages for a thread', async () => { + mockOpenAI.setThreads([thread1, thread2]); + mockOpenAI.setError('listMessages'); // fail the first thread's message fetching + const res = await GET({ + request, + locals: getLocalsMock({ + supabase: supabaseFromMockWrapper({ + ...supabaseSelectSingleByIdMock(fakeProfile) + }) + }) + } as RequestEvent); + + expect(res.status).toEqual(200); + const resJson = await res.json(); + expect(resJson[0].id).toEqual(thread2.id); + }); + it('returns an empty array if there is an unhandled error fetching threads', async () => { + vi.spyOn(apiHelpers, 'getThreadWithMessages').mockImplementationOnce(() => { + throw new Error('fake error'); + }); + const consoleSpy = vi.spyOn(console, 'error'); + + const res = await GET({ + request, + locals: getLocalsMock({ + supabase: supabaseFromMockWrapper({ + ...supabaseSelectSingleByIdMock(fakeProfile) + }) + }) + } as RequestEvent); + + expect(res.status).toEqual(200); + const resJson = await res.json(); + expect(resJson).toEqual([]); + // ensure we hit the correct catch block/error case with this test + expect(consoleSpy).toHaveBeenCalledWith('Error fetching threads: Error: fake error'); + }); + it("returns a 500 is an error getting the user's profile", async () => { + await expect( + GET({ + request, + locals: getLocalsMock({ + supabase: supabaseFromMockWrapper({ + ...selectSingleReturnsMockError() + }) + }) + } as RequestEvent) + ).rejects.toMatchObject({ + status: 500 + }); + }); +}); 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 f082615c5..a9c359274 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 @@ -3,7 +3,7 @@ import { LFTextArea, PoweredByDU } from '$components'; import { Hr, ToolbarButton } from 'flowbite-svelte'; import { onMount, tick } from 'svelte'; - import { threadsStore, toastStore } from '$stores'; + import { assistantsStore, threadsStore, toastStore } from '$stores'; import { type Message as VercelAIMessage, useAssistant, useChat } from '@ai-sdk/svelte'; import { page } from '$app/stores'; import Message from '$components/Message.svelte'; @@ -29,35 +29,28 @@ import ChatFileUploadForm from '$components/ChatFileUpload.svelte'; import FileChatActions from '$components/FileChatActions.svelte'; import LFCarousel from '$components/LFCarousel.svelte'; - - export let data; + import type { LFThread } from '$lib/types/threads'; /** LOCAL VARS **/ let lengthInvalid: boolean; // bound to child LFTextArea - let assistantsList: Array<{ id: string; text: string }>; let uploadingFiles = false; let attachedFiles: LFFile[] = []; // the actual files uploaded let attachedFileMetadata: FileMetadata[] = []; // metadata about the files uploaded, e.g. upload status, extracted text, etc... + let activeThread: LFThread | undefined = undefined; /** END LOCAL VARS **/ /** REACTIVE STATE **/ $: componentHasMounted = false; - $: $page.params.thread_id, threadsStore.setLastVisitedThreadId($page.params.thread_id); - $: $page.params.thread_id, - resetMessages({ - activeThread: data.thread, - setChatMessages, - setAssistantMessages - }); - - $: activeThreadMessages = - $threadsStore.threads.find((thread) => thread.id === $page.params.thread_id)?.messages || []; + $: activeThread = $threadsStore.threads.find( + (thread: LFThread) => thread.id === $page.params.thread_id + ); + $: $page.params.thread_id, handleThreadChange(); $: messageStreaming = $isLoading || $status === 'in_progress'; $: latestChatMessage = $chatMessages[$chatMessages.length - 1]; $: latestAssistantMessage = $assistantMessages[$assistantMessages.length - 1]; $: assistantMode = - $threadsStore.selectedAssistantId !== NO_SELECTED_ASSISTANT_ID && - $threadsStore.selectedAssistantId !== 'manage-assistants'; + $assistantsStore.selectedAssistantId !== NO_SELECTED_ASSISTANT_ID && + $assistantsStore.selectedAssistantId !== 'manage-assistants'; $: if (messageStreaming) threadsStore.setSendingBlocked(true); @@ -78,6 +71,26 @@ /** END REACTIVE STATE **/ + const handleThreadChange = () => { + if ($page.params.thread_id) { + if (activeThread) { + threadsStore.setLastVisitedThreadId(activeThread.id); + resetMessages({ + activeThread, + setChatMessages, + setAssistantMessages + }); + } + } else { + threadsStore.setLastVisitedThreadId(''); + resetMessages({ + activeThread, + setChatMessages, + setAssistantMessages + }); + } + }; + const resetFiles = () => { uploadingFiles = false; attachedFileMetadata = []; @@ -100,13 +113,13 @@ ); const message = await messageRes.json(); // store the assistant id on the user msg to know it's associated with an assistant - message.metadata.assistant_id = $threadsStore.selectedAssistantId; + message.metadata.assistant_id = $assistantsStore.selectedAssistantId; await threadsStore.addMessageToStore(message); } else if (latestAssistantMessage?.role !== 'user') { // Streamed assistant responses don't contain an assistant_id, so we add it here // and also add a createdAt date if not present if (!latestAssistantMessage.assistant_id) { - latestAssistantMessage.assistant_id = $threadsStore.selectedAssistantId; + latestAssistantMessage.assistant_id = $assistantsStore.selectedAssistantId; } if (!latestAssistantMessage.createdAt) @@ -144,10 +157,10 @@ // Handle completed AI Responses onFinish: async (message: VercelAIMessage) => { try { - if (data.thread?.id) { + if (activeThread?.id) { // Save with API to db const newMessage = await saveMessage({ - thread_id: data.thread.id, + thread_id: activeThread.id, content: getMessageText(message), role: 'assistant' }); @@ -183,7 +196,7 @@ append: assistantAppend } = useAssistant({ api: '/api/chat/assistants', - threadId: data.thread?.id, + threadId: activeThread?.id, onError: async (e) => { // ignore this error b/c it is expected on cancel if (e.message !== 'BodyStreamBuffer was aborted') { @@ -197,7 +210,7 @@ const sendAssistantMessage = async (e: SubmitEvent | KeyboardEvent) => { await threadsStore.setSendingBlocked(true); - if (data.thread?.id) { + if (activeThread?.id) { // assistant mode $assistantInput = $chatInput; $chatInput = ''; // clear chat input @@ -206,8 +219,8 @@ // submit to AI (/api/chat/assistants) data: { message: $chatInput, - assistantId: $threadsStore.selectedAssistantId, - threadId: data.thread.id + assistantId: $assistantsStore.selectedAssistantId, + threadId: activeThread.id } }); $assistantInput = ''; @@ -218,13 +231,13 @@ const sendChatMessage = async (e: SubmitEvent | KeyboardEvent) => { try { await threadsStore.setSendingBlocked(true); - if (data.thread?.id) { + if (activeThread?.id) { let extractedFilesTextString = JSON.stringify(attachedFileMetadata); if (attachedFileMetadata.length > 0) { // Save the text of the document as its own message before sending actual question const contextMsg = await saveMessage({ - thread_id: data.thread.id, + thread_id: activeThread.id, content: `${FILE_UPLOAD_PROMPT}: ${extractedFilesTextString}`, role: 'user', metadata: { @@ -237,7 +250,7 @@ // Save with API const newMessage = await saveMessage({ - thread_id: data.thread.id, + thread_id: activeThread.id, content: $chatInput, role: 'user', ...(attachedFileMetadata.length > 0 @@ -270,11 +283,11 @@ // setSendingBlocked (when called with the value 'false') automatically handles this delay const onSubmit = async (e: SubmitEvent | KeyboardEvent) => { e.preventDefault(); - if (($isLoading || $status === 'in_progress') && data.thread?.id) { + if (($isLoading || $status === 'in_progress') && activeThread?.id) { const isAssistantChat = $status === 'in_progress'; // message still sending await stopThenSave({ - activeThreadId: data.thread.id, + activeThreadId: activeThread.id, messages: isAssistantChat ? $assistantMessages : $chatMessages, status: $status, isLoading: $isLoading || false, @@ -285,7 +298,7 @@ return; } else { if (sendDisabled) return; - if (!data.thread?.id) { + if (!activeThread?.id) { // create new thread await threadsStore.newThread($chatInput); await tick(); // allow store to update @@ -305,19 +318,13 @@ onMount(async () => { componentHasMounted = true; - assistantsList = [...(data.assistants || [])].map((assistant) => ({ - id: assistant.id, - text: assistant.name || 'unknown' - })); - assistantsList.unshift({ id: NO_SELECTED_ASSISTANT_ID, text: 'Select assistant...' }); // add dropdown item for no assistant selected - assistantsList.unshift({ id: `manage-assistants`, text: 'Manage assistants' }); // add dropdown item for manage assistants button }); beforeNavigate(async () => { - if (($isLoading || $status === 'in_progress') && data.thread?.id) { + if (($isLoading || $status === 'in_progress') && activeThread?.id) { const isAssistantChat = $status === 'in_progress'; await stopThenSave({ - activeThreadId: data.thread.id, + activeThreadId: activeThread.id, messages: isAssistantChat ? $assistantMessages : $chatMessages, status: $status, isLoading: $isLoading || false, @@ -331,19 +338,21 @@
- {#each activeThreadMessages as message, index (message.id)} - {#if message.metadata?.hideMessage !== 'true'} - - {/if} - {/each} + {#if activeThread} + {#each activeThread.messages as message, index (message.id)} + {#if message.metadata?.hideMessage !== 'true'} + + {/if} + {/each} + {/if} {#if $threadsStore.streamingMessage} @@ -352,7 +361,7 @@

- +
{ - const promises = [fetch('/api/assistants'), fetch('/api/files')]; - - if (params.thread_id) promises.push(fetch(`/api/threads/${params.thread_id}`)); - - const promiseResponses = await Promise.all(promises); - - const assistants = await promiseResponses[0].json(); - const files = await promiseResponses[1].json(); - - let thread: LFThread | undefined = undefined; - if (params.thread_id) { - thread = await promiseResponses[2].json(); - } - - if (browser) { - if (thread) { - // update store with latest thread fetched by page data - threadsStore.updateThread(thread); - } - } - - return { thread, assistants, files }; -}; 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 0a3cefa37..21857b0e8 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 @@ -17,7 +17,6 @@ import { mockNewMessageError } from '$lib/mocks/chat-mocks'; import { getMessageText } from '$helpers/threads'; -import { load } from './+page'; import { mockOpenAI } from '../../../../../vitest-setup'; import { ERROR_GETTING_AI_RESPONSE_TOAST, ERROR_SAVING_MSG_TOAST } from '$constants/toastMessages'; @@ -27,7 +26,6 @@ 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'; type LayoutServerLoad = { threads: LFThread[]; @@ -60,17 +58,9 @@ describe('when there is an active thread selected', () => { mockOpenAI.setMessages(allMessages); mockOpenAI.setFiles(files); - // @ts-expect-error: full mocking of load function params not necessary and is overcomplicated - data = await load({ - fetch: global.fetch, - depends: vi.fn(), - params: { thread_id: fakeThreads[0].id } - }); - threadsStore.set({ threads: fakeThreads, lastVisitedThreadId: fakeThreads[0].id, - selectedAssistantId: NO_SELECTED_ASSISTANT_ID, sendingBlocked: false, streamingMessage: null }); diff --git a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage_no_thread.test.ts b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage_no_thread.test.ts index 71242a2b2..6ec9995cb 100644 --- a/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage_no_thread.test.ts +++ b/src/leapfrogai_ui/src/routes/chat/(dashboard)/[[thread_id]]/chatpage_no_thread.test.ts @@ -8,7 +8,7 @@ import { mockNewMessage, mockNewThreadError } from '$lib/mocks/chat-mocks'; -import { load } from './+page'; + import { mockOpenAI } from '../../../../../vitest-setup'; import ChatPageWithToast from './ChatPageWithToast.test.svelte'; import type { LFThread } from '$lib/types/threads'; @@ -34,13 +34,6 @@ describe('when there is NO active thread selected', () => { mockOpenAI.setThreads(fakeThreads); mockOpenAI.setMessages(allMessages); mockOpenAI.setFiles(files); - - // @ts-expect-error: full mocking of load function params not necessary and is overcomplicated - data = await load({ - params: {}, - fetch: global.fetch, - depends: vi.fn() - }); }); afterAll(() => { diff --git a/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts b/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts index 1cc33e4e8..ae0ec066c 100644 --- a/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts +++ b/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.server.ts @@ -30,7 +30,6 @@ export const load: PageServerLoad = async ({ depends, locals: { session } }) => if (!res.ok) { return error(500, { message: 'Error fetching API keys' }); } - keys = (await res.json()) as APIKeyRow[]; // convert from seconds to milliseconds keys.forEach((key) => { diff --git a/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.svelte b/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.svelte index e854a8e6f..413cf8e23 100644 --- a/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.svelte +++ b/src/leapfrogai_ui/src/routes/chat/(settings)/api-keys/+page.svelte @@ -137,7 +137,11 @@
{#if editMode} -
+
{#if deleting} {#if deleting}