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

[UI] File Attachment Added #804

Merged
merged 33 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
17dd5c7
PaperClipIcon raw functionality implemented
mulla028 Jan 27, 2025
c461880
Hover effect that makes + button appear implemented without functiona…
mulla028 Jan 27, 2025
0574603
Deleted prop
mulla028 Jan 27, 2025
ea1c53e
PaperClip integrated
mulla028 Jan 27, 2025
fc0f99b
Unused import deleted
mulla028 Jan 27, 2025
34318ae
naming problem fixed
mulla028 Jan 27, 2025
3d98b4d
file renamed to pass CI
mulla028 Jan 27, 2025
873582c
Added 2nd prop for Paperclip component to handle the attachment
mulla028 Jan 27, 2025
3a61614
+ button removed, new functionality for paperclip button and now user…
mulla028 Jan 27, 2025
0988084
formatFileSize helper functions moved to lib/utils.ts where it belongs
mulla028 Jan 28, 2025
8945601
acceptableFileFormats created in lib/utils and replaced everywhere wh…
mulla028 Jan 28, 2025
3d51ad3
PaperClipIcon moved to the right of the mic
mulla028 Jan 28, 2025
0f8e4b8
refreshFiles applied
mulla028 Jan 28, 2025
3589625
Delete icon changed, download icon deleted, download file via file.name
mulla028 Jan 28, 2025
8715d29
modal body scrollable, doesn't extend endlessly
mulla028 Jan 28, 2025
a06691b
file icon component implemented for code simplicity
mulla028 Jan 28, 2025
20eb81e
hover's background color changes base on theme color
mulla028 Jan 28, 2025
6fd5b47
Tooltip's label appears on top vs. below
mulla028 Jan 28, 2025
27ee13d
Modal Header now shows the number of files attached
mulla028 Jan 28, 2025
ba04853
X icon animation removed
mulla028 Jan 28, 2025
b41a67d
Attach File... Card deleted, buttons delete all and add files added t…
mulla028 Jan 28, 2025
bca55ca
colorscheme for remove button deleted
mulla028 Jan 28, 2025
05daf54
icon removed, Remove All red, variant ghost, colorscheme red
mulla028 Jan 28, 2025
548211c
Download Icon added and placed on the left side of the card
mulla028 Jan 29, 2025
35b95fd
renameFile function implemented with validation
mulla028 Jan 29, 2025
f03a54a
edit file name by clicking on the name added
mulla028 Jan 29, 2025
92a5d40
renameFile deleted since it was implemented in #800
mulla028 Jan 30, 2025
0089cbc
renameFile receive arguments in proper order
mulla028 Jan 30, 2025
553a576
onRefresh() removed in Download File
mulla028 Jan 30, 2025
fb31e3d
Added try catch and error modal window if deletion was failed
mulla028 Jan 30, 2025
0dd9ed6
acceptableFileFormats moved from lib/utils.tsx to hooks/use-file-import
mulla028 Jan 31, 2025
b1e2889
path to acceptableFileFormats adjusted
mulla028 Jan 31, 2025
12653ae
jsonl files upload support added
mulla028 Jan 31, 2025
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
183 changes: 183 additions & 0 deletions src/components/FileIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {
Box,
Editable,
EditablePreview,
EditableInput,
Flex,
IconButton,
Text,
Tooltip,
useColorModeValue,
} from "@chakra-ui/react";
import { IconType } from "react-icons";
import { IoClose } from "react-icons/io5";
import {
BsFiletypeJpg,
BsFiletypeCsv,
BsFiletypeDoc,
BsFiletypePdf,
BsFiletypePng,
BsFiletypeXls,
BsFiletypeTxt,
BsFiletypeJson,
BsFiletypeMd,
BsFiletypeSvg,
BsFileEarmark,
} from "react-icons/bs";
import { ChatCraftChat } from "../lib/ChatCraftChat";
import { downloadFile, removeFile, renameFile } from "../lib/fs";
import { formatFileSize } from "../lib/utils";
import { IoMdDownload } from "react-icons/io";

// File icon map moved to the new component
const FILE_ICON_MAP: Record<string, IconType> = {
// Images
jpg: BsFiletypeJpg,
jpeg: BsFiletypeJpg,
png: BsFiletypePng,
svg: BsFiletypeSvg,
// Documents
doc: BsFiletypeDoc,
docx: BsFiletypeDoc,
pdf: BsFiletypePdf,
txt: BsFiletypeTxt,
// Spreadsheets
csv: BsFiletypeCsv,
xls: BsFiletypeXls,
xlsx: BsFiletypeXls,
// Code
json: BsFiletypeJson,
md: BsFiletypeMd,
};

type FileIconProps = {
file: {
id: string;
name: string;
size: number;
};
chat: ChatCraftChat;
onRefresh: () => void;
};

const FileIcon = ({ file, chat, onRefresh }: FileIconProps) => {
const hoverBg = useColorModeValue("gray.300", "gray.600");

// Moved the fileIcon function into the component
const fileIcon = (name: string) => {
const extension = name.split(".").pop()?.toLowerCase() || "";
const IconComponent = FILE_ICON_MAP[extension] || BsFileEarmark;
return <IconComponent size="80px" />;
};

return (
<Box
key={file.id}
p={6}
borderWidth="1px"
borderRadius="md"
minW="200px"
maxW="200px"
h="200px"
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="space-between"
aspectRatio="1"
position="relative"
_hover={{
borderColor: "blue.500",
"& .hover-buttons": {
opacity: 1,
transform: "translateY(0)",
},
bg: hoverBg,
}}
transition="all 0.2s ease-in-out"
>
<Box position="absolute" top="40%" left="50%" transform="translate(-50%, -50%)" opacity="0.7">
{fileIcon(file.name)}
</Box>
<Flex
className="hover-buttons"
position="absolute"
top="4"
right="4"
gap={2}
opacity={0}
transition="all 0.2s ease-in-out"
>
<Tooltip label="Delete File">
<IconButton
aria-label="Remove file"
icon={<IoClose />}
size="sm"
variant="ghost"
onClick={async () => {
await removeFile(file.name, chat);
onRefresh();
}}
/>
</Tooltip>
</Flex>
<Flex
className="hover-buttons"
position="absolute"
top="4"
left="4"
gap={2}
opacity={0}
transition="all 0.2s ease-in-out"
>
<Tooltip label="Download File">
<IconButton
aria-label="Download File"
icon={<IoMdDownload />}
size="sm"
variant="ghost"
onClick={async () => {
await downloadFile(file.name, chat);
}}
/>
</Tooltip>
</Flex>
<Box position="absolute" bottom={6} width="full" textAlign="center">
<Tooltip label="Edit Name">
<Editable
defaultValue={file.name}
mx="auto"
w="90%"
onSubmit={async (newVal) => {
await renameFile(file.name, newVal, chat);
onRefresh();
}}
>
<EditablePreview
fontSize="sm"
color="blue.300"
noOfLines={1}
px={2}
cursor="pointer"
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
/>
<EditableInput
fontSize="sm"
color="blue.300"
px={2}
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
/>
</Editable>
</Tooltip>
<Text fontSize="xs" color="gray.400">
{formatFileSize(file.size)}
</Text>
</Box>
</Box>
);
};

export default FileIcon;
3 changes: 2 additions & 1 deletion src/components/OptionsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useSettings } from "../hooks/use-settings";
import ShareModal from "./ShareModal";
import { download } from "../lib/utils";
import { Menu, MenuDivider, MenuItem, MenuItemLink, SubMenu } from "./Menu";
import { acceptableFileFormats } from "../hooks/use-file-import";

function ShareMenuItem({ chat }: { chat: ChatCraftChat }) {
const { isOpen, onOpen, onClose } = useDisclosure();
Expand Down Expand Up @@ -218,7 +219,7 @@ function OptionsButton({
ref={fileInputRef}
hidden
onChange={handleFileChange}
accept="image/*,text/*,.pdf,application/pdf,*.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.json,application/json,application/markdown"
accept={acceptableFileFormats}
/>
<MenuItem icon={<BsPaperclip />} onClick={handleAttachFiles}>
Attach Files...
Expand Down
2 changes: 2 additions & 0 deletions src/components/PromptForm/DesktopPromptForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useKeyDownHandler } from "../../hooks/use-key-down-handler";
import ImageModal from "../ImageModal";
import { ChatCraftChat } from "../../lib/ChatCraftChat";
import { useFileImport } from "../../hooks/use-file-import";
import PaperclipIcon from "./PaperclipIcon";

type KeyboardHintProps = {
isVisible: boolean;
Expand Down Expand Up @@ -380,6 +381,7 @@ function DesktopPromptForm({
onTranscriptionAvailable={handleTranscriptionAvailable}
onCancel={handleRecordingCancel}
/>
<PaperclipIcon chat={chat} onAttachFiles={importFiles} />
</Flex>
</Flex>
</InputGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/components/PromptForm/MicIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export default function MicIcon({
};

return (
<Tooltip label={isRecording ? "Finish Recording" : "Start Recording"}>
<Tooltip label={isRecording ? "Finish Recording" : "Start Recording"} placement="top">
<IconButton
isRound
isDisabled={isDisabled}
Expand Down
159 changes: 159 additions & 0 deletions src/components/PromptForm/PaperclipIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import {
IconButton,
Modal,
Tooltip,
useDisclosure,
ModalHeader,
ModalContent,
ModalOverlay,
ModalCloseButton,
ModalBody,
Text,
SimpleGrid,
Input,
ModalFooter,
Button,
} from "@chakra-ui/react";
import { FaPaperclip } from "react-icons/fa";
import { ChatCraftChat } from "../../lib/ChatCraftChat";
import { useFiles } from "../../hooks/use-fs";
import { useAlert } from "../../hooks/use-alert";
import { useCallback, useRef, useState } from "react";
import { acceptableFileFormats } from "../../hooks/use-file-import";
import FileIcon from "../FileIcon";
import { removeFile } from "../../lib/fs";

type PaperClipProps = {
chat: ChatCraftChat;
onAttachFiles?: (files: File[]) => Promise<void>;
};

function PaperclipIcon({ chat, onAttachFiles }: PaperClipProps) {
const { files, loading, error, refreshFiles } = useFiles(chat);
const isAttached = files.length ? true : false;
const { isOpen, onOpen, onClose } = useDisclosure();
const { isOpen: isErrorOpen, onOpen: onErrorOpen, onClose: onErrorClose } = useDisclosure();
const { error: alertError } = useAlert();
const fileInputRef = useRef<HTMLInputElement>(null);
const [deleteError, setDeleteError] = useState<string>("");

const handleFileChange = useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
if (!onAttachFiles || !event.target.files?.length) {
return;
}
await onAttachFiles(Array.from(event.target.files))
.then(() => refreshFiles)
.catch((err) => alertError({ title: "Unable to Attach Files", message: err.message }));
},
[onAttachFiles, alertError, refreshFiles]
);

const handleAttachFiles = useCallback(() => {
fileInputRef.current?.click();
}, [fileInputRef]);

const handlePaperClipToggle = () => {
onOpen();
};

const handleDeleteAll = async () => {
try {
await Promise.all(files.map((file) => removeFile(file.name, chat)));
refreshFiles();
onClose();
} catch (error) {
setDeleteError(error instanceof Error ? error.message : "Failed to delete files");
onErrorOpen();
}
};

return (
<>
<Tooltip label="Attach Files..." placement="top">
{!isAttached && (
<Input
multiple
type="file"
ref={fileInputRef}
hidden
onChange={handleFileChange}
accept={acceptableFileFormats}
/>
)}
<IconButton
isRound
icon={<FaPaperclip />}
variant="ghost"
size="md"
fontSize="1rem"
transition={"all 150ms ease-in-out"}
onClick={isAttached ? handlePaperClipToggle : handleAttachFiles}
aria-label=""
/>
</Tooltip>
<Modal isCentered onClose={onClose} isOpen={isOpen}>
<ModalOverlay />
<ModalContent maxW="900px" w="90vw" p={4} position="absolute">
<ModalHeader>{files.length} Attached Files</ModalHeader>
<ModalCloseButton />
<ModalBody maxH="70vh" overflowY="auto">
{loading ? (
<Text>Loading Files...</Text>
) : error ? (
<Text color="red.500">Error loading files!</Text>
) : (
<SimpleGrid columns={3} spacing={6} width="full">
<Input
multiple
type="file"
ref={fileInputRef}
hidden
onChange={handleFileChange}
accept={acceptableFileFormats}
/>
{files.map((file) => (
<FileIcon key={file.id} file={file} chat={chat} onRefresh={refreshFiles} />
))}
</SimpleGrid>
)}
</ModalBody>
<ModalFooter gap={2}>
<Button
onClick={handleDeleteAll}
maxH="30"
variant="ghost"
color="red"
colorScheme="red"
>
Remove All
</Button>
<Button gap={2} onClick={handleAttachFiles} maxH="30px">
Add Files
</Button>
</ModalFooter>
</ModalContent>
</Modal>

{/* Error modal */}
<Modal isCentered isOpen={isErrorOpen} onClose={onErrorClose} size="sm">
<ModalOverlay backdropFilter="blur(2px)" />
<ModalContent>
<ModalHeader
color="red"
display="flex"
alignItems="center"
justifyContent="space-between"
>
Error !
<ModalCloseButton color="white" />
</ModalHeader>
<ModalBody color="red" pb={6}>
{deleteError}
</ModalBody>
</ModalContent>
</Modal>
</>
);
}
export default PaperclipIcon;
3 changes: 3 additions & 0 deletions src/hooks/use-file-import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { getSettings } from "../lib/settings";
import { JinaAIProvider, type JinaAiReaderResponse } from "../lib/providers/JinaAIProvider";
import { ChatCraftFile } from "../lib/ChatCraftFile";

export const acceptableFileFormats =
"image/*,text/*,.pdf,application/pdf,*.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document,.json,application/json,.jsonl,application/x-jsonlines,application/jsonl,application/markdown";

function readTextFile(file: File) {
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
Expand Down
Loading