Skip to content

Commit

Permalink
667 upload file types (#760)
Browse files Browse the repository at this point in the history
* Allows upload of additional file types
  • Loading branch information
andrewrisse authored Jul 12, 2024
1 parent 4d8ff03 commit cc3af1f
Show file tree
Hide file tree
Showing 10 changed files with 466 additions and 96 deletions.
276 changes: 270 additions & 6 deletions src/leapfrogai_ui/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/leapfrogai_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"ai": "^3.1.11",
"carbon-pictograms-svelte": "^12.10.0",
"concurrently": "^8.2.2",
"docx": "^8.5.0",
"dompurify": "^3.1.6",
"fuse.js": "^7.0.0",
"highlight.js": "^11.10.0",
Expand All @@ -87,8 +88,10 @@
"msw": "^2.2.14",
"openai": "^4.47.1",
"playwright": "^1.42.1",
"pptxgenjs": "^3.12.0",
"sveltekit-superforms": "^2.13.1",
"uuid": "^9.0.1",
"xlsx": "^0.18.5",
"yup": "^1.4.0"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import LFMultiSelect from '$components/LFMultiSelect.svelte';
import { filesStore } from '$stores';
import type { FilesForm } from '$lib/types/files';
import { ACCEPTED_FILE_TYPES } from '$constants';
export let filesForm: FilesForm;
Expand All @@ -21,7 +22,7 @@
label="Choose data sources"
items={$filesStore.files.map((file) => ({ id: file.id, text: file.filename }))}
direction="top"
accept={['.pdf', '.txt', '.text']}
accept={ACCEPTED_FILE_TYPES}
bind:selectedIds={$filesStore.selectedAssistantFileIds}
{filesForm}
/>
Expand Down
3 changes: 2 additions & 1 deletion src/leapfrogai_ui/src/lib/components/AttachFile.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script lang="ts">
import { Attachment } from 'carbon-icons-svelte';
import { Button } from 'carbon-components-svelte';
import { ACCEPTED_FILE_TYPES } from '$constants';
export let disabled = false;
export let accept = ['.pdf', '.txt', '.text'];
export let accept = ACCEPTED_FILE_TYPES;
export let multiple = false;
export let files: File[] = [];
export let handleAttach: () => void;
Expand Down
24 changes: 22 additions & 2 deletions src/leapfrogai_ui/src/lib/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,29 @@ export const assistantDefaults: Omit<LFAssistant, 'id' | 'created_at'> = {
temperature: 0.2,
response_format: 'auto'
};

export const ACCEPTED_FILE_TYPES = [
'.pdf',
'.txt',
'.text',
'.xls',
'.xlsx',
'.ppt',
'.pptx',
'.doc',
'.docx'
];
export const ACCEPTED_MIME_TYPES = [
'application/pdf', // .pdf
'text/plain', // .txt, .text
'application/vnd.ms-excel', // .xls
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/vnd.ms-powerpoint', // .ppt
'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .pptx
'application/msword', // .doc
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' //.docx,
];
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 INVALID_FILE_TYPE_ERROR_TEXT = `Invalid file type, accepted types are: ${ACCEPTED_FILE_TYPES.join(', ')}`;
export const NO_SELECTED_ASSISTANT_ID = 'noSelectedAssistantId';
13 changes: 9 additions & 4 deletions src/leapfrogai_ui/src/lib/schemas/files.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { array, mixed, object, string, ValidationError } from 'yup';
import { FILE_SIZE_ERROR_TEXT, MAX_FILE_SIZE } from '$constants';
import {
ACCEPTED_MIME_TYPES,
FILE_SIZE_ERROR_TEXT,
INVALID_FILE_TYPE_ERROR_TEXT,
MAX_FILE_SIZE
} from '$constants';

export const filesSchema = object({
files: array().of(
Expand All @@ -15,12 +20,12 @@ export const filesSchema = object({
}
return true;
})
.test('type', 'Invalid file type, accepted types are: pdf and txt', (value) => {
.test('type', INVALID_FILE_TYPE_ERROR_TEXT, (value) => {
if (value == null) {
return true;
}
if (value.type !== 'application/pdf' && value.type !== 'text/plain') {
return new ValidationError('Invalid file type, accepted types are: pdf and txt');
if (!ACCEPTED_MIME_TYPES.includes(value.type)) {
return new ValidationError(INVALID_FILE_TYPE_ERROR_TEXT);
}
return true;
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { fail } from '@sveltejs/kit';
import { superValidate, withFiles } from 'sveltekit-superforms';
import { superValidate, withFiles, fail } from 'sveltekit-superforms';
import { yup } from 'sveltekit-superforms/adapters';
import type { FileObject } from 'openai/resources/files';
import { filesSchema } from '$schemas/files';
Expand All @@ -17,6 +16,13 @@ export const actions = {
const form = await superValidate(request, yup(filesSchema));

if (!form.valid) {
console.log(
'Files form action: Invalid form submission.',
'id:',
form.id,
'errors:',
form.errors
);
return fail(400, { form });
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { afterNavigate, invalidate } from '$app/navigation';
import type { Assistant } from 'openai/resources/beta/assistants';
import ConfirmAssistantDeleteModal from '$components/modals/ConfirmAssistantDeleteModal.svelte';
import { ACCEPTED_FILE_TYPES } from '$constants';
export let data;
Expand All @@ -33,15 +34,24 @@
$: affectedAssistants = [];
$: if ($filesStore.selectedFileManagementFileIds.length === 0) active = false;
const { enhance, submit, submitting } = superForm(data.form, {
// Form error in form action (e.g. validation failure)
$: $errors._errors && $errors._errors.length > 0 && handleFormError();
const handleFormError = () => {
filesStore.setAllUploadingToError();
toastStore.addToast({
kind: 'error',
title: 'Import Failed',
subtitle: `${$errors._errors?.join(', ') || 'Please try again or contact support'}`
});
};
const { enhance, submit, submitting, errors } = superForm(data.form, {
validators: yup(filesSchema),
invalidateAll: false,
onError() {
toastStore.addToast({
kind: 'error',
title: 'Import Failed',
subtitle: `Please try again or contact support`
});
// Non-handled error in form action
handleFormError();
},
onResult: async ({ result }) => {
if (result.type === 'success') {
Expand Down Expand Up @@ -181,7 +191,7 @@
disableLabelChanges
disabled={$submitting}
labelText="Upload"
accept={['.pdf', '.txt', '.text']}
accept={ACCEPTED_FILE_TYPES}
/>
</ToolbarContent>
</Toolbar>
Expand Down
109 changes: 39 additions & 70 deletions src/leapfrogai_ui/tests/file-management.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { expect, test } from './fixtures';
import { faker } from '@faker-js/faker';
import { getTableRow, getSimpleMathQuestion, loadChatPage } from './helpers/helpers';
import {
confirmDeletion,
createExcelFile,
createPDF,
createPowerpointFile,
createTextFile,
createWordFile,
deleteFileByName,
deleteFixtureFile,
deleteTestFilesWithApi,
initiateDeletion,
loadFileManagementPage,
testFileUpload,
uploadFile
} from './helpers/fileHelpers';
import { sendMessage } from './helpers/threadHelpers';
Expand Down Expand Up @@ -44,73 +47,44 @@ test('it can navigate to the file management page', async ({ page }) => {
});

test('it can upload a pdf file', async ({ page, openAIClient }) => {
const filename = `${faker.word.noun()}-test.pdf`;
await createPDF(filename);
await loadFileManagementPage(page);
await uploadFile(page, filename);

const row = await getTableRow(page, filename);
expect(row).not.toBeNull();

const uploadingFileIcon = row!.getByTestId('uploading-file-icon');
const fileUploadedIcon = row!.getByTestId('file-uploaded-icon');

// test loading icon shows then disappears
await expect(uploadingFileIcon).toBeVisible();
// Ensure an additional checkbox is not added during upload (it should not have one on that row. row is in nonSelectableRowIds)
const rowCheckboxesBefore = await row!.getByRole('checkbox').all();
expect(rowCheckboxesBefore.length).toEqual(0);
await expect(fileUploadedIcon).toBeVisible();
await expect(uploadingFileIcon).not.toBeVisible();

// Checkbox should now be present
const rowCheckboxesAfter = await row!.getByRole('checkbox').all();
expect(rowCheckboxesAfter.length).toEqual(1);

// test toast
await expect(page.getByText(`${filename} imported successfully`)).toBeVisible();

// test complete icon disappears
await expect(fileUploadedIcon).not.toBeVisible();

// cleanup
deleteFixtureFile(filename);
await deleteFileByName(filename, openAIClient);
const filename = await createPDF();
await testFileUpload(filename, page, openAIClient);
});

test('it can upload a txt file', async ({ page, openAIClient }) => {
const filename = `${faker.word.noun()}-test.txt`;
createTextFile(filename);
await loadFileManagementPage(page);
await uploadFile(page, filename);

const row = await getTableRow(page, filename);
expect(row).not.toBeNull();
test('it can upload a .txt file', async ({ page, openAIClient }) => {
const filename = createTextFile();
await testFileUpload(filename, page, openAIClient);
});

const uploadingFileIcon = row!.getByTestId('uploading-file-icon');
const fileUploadedIcon = row!.getByTestId('file-uploaded-icon');
test('it can upload a .text file', async ({ page, openAIClient }) => {
const filename = createTextFile({ extension: '.text' });
await testFileUpload(filename, page, openAIClient);
});

// test loading icon shows then disappears
await expect(uploadingFileIcon).toBeVisible();
// Ensure an additional checkbox is not added during upload (it should not have one on that row. row is in nonSelectableRowIds)
const rowCheckboxesBefore = await row!.getByRole('checkbox').all();
expect(rowCheckboxesBefore.length).toEqual(0);
await expect(fileUploadedIcon).toBeVisible();
await expect(uploadingFileIcon).not.toBeVisible();
test('it can upload a .docx word file', async ({ page, openAIClient }) => {
const filename = createWordFile();
await testFileUpload(filename, page, openAIClient);
});

// Checkbox should now be present
const rowCheckboxesAfter = await row!.getByRole('checkbox').all();
expect(rowCheckboxesAfter.length).toEqual(1);
test('it can upload a .doc word file', async ({ page, openAIClient }) => {
const filename = createWordFile({ extension: '.doc' });
await testFileUpload(filename, page, openAIClient);
});

// test toast
await expect(page.getByText(`${filename} imported successfully`)).toBeVisible();
test('it can upload a .xlsx excel file', async ({ page, openAIClient }) => {
const filename = createExcelFile();
await testFileUpload(filename, page, openAIClient);
});

// test complete icon disappears
await expect(fileUploadedIcon).not.toBeVisible();
test('it can upload a .xls excel file', async ({ page, openAIClient }) => {
const filename = createExcelFile({ extension: '.xls' });
await testFileUpload(filename, page, openAIClient);
});

// cleanup
deleteFixtureFile(filename);
await deleteFileByName(filename, openAIClient);
// pptxgenjs library not capable of creating .ppt files, so only testing .pptx
test('it can upload a .pptx powerpoint file', async ({ page, openAIClient }) => {
const filename = await createPowerpointFile();
await testFileUpload(filename, page, openAIClient);
});

test('confirms any affected assistants then deletes multiple files', async ({
Expand All @@ -119,10 +93,8 @@ test('confirms any affected assistants then deletes multiple files', async ({
}) => {
await loadFileManagementPage(page);

const filename1 = `${faker.word.noun()}-test.pdf`;
const filename2 = `${faker.word.noun()}-test.pdf`;
await createPDF(filename1);
await createPDF(filename2);
const filename1 = await createPDF();
const filename2 = await createPDF();

await uploadFile(page, filename1);
await expect(page.getByText(`${filename1} imported successfully`)).toBeVisible();
Expand Down Expand Up @@ -153,8 +125,7 @@ test('confirms any affected assistants then deletes multiple files', async ({
test('it cancels the delete confirmation modal', async ({ page, openAIClient }) => {
await loadFileManagementPage(page);

const filename = `${faker.word.noun()}-test.pdf`;
await createPDF(filename);
const filename = await createPDF();

await uploadFile(page, filename);
await expect(page.getByText(`${filename} imported successfully`)).toBeVisible();
Expand All @@ -179,8 +150,7 @@ test('shows an error toast when there is an error deleting a file', async ({
page,
openAIClient
}) => {
const filename = `${faker.word.noun()}-test.pdf`;
await createPDF(filename);
const filename = await createPDF();

let hasBeenCalled = false;
await page.route('*/**/api/files/delete', async (route) => {
Expand Down Expand Up @@ -231,8 +201,7 @@ test('it shows toast when there is an error submitting the form', async ({

await loadFileManagementPage(page);

const filename = `${faker.word.noun()}-test.pdf`;
await createPDF(filename);
const filename = await createPDF();

await uploadFile(page, filename);

Expand Down
Loading

0 comments on commit cc3af1f

Please sign in to comment.