Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assistant MVP #140

Closed
wants to merge 93 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
b19a62a
Add assistant mode
williamtriinh Jul 11, 2023
b5ed966
Change icon for assistant mode
williamtriinh Jul 11, 2023
06c7478
Save uploaded files locally
williamtriinh Jul 11, 2023
2393fd6
Call read file and print its content
williamtriinh Jul 12, 2023
9b4ca89
Stub the writeToFile function
williamtriinh Jul 12, 2023
325be8d
Add getTimeStamp function
williamtriinh Jul 12, 2023
1bf260b
Fix build error
williamtriinh Jul 12, 2023
62e4e6c
Have function calling on client side
williamtriinh Jul 14, 2023
6cfbb09
Hide messages pertaining function calling
williamtriinh Jul 14, 2023
20c96fe
Read and write to local storage using function calling
williamtriinh Jul 14, 2023
ffff9ec
Add function calling error handling
williamtriinh Jul 14, 2023
d7d4f22
Add the ability to list files
williamtriinh Jul 14, 2023
0843e28
Refactor AttachFilesButton
williamtriinh Jul 14, 2023
d1a1c81
Add file deletion function calling
williamtriinh Jul 14, 2023
002b4f4
Remove console logs
williamtriinh Jul 14, 2023
53f5efb
Fix function calling descriptions
williamtriinh Jul 14, 2023
d232b67
Merge branch 'chat-everywhere' into will/20230706-read-write-files
williamtriinh Jul 20, 2023
6ea387b
Update OpenAIStream arguments
williamtriinh Jul 20, 2023
892fad0
Update assistant to use azure model
williamtriinh Jul 20, 2023
1e4be2f
Create files modal
williamtriinh Jul 21, 2023
055fd8c
Add deleting and renaming files
williamtriinh Jul 22, 2023
f240f76
Fix renaming edge cases and update rename/delete handlers
williamtriinh Jul 22, 2023
e1bdcf6
Add type to attachments
williamtriinh Jul 22, 2023
eff0491
Add downloading file
williamtriinh Jul 22, 2023
62629c0
Display file size
williamtriinh Jul 22, 2023
076f17a
Add drag and drop for files; refactor AttachFilesButton
williamtriinh Jul 22, 2023
4d4a299
Make AttachmentItem responsive
williamtriinh Jul 23, 2023
ab7e0db
Make files model mobile friendly
williamtriinh Jul 23, 2023
3c6994a
Remove onclick from row
williamtriinh Jul 23, 2023
4fc784e
Add updated at column
williamtriinh Jul 23, 2023
8a51c7f
Update bytes formatting
williamtriinh Jul 23, 2023
c52d72b
Add column headers
williamtriinh Jul 23, 2023
67fbb64
Remove hover effect from row item
williamtriinh Jul 23, 2023
179fd11
Scroll to menu when clicking on the 3 dots
williamtriinh Jul 23, 2023
ab29b43
Merge branch 'chat-everywhere' into will/20230706-read-write-files
williamtriinh Jul 23, 2023
4379a4e
Run npm install after merge
williamtriinh Jul 23, 2023
8e86c76
Refactor function calling for files manipulation
williamtriinh Jul 23, 2023
df47535
Fix typo
williamtriinh Jul 23, 2023
4e252c6
Remove console logs
williamtriinh Jul 23, 2023
704aea4
Merge branch 'chat-everywhere' into will/20230706-read-write-files
williamtriinh Jul 31, 2023
85b301e
Add no files text
williamtriinh Jul 31, 2023
6508bf7
Fix drag and drop area
williamtriinh Aug 1, 2023
f8165e8
Upload files to supabase
williamtriinh Aug 1, 2023
061cdb0
Remove files from the server
williamtriinh Aug 2, 2023
31cc46b
Fetch files from the server
williamtriinh Aug 3, 2023
8065ee1
Fix error when deleting attachment
williamtriinh Aug 3, 2023
908a86d
Remove unused import
williamtriinh Aug 3, 2023
71c30af
Create files table
williamtriinh Aug 3, 2023
a084db5
Refactor uploading files
williamtriinh Aug 4, 2023
cca3da1
Use alternative pagination approach for files; refactoring
williamtriinh Aug 4, 2023
6ce8125
Remove console logs
williamtriinh Aug 4, 2023
905d8e5
Clean up function
williamtriinh Aug 4, 2023
76c3cce
Refactor 'Attachments' to 'Uploaded Files'
williamtriinh Aug 5, 2023
1e432d6
Forgot to fix import when refactoring 'attachments'
williamtriinh Aug 5, 2023
db3d4e4
Replace 'attachments' with 'files'
williamtriinh Aug 5, 2023
279c525
Fix uploading files
williamtriinh Aug 5, 2023
43173ba
Fix renaming files
williamtriinh Aug 5, 2023
a97a207
Rename 'attachments' api to 'files'
williamtriinh Aug 5, 2023
6a9b0a3
Update file deletion
williamtriinh Aug 5, 2023
afa50bb
Refactor some code
williamtriinh Aug 5, 2023
831d615
Improve fetching files after uploading
williamtriinh Aug 5, 2023
5d62c33
Merge branch 'chat-everywhere' into will/20230706-read-write-files
williamtriinh Aug 6, 2023
b69293a
Make file pagination more consistent when uploading files
williamtriinh Aug 6, 2023
23f2951
Prevent duplicate files from appearing when uploading files
williamtriinh Aug 6, 2023
9fdb0a6
Add the ability to refresh files
williamtriinh Aug 6, 2023
fd8dc1f
Scroll list to top when refreshing files
williamtriinh Aug 6, 2023
8ceb3d2
Improve file syncing
williamtriinh Aug 8, 2023
2b3d286
Shorten button labels on mobile
williamtriinh Aug 8, 2023
2c5b0c9
Fix GPT's file reading
williamtriinh Aug 11, 2023
0cf6c27
Merge branch 'chat-everywhere' into will/20230706-read-write-files
williamtriinh Aug 11, 2023
5f8bfad
Run npm install
williamtriinh Aug 11, 2023
8940460
Rename state
williamtriinh Aug 11, 2023
e4a8b21
Add comment to callable function read
williamtriinh Aug 11, 2023
6708da7
Use filename instead of file id
williamtriinh Aug 11, 2023
fb25261
Refactor renaming files endpoint
williamtriinh Aug 11, 2023
5a21330
Refactor file removal
williamtriinh Aug 11, 2023
b932b69
Fix writing operation using function calling
williamtriinh Aug 12, 2023
0885baf
Improve file searching using GPT
williamtriinh Aug 13, 2023
c1f8489
Add renaming files to assistant
williamtriinh Aug 13, 2023
8a870a0
Update downloading files locally
williamtriinh Aug 13, 2023
c3d16e6
Fix file downloads using function calling
williamtriinh Aug 14, 2023
1d0a66a
Refactor file removal
williamtriinh Aug 14, 2023
ca31807
Restrict files to text/plain and improve uploading error handling
williamtriinh Aug 14, 2023
796dc9a
Refactor
williamtriinh Aug 14, 2023
d4bc376
Updating assistant system prompt
williamtriinh Aug 15, 2023
12d1be1
Merge branch 'chat-everywhere' into will/20230706-read-write-files
williamtriinh Aug 15, 2023
1397c57
Add creating files bucket to seed file
williamtriinh Aug 15, 2023
4765b73
Display max file upload size and total files in files modal
williamtriinh Aug 15, 2023
d7c6995
Make files header align properly when scroll bar is visible
williamtriinh Aug 15, 2023
e803a21
Fix FilesList element layout
williamtriinh Aug 15, 2023
dd807ed
Limit the number of files users can have.
williamtriinh Aug 15, 2023
4f979d7
Display better error messages when file syncing fails
williamtriinh Aug 15, 2023
2614f0b
Merge branch 'chat-everywhere' into will/20230706-read-write-files
thejackwu Sep 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions components/Assistant/FileItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { IconCheck, IconDotsVertical, IconDownload, IconFile, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
import { Fragment, KeyboardEvent, MouseEvent, MouseEventHandler, PropsWithChildren, useContext, useEffect, useRef, useState } from "react";
import { Menu, Transition } from "@headlessui/react";
import prettyBytes from "pretty-bytes";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";

import SidebarActionButton from "../Buttons/SidebarActionButton/SidebarActionButton";
import FilesModalContext from "./FilesModal.context";
import { UploadedFile } from "@/types/uploadedFile";

dayjs.extend(relativeTime);

type Props = {
file: UploadedFile;
}

export function FileItem({ file }: Props): JSX.Element {
const {
deleteFile,
renameFile,
downloadFile,
} = useContext(FilesModalContext);

const [isDeleting, setIsDeleting] = useState<boolean>(false);
const [isRenaming, setIsRenaming] = useState<boolean>(false);
const [renameValue, setRenameValue] = useState<string>(file.name);

const inputRef = useRef<HTMLInputElement>(null);
const menuRef = useRef<HTMLDivElement>(null);

const { t } = useTranslation('model');

const handleDeleteButtonClick: MouseEventHandler<HTMLButtonElement> = (e) => {
e.stopPropagation();
setIsDeleting(true);
};

const handleRenameButtonClick: MouseEventHandler<HTMLButtonElement> = (e) => {
e.stopPropagation();
setIsRenaming(true);
};

const handleConfirmButtonClick: MouseEventHandler<HTMLButtonElement> = async (e) => {
e.stopPropagation();
if (isDeleting) {
deleteFile(file.name);
setIsDeleting(false);
} else if (isRenaming && await renameFile(file.name, renameValue)) {
setIsRenaming(false);
}
};

const handleCancelButtonClick: MouseEventHandler<HTMLButtonElement> = (e) => {
e.stopPropagation();
setIsDeleting(false);
setIsRenaming(false);
setRenameValue(file.name);
};

const handleInputKeyDown = async (e: KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
if (e.code === 'Enter' && await renameFile(file.name, renameValue)) {
setIsRenaming(false);
}
};

useEffect(() => {
if (isRenaming && inputRef.current) {
const filename: string | undefined = file.name.match(/(.+?)\.[^.]*$|$/)![1];
inputRef.current.setSelectionRange(0, filename?.length || 0);
}
}, [isRenaming, file.name]);

useEffect(() => {
setRenameValue(file.name);
}, [file.name]);

return (
<Menu
as="div"
className="relative flex flex-row flex-grow flex-shrink items-center min-w-0 gap-3 p-3 select-none"
>
<IconFile
className="flex-shrink-0"
size={18}
/>
{isRenaming ? (
<input
className="flex-1 min-w-0 border-neutral-400 bg-transparent text-left text-sm text-white outline-none focus:border-neutral-100 pointer-events-auto"
type="text"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={handleInputKeyDown}
autoFocus
ref={inputRef}
/>
) : (
<p className="flex-1 text-sm text-left text-ellipsis overflow-hidden">
{file.name}
</p>
)}

{(isDeleting || isRenaming) && (
<div className="flex flex-row flex-shrink-0 items-center space-x-2 text-gray-300 pointer-events-auto">
<SidebarActionButton handleClick={handleConfirmButtonClick}>
<IconCheck size={18} />
</SidebarActionButton>
<SidebarActionButton handleClick={handleCancelButtonClick}>
<IconX size={18} />
</SidebarActionButton>
</div>
)}

{!isDeleting && !isRenaming && (
<div className="flex flex-row flex-shrink-0 items-center gap-2 text-gray-300">
<p className="block mobile:hidden text-sm text-neutral-400 whitespace-nowrap">
{dayjs(file.updatedAt).fromNow()}
</p>
<p className="w-20 mr-2 text-sm text-right text-neutral-400 whitespace-nowrap">
{prettyBytes(file.size, { minimumFractionDigits: 0, maximumFractionDigits: 1 }) || '--'}
</p>
<div className="flex tablet:hidden flex-row items-center gap-2 pointer-events-auto">
<SidebarActionButton handleClick={(e) => {
e.stopPropagation();
downloadFile(file.name);
}}>
<IconDownload size={18} />
</SidebarActionButton>
<SidebarActionButton handleClick={handleRenameButtonClick}>
<IconPencil size={18} />
</SidebarActionButton>
<SidebarActionButton handleClick={handleDeleteButtonClick}>
<IconTrash size={18} />
</SidebarActionButton>
</div>
<Menu.Button
className="hidden tablet:block p-1 pointer-events-auto"
onClick={(event) => {
event.stopPropagation();
setTimeout(() => {
if (menuRef.current) {
menuRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
}}
>
<IconDotsVertical size={18} />
</Menu.Button>
</div>
)}
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute top-full right-0 w-56 p-2 rounded-md bg-[#202123] drop-shadow-xl focus:outline-none z-10"
onClick={(event) => event.stopPropagation()}
>
<div ref={menuRef}>
<MenuItemButton
onClick={(event) => {
event.stopPropagation();
downloadFile(file.name);
}}
>
<IconDownload size={18} />
{t('Download')}
</MenuItemButton>
<MenuItemButton
onClick={handleRenameButtonClick}
>
<IconPencil size={18} />
{t('Rename')}
</MenuItemButton>
<MenuItemButton
onClick={handleDeleteButtonClick}
>
<IconTrash size={18} />
{t('Delete')}
</MenuItemButton>
</div>
</Menu.Items>
</Transition>
</Menu>
);
}

type MenuItemButtonProps = {
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
} & PropsWithChildren;

function MenuItemButton({ children, onClick }: MenuItemButtonProps): JSX.Element {
return (
<Menu.Item>
{({ active }) => (
<button
className={`
${ active ? 'bg-[#343541]/90 text-white' : 'text-gray-300' }
flex w-full items-center rounded-md p-2 text-sm `
}
onClick={onClick}
>
<div className="flex flex-row items-center gap-2">
{children}
</div>
</button>
)}
</Menu.Item>
)
}
163 changes: 163 additions & 0 deletions components/Assistant/FilesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { ForwardedRef, UIEvent, forwardRef, useCallback, useContext, useImperativeHandle, useRef } from "react";
import { useTranslation } from "react-i18next";

import FilesModelContext from "./FilesModal.context";
import { FileItem } from "./FileItem";
import Spinner from "../Spinner/Spinner";
import { sortByName } from "@/utils/app/uploadedFiles";
import { MAX_NUM_FILES } from "@/utils/app/const";

const Component = (props: unknown, ref: ForwardedRef<HTMLDivElement>): JSX.Element => {
const {
state: {
uploadedFiles,
uploadedFilenames,
loading,
nextFile,
totalFiles,
},
dispatch,
loadFiles,
uploadFiles,
} = useContext(FilesModelContext);

const enterTarget = useRef<HTMLElement | null>(null);
const dropAreaRef = useRef<HTMLDivElement>(null);

const { t } = useTranslation('models');

const handleDragEnter = (event: React.DragEvent<HTMLDivElement>): void => {
event.preventDefault();

enterTarget.current = event.target as HTMLElement;

if (event.dataTransfer.items) {
const item = event.dataTransfer.items[0];
if (!item || item.kind !== 'file') return;
}

if (dropAreaRef.current) {
dropAreaRef.current.style.opacity = '1';
}
};

const handleDragLeave = (event: React.DragEvent<HTMLDivElement>): void => {
event.preventDefault();

if (enterTarget.current == event.target) {
if (dropAreaRef.current) {
dropAreaRef.current.style.opacity = '0';
}
}
};

const handleDragOver = (event: React.DragEvent<HTMLDivElement>): void => {
event.preventDefault();
};

const handleDrop = async (event: React.DragEvent<HTMLDivElement>): Promise<void> => {
event.preventDefault();

let files!: FileList | File[];
if (event.dataTransfer.items) {
files = [];
for (let i = 0; i < event.dataTransfer.items.length; i++) {
const item = event.dataTransfer.items[i];
if (item.kind === 'file') {
const file = item.getAsFile();
if (file) files.push(file);
}
}
} else {
files = event.dataTransfer.files;
}

if (dropAreaRef.current) {
dropAreaRef.current.style.opacity = '0';
}

await uploadFiles(files);
};

const handleScroll = useCallback(async (event: UIEvent<HTMLDivElement>): Promise<void> => {
if (!nextFile || loading) return;
// Load more files when we reach the bottom of the scroll element
const target = event.currentTarget;
if (Math.abs(target.scrollHeight - target.clientHeight - target.scrollTop) < 1) {
dispatch({ field: 'loading', value: true });
loadFiles(nextFile)
.then(({ files, next }) => {
const updatedUploadedFiles = { ...uploadedFiles };
const updatedUploadedFilenames = [...uploadedFilenames];

for (const file of files) {
if (!uploadedFiles[file.name]) // Prevents duplicate files
updatedUploadedFilenames.push(file.name);
updatedUploadedFiles[file.name] = file;
}
updatedUploadedFilenames.sort(sortByName);

dispatch({ field: 'nextFile', value: next });
dispatch({ field: 'uploadedFiles', value: updatedUploadedFiles });
dispatch({ field: 'uploadedFilenames', value: updatedUploadedFilenames });
})
.finally(() => {
dispatch({ field: 'loading', value: false });
});
}
}, [uploadedFiles, uploadedFilenames, loading, nextFile, loadFiles, dispatch]);

return (
<>
<div className="flex flex-col pb-2">
<p className="text-sm text-neutral-400">{t('Maximum file size: 1MB')}</p>
<p className="text-sm text-neutral-400">{t('Total files:')} {totalFiles}/{MAX_NUM_FILES}</p>
</div>
<div className="relative flex flex-col flex-1 -mx-3 overflow-hidden">
<div
className="absolute left-0 w-full h-full rounded-md border-2 bg-indigo-300/30 border-indigo-400 opacity-0 transition-opacity ease-out duration-200 pointer-events-none z-50"
ref={dropAreaRef}
/>
<div
className="flex-1 overflow-y-auto"
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragOver={handleDragOver}
onScroll={handleScroll}
ref={ref}
>
<div className="relative h-7">
<div className="fixed left-0 right-0 flex flex-row justify-between text-sm text-neutral-400 py-2 pl-[53px] pr-[149px] tablet:pr-[64px] bg-neutral-900 z-30">
<p>{t('Name')}</p>
<div className="flex flex-row gap-2">
<p className="block mobile:hidden">{t('Updated At')}</p>
<p className="w-20 text-right">{t('Size')}</p>
</div>
</div>
</div>
{!loading && uploadedFilenames.length <= 0 && (
<p className="mt-3 text-[14px] leading-normal text-center text-white opacity-50">{t('No files')}</p>
)}
{(uploadedFilenames.map((filename) => {
const file = uploadedFiles[filename];
return (
<FileItem
file={file}
key={file.name}
/>
);
})
)}
</div>
{loading && (
<div className="absolute left-0 w-full h-full flex justify-center items-center rounded-md bg-black/30">
<Spinner size="16px" className="mx-auto" />
</div>
)}
</div>
</>
);
};

export const FilesList = forwardRef<HTMLDivElement, unknown>(Component);
Loading