Skip to content

Commit

Permalink
feat(ui): 531 file management (#558)
Browse files Browse the repository at this point in the history
* Adds a file management page accessible through the top right settings icon.

* Allows multiple file upload and delete.

* Show upload success and error icon animations that disappear after 1.5 seconds. Also has toasts.

* Allows search by filename and created_at date.

* Allows sort by filename or created_at fields.

* Includes a refactor to change the data fetching and invalidation pattern for assistants and files.
---------

Co-authored-by: John Alling <john.alling@defenseunicorns.com>
Co-authored-by: Jon Perry <yrrepnoj@gmail.com>
  • Loading branch information
3 people authored May 31, 2024
1 parent 5371956 commit 884761b
Show file tree
Hide file tree
Showing 70 changed files with 1,319 additions and 294 deletions.
15 changes: 11 additions & 4 deletions src/leapfrogai_ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/leapfrogai_ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"@typescript-eslint/parser": "^7.7.1",
"carbon-components-svelte": "^0.85.0",
"carbon-icons-svelte": "^12.6.0",
"carbon-preprocess-svelte": "^0.11.0",
"carbon-preprocess-svelte": "^0.11.3",
"dotenv": "^16.4.5",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
Expand Down
9 changes: 4 additions & 5 deletions src/leapfrogai_ui/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@ dotenv.config();
const config: PlaywrightTestConfig = {
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{ name: 'clear_db', testMatch: /.*\clear_db\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// Use prepared auth state.
storageState: 'playwright/.auth/user.json'
},
dependencies: ['clear_db', 'setup']
dependencies: ['setup']
},
{
name: 'firefox',
Expand All @@ -24,12 +23,12 @@ const config: PlaywrightTestConfig = {
// Use prepared auth state.
storageState: 'playwright/.auth/user.json'
},
dependencies: ['clear_db', 'setup']
dependencies: ['setup']
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'], storageState: 'playwright/.auth/user.json' },
dependencies: ['clear_db', 'setup']
dependencies: ['setup']
},
{
name: 'Edge',
Expand All @@ -38,7 +37,7 @@ const config: PlaywrightTestConfig = {
channel: 'msedge',
storageState: 'playwright/.auth/user.json'
},
dependencies: ['clear_db', 'setup']
dependencies: ['setup']
}
],
webServer: {
Expand Down
7 changes: 5 additions & 2 deletions src/leapfrogai_ui/src/app.d.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
import { Session, SupabaseClient } from '@supabase/supabase-js';
import { Session, SupabaseClient, User } from '@supabase/supabase-js';
import type { LFAssistant } from '$lib/types/assistants';
import type { Profile } from '$lib/types/profile';
import type { LFThread } from '$lib/types/threads';
import type { FileObject } from 'openai/resources/files';

declare global {
namespace App {
// interface Error {}
interface Locals {
supabase: SupabaseClient;
getSession(): Promise<Session | null>;
safeGetSession(): Promise<{ session: Session | null; user: User | null }>;
}
interface PageData {
title?: string | null;
session?: Session | null;
user?: User | null;
profile?: Profile;
threads?: LFThread[];
assistants?: LFAssistant[];
assistant?: LFAssistant;
files?: FileObject[];
}
// interface PageState {}
// interface Platform {}
Expand Down
23 changes: 19 additions & 4 deletions src/leapfrogai_ui/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,33 @@ export const handle: Handle = async ({ event, resolve }) => {
);

/**
* A convenience helper so we can just call await getSession() instead const { data: { session } } = await supabase.auth.getSession()
* Unlike `supabase.auth.getSession()`, which returns the session _without_
* validating the JWT, this function also calls `getUser()` to validate the
* JWT before returning the session.
*/
event.locals.getSession = async () => {
event.locals.safeGetSession = async () => {
const {
data: { session }
} = await event.locals.supabase.auth.getSession();
return session;
if (!session) {
return { session: null, user: null };
}

const {
data: { user },
error
} = await event.locals.supabase.auth.getUser();
if (error) {
// JWT validation has failed
return { session: null, user: null };
}

return { session, user };
};

return resolve(event, {
filterSerializedResponseHeaders(name) {
return name === 'content-range';
return name === 'content-range' || name === 'x-supabase-api-version';
}
});
};
Expand Down
8 changes: 5 additions & 3 deletions src/leapfrogai_ui/src/lib/components/AssistantForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import { superForm } from 'sveltekit-superforms';
import { Add } from 'carbon-icons-svelte';
import { page } from '$app/stores';
import { beforeNavigate, goto } from '$app/navigation';
import { beforeNavigate, goto, invalidate } from '$app/navigation';
import { Button, Modal, Slider, TextArea, TextInput } from 'carbon-components-svelte';
import AssistantAvatar from '$components/AssistantAvatar.svelte';
import { yup } from 'sveltekit-superforms/adapters';
import { toastStore } from '$stores';
import InputTooltip from '$components/InputTooltip.svelte';
import { editAssistantInputSchema, supabaseAssistantInputSchema } from '$lib/schemas/assistants';
import { editAssistantInputSchema, assistantInputSchema } from '$lib/schemas/assistants';
import type { NavigationTarget } from '@sveltejs/kit';
import { onMount } from 'svelte';
Expand All @@ -23,8 +23,10 @@
let bypassCancelWarning = false;
const { form, errors, enhance, submitting, isTainted } = superForm(data.form, {
validators: yup(isEditMode ? editAssistantInputSchema : supabaseAssistantInputSchema),
invalidateAll: false,
validators: yup(isEditMode ? editAssistantInputSchema : assistantInputSchema),
onResult({ result }) {
invalidate('/api/assistants');
if (result.type === 'redirect') {
toastStore.addToast({
kind: 'success',
Expand Down
21 changes: 14 additions & 7 deletions src/leapfrogai_ui/src/lib/components/AssistantTile.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { goto, invalidateAll } from '$app/navigation';
import { goto, invalidate } from '$app/navigation';
import { fade } from 'svelte/transition';
import DynamicPictogram from '$components/DynamicPictogram.svelte';
import { Modal, OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
Expand All @@ -23,7 +23,7 @@
deleteModalOpen = false;
if (res.ok) {
await invalidateAll();
await invalidate('/api/assistants');
toastStore.addToast({
kind: 'info',
title: 'Assistant Deleted.',
Expand Down Expand Up @@ -72,11 +72,13 @@
{/if}
<!--With fixed width and font sizes, there isn't a simple solution for multi line text ellipses, so doing it manually at specific character length instead-->
<p class="name">
{assistant.name.length > 20 ? `${assistant.name.slice(0, 20)}...` : assistant.name}
{assistant.name && assistant.name.length > 20
? `${assistant.name.slice(0, 20)}...`
: assistant.name}
</p>
<p class="description">
{assistant.description && assistant.description.length > 62
? `${assistant.description?.slice(0, 62)}...`
{assistant.description && assistant.description.length > 75
? `${assistant.description?.slice(0, 75)}...`
: assistant.description}
</p>

Expand Down Expand Up @@ -113,10 +115,15 @@
.name {
@include type.type-style('heading-03');
height: 1.75rem;
}
.description {
@include type.type-style('body-01');
width: 256px;
height: 2.5rem;
word-wrap: break-word;
overflow: hidden;
}
.overflow-menu-container {
Expand All @@ -137,8 +144,8 @@
display: flex;
justify-content: center;
align-items: center;
width: 3rem;
height: 3rem;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
.mini-avatar-image {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
export let iconName = 'default';
export let width = '40px';
export let height = '40px';
export let height = '2.5rem';
let Pictogram = iconMap.default;
Expand Down
17 changes: 13 additions & 4 deletions src/leapfrogai_ui/src/lib/components/LFHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,18 @@
isOpen={activeHeaderAction.settings}
on:open={() => setActiveHeaderAction('settings')}
>
<div class="link-container">
<div class="links-container">
<a
href="/chat/assistants-management"
class="header-link"
on:click={() => setActiveHeaderAction('')}>Assistants Management</a
>

<a
href="/chat/file-management"
class="header-link"
on:click={() => setActiveHeaderAction('')}>File Management</a
>
</div>
</HeaderAction>
<HeaderAction
Expand All @@ -64,7 +70,7 @@
isOpen={activeHeaderAction.user}
on:open={() => setActiveHeaderAction('user')}
>
<div class="link-container">
<div class="links-container">
<form bind:this={signOutForm} method="post" action="/auth?/signout">
<button
class="header-link"
Expand All @@ -84,8 +90,11 @@
height: 36px;
}
.link-container {
padding: layout.$spacing-05;
.links-container {
display: flex;
padding: 1rem;
flex-direction: column;
gap: 0.88rem;
}
.header-link {
Expand Down
2 changes: 2 additions & 0 deletions src/leapfrogai_ui/src/lib/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { LFAssistant } from '$lib/types/assistants';
export const MAX_LABEL_SIZE = 100;
export const DEFAULT_ASSISTANT_TEMP = 0.2;
export const MAX_AVATAR_SIZE = 5000000;
export const MAX_FILE_SIZE = 512000000;

// PER OPENAI SPEC
export const ASSISTANTS_NAME_MAX_LENGTH = 256;
Expand Down Expand Up @@ -33,3 +34,4 @@ export const assistantDefaults: Omit<LFAssistant, 'id' | 'created_at'> = {

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`;
30 changes: 30 additions & 0 deletions src/leapfrogai_ui/src/lib/helpers/dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,33 @@ export const organizeThreadsByDate = (
};

export const getUnixSeconds = (date: Date) => date.getTime() / 1000;

export const formatDate = (date: Date) => {
// Create an array with month names
const months = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
];

// Get the day of the month
const day = date.getDate();

// Get the month name from the array
const month = months[date.getMonth()];

// Get the full year
const year = date.getFullYear();

// Return the formatted date string
return `${day} ${month} ${year}`;
};
20 changes: 20 additions & 0 deletions src/leapfrogai_ui/src/lib/mocks/file-mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { server } from '../../../vitest-setup';
import { delay, http, HttpResponse } from 'msw';
import type { FileObject } from 'openai/resources/files';

export const mockDeleteFile = () => {
server.use(http.delete('/api/files/delete', () => new HttpResponse(null, { status: 204 })));
};

export const mockDeleteFileWithDelay = () => {
server.use(
http.delete('/api/files/delete', async () => {
await delay(500);
return new HttpResponse(null, { status: 204 });
})
);
};

export const mockGetFiles = (files: FileObject[]) => {
server.use(http.get('/api/files', () => HttpResponse.json(files)));
};
Loading

0 comments on commit 884761b

Please sign in to comment.