-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
532 upload file with assistant (#608)
* Feature - upload new files while creating or editing an assistant * Also adds a "filesStore" which will make completing #559 much easier. We will just refactor the file management page to follow the pattern used in this feature. * Due to the custom behavior of the MultiSelect necessary for this story, we copied the Carbon Components Svelte MultiSelect code into our own custom version of it called LFMultiSelect. --------- Co-authored-by: John Alling <[email protected]> Co-authored-by: Jon Perry <[email protected]>
- Loading branch information
1 parent
3b91e7f
commit 74911a8
Showing
17 changed files
with
1,122 additions
and
79 deletions.
There are no files selected for viewing
58 changes: 42 additions & 16 deletions
58
src/leapfrogai_ui/src/lib/components/AssistantFileSelect.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
58 changes: 58 additions & 0 deletions
58
src/leapfrogai_ui/src/lib/components/AssistantFileSelect.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { filesStore } from '$stores'; | ||
import { render, screen } from '@testing-library/svelte'; | ||
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 { getUnixSeconds } from '$helpers/dates'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
const filesForm = await superValidate({}, yup(filesSchema), { errors: false }); | ||
|
||
describe('AssistantFileSelect', () => { | ||
const mockFiles: FileRow[] = [ | ||
{ 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()) } | ||
]; | ||
|
||
beforeEach(() => { | ||
filesStore.set({ | ||
files: mockFiles, | ||
selectedAssistantFileIds: ['1', '2'], | ||
uploading: false | ||
}); | ||
}); | ||
|
||
it('renders each selected file', async () => { | ||
render(AssistantFileSelect, { | ||
filesForm | ||
}); | ||
|
||
expect(screen.getByTestId(`${mockFiles[0].filename}-${mockFiles[0].status}-uploader-item`)); | ||
expect(screen.getByTestId(`${mockFiles[1].filename}-${mockFiles[1].status}-uploader-item`)); | ||
expect( | ||
screen.queryByTestId(`${mockFiles[2].filename}-${mockFiles[2].status}-uploader-item`) | ||
).not.toBeInTheDocument(); | ||
}); | ||
|
||
it('can select files', async () => { | ||
filesStore.set({ | ||
files: mockFiles, | ||
selectedAssistantFileIds: [], | ||
uploading: false | ||
}); | ||
|
||
render(AssistantFileSelect, { | ||
filesForm | ||
}); | ||
|
||
expect( | ||
screen.queryByTestId(`${mockFiles[0].filename}-${mockFiles[0].status}-uploader-item`) | ||
).not.toBeInTheDocument(); | ||
|
||
await userEvent.click(screen.getByText(mockFiles[0].filename)); | ||
screen.getByTestId(`${mockFiles[0].filename}-${mockFiles[0].status}-uploader-item`); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
src/leapfrogai_ui/src/lib/components/FileUploadMenuItem.svelte
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
<script lang="ts"> | ||
import { Upload } from 'carbon-icons-svelte'; | ||
import { ListBoxMenuItem } from 'carbon-components-svelte'; | ||
import { createEventDispatcher } from 'svelte'; | ||
import type { FilesForm } from '$lib/types/files'; | ||
import { filesStore, toastStore } from '$stores'; | ||
import { superForm } from 'sveltekit-superforms'; | ||
import { yup } from 'sveltekit-superforms/adapters'; | ||
import { filesSchema } from '$schemas/files'; | ||
import type { ActionResult } from '@sveltejs/kit'; | ||
export let multiple = false; | ||
export let files: File[] = []; | ||
export let labelText = 'Add file'; | ||
export let ref: HTMLInputElement | null = null; | ||
export let id = 'ccs-' + Math.random().toString(36); | ||
export let disabled = false; | ||
export let tabindex = '0'; | ||
export let role = 'button'; | ||
export let accept: ReadonlyArray<string> = []; | ||
export let disableLabelChanges = false; | ||
export let filesForm: FilesForm; // for the form | ||
export let open: boolean; //Parent LFMultiSelect open reactive variable | ||
let initialLabelText = labelText; | ||
const dispatch = createEventDispatcher(); | ||
$: if (ref && files.length === 0) { | ||
labelText = initialLabelText; | ||
ref.value = ''; | ||
} | ||
$: if (files.length > 0) { | ||
handleUpload(); | ||
} | ||
const handleUpload = () => { | ||
open = false; // close parent multi select | ||
filesStore.setUploading(true); | ||
filesStore.addUploadingFiles(files); | ||
submit(); | ||
}; | ||
// The parent ListBox that uses this component has on:click|preventDefault for the other | ||
// items in the list box to prevent it from closing. We get around that with this function | ||
// to ensure you can still open a file upload dialog. | ||
const handleClick = (e: MouseEvent) => { | ||
e.stopPropagation(); | ||
if (ref) { | ||
ref.click(); | ||
} | ||
}; | ||
const handleResult = async (result: ActionResult) => { | ||
if (result.type === 'success') { | ||
const idsToSelect: string[] = []; | ||
const uploadedFiles = result.data?.uploadedFiles; | ||
filesStore.updateWithUploadResults(result.data?.uploadedFiles); | ||
for (const uploadedFile of uploadedFiles) { | ||
idsToSelect.push(uploadedFile.id); | ||
if (uploadedFile.status === 'error') { | ||
toastStore.addToast({ | ||
kind: 'error', | ||
title: 'Upload Failed', | ||
subtitle: `${uploadedFile.filename} upload failed.` | ||
}); | ||
} else { | ||
toastStore.addToast({ | ||
kind: 'success', | ||
title: 'Uploaded Successfully', | ||
subtitle: `${uploadedFile.filename} uploaded successfully.` | ||
}); | ||
} | ||
} | ||
filesStore.addSelectedAssistantFileIds(idsToSelect); | ||
} | ||
filesStore.setUploading(false); | ||
}; | ||
const { enhance, submit } = superForm(filesForm, { | ||
validators: yup(filesSchema), | ||
invalidateAll: false, | ||
onError() { | ||
// Backend failure, not just a single file failure | ||
filesStore.setAllUploadingToError(); | ||
toastStore.addToast({ | ||
kind: 'error', | ||
title: 'Upload Failed', | ||
subtitle: `Please try again or contact support` | ||
}); | ||
}, | ||
onResult: async ({ result }) => handleResult(result) | ||
}); | ||
</script> | ||
|
||
<form | ||
class="file-upload-container" | ||
method="POST" | ||
enctype="multipart/form-data" | ||
use:enhance | ||
action="/chat/file-management" | ||
> | ||
<ListBoxMenuItem on:click={handleClick}> | ||
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> | ||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> | ||
<label | ||
for={id} | ||
aria-disabled={disabled} | ||
tabindex={disabled ? '-1' : tabindex} | ||
on:keydown | ||
on:keydown={({ key }) => { | ||
if (key === ' ' || key === 'Enter') { | ||
ref?.click(); | ||
} | ||
}} | ||
> | ||
<span {role}> | ||
<slot name="labelText"> | ||
<div class="upload-item"> | ||
<div class="upload-icon"> | ||
<Upload /> | ||
</div> | ||
<span class="bx--checkbox-label-text">{labelText}</span> | ||
</div> | ||
</slot> | ||
</span> | ||
</label> | ||
<input | ||
bind:this={ref} | ||
{disabled} | ||
type="file" | ||
tabindex="-1" | ||
{accept} | ||
{multiple} | ||
name="files" | ||
class:bx--visually-hidden={true} | ||
{...$$restProps} | ||
on:change|stopPropagation={({ target }) => { | ||
if (target) { | ||
files = [...target.files]; | ||
if (files && !disableLabelChanges) { | ||
labelText = files.length > 1 ? `${files.length} files` : files[0].name; | ||
} | ||
dispatch('change', files); | ||
} | ||
}} | ||
on:click | ||
on:click={({ target }) => { | ||
if (target) { | ||
target.value = null; | ||
} | ||
}} | ||
/> | ||
</ListBoxMenuItem> | ||
</form> | ||
|
||
<style lang="scss"> | ||
.file-upload-container { | ||
outline: 1px solid themes.$border-subtle-03; | ||
} | ||
.upload-item { | ||
display: flex; | ||
align-items: center; | ||
cursor: pointer; | ||
} | ||
.upload-icon { | ||
display: flex; | ||
width: 1.4rem; | ||
justify-content: center; | ||
margin-right: 0.3rem; | ||
} | ||
</style> |
Oops, something went wrong.