Skip to content

Commit

Permalink
Command button enhancements (#431)
Browse files Browse the repository at this point in the history
* Command button enhancements

* Review fixes, integrate Attach Files

* Fix build issue for mobile

* Add Download icon

* Switch to gear icon
  • Loading branch information
humphd committed Feb 12, 2024
1 parent fe6c797 commit 68e0ff0
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 123 deletions.
4 changes: 2 additions & 2 deletions src/Chat/ChatBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "../lib/ChatCraftMessage";
import { ChatCraftChat } from "../lib/ChatCraftChat";
import { useUser } from "../hooks/use-user";
import NewButton from "../components/NewButton";
import OptionsButton from "../components/OptionsButton";
import { useSettings } from "../hooks/use-settings";
import { useModels } from "../hooks/use-models";
import ChatHeader from "./ChatHeader";
Expand Down Expand Up @@ -362,7 +362,7 @@ function ChatBase({ chat }: ChatBaseProps) {
<Box maxW="900px" mx="auto" h="100%">
{chat.readonly ? (
<Flex w="100%" h="45px" justify="end" align="center" p={2}>
<NewButton forkUrl={`./fork`} variant="solid" />
<OptionsButton forkUrl={`./fork`} variant="solid" />
</Flex>
) : (
<PromptForm
Expand Down
4 changes: 2 additions & 2 deletions src/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Sidebar from "./components/Sidebar";
import db, { ChatCraftMessageTable } from "./lib/db";
import Message from "./components/Message";
import { ChatCraftMessage } from "./lib/ChatCraftMessage";
import NewButton from "./components/NewButton";
import OptionsButton from "./components/OptionsButton";
import { useSettings } from "./hooks/use-settings";

export async function loader({ request }: LoaderFunctionArgs) {
Expand Down Expand Up @@ -149,7 +149,7 @@ export default function Search() {

<GridItem>
<Flex w="100%" maxW="900px" mx="auto" h="45px" justify="end" align="center" p={2}>
<NewButton variant="solid" />
<OptionsButton variant="solid" />
</Flex>
</GridItem>
</Grid>
Expand Down
47 changes: 0 additions & 47 deletions src/components/NewButton.tsx

This file was deleted.

229 changes: 229 additions & 0 deletions src/components/OptionsButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import {
Button,
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
IconButton,
Input,
useDisclosure,
} from "@chakra-ui/react";
import { Link as ReactRouterLink, useFetcher, useLoaderData } from "react-router-dom";
import { TbShare2, TbTrash, TbCopy, TbDownload } from "react-icons/tb";
import { PiGearBold } from "react-icons/pi";
import { BsPaperclip } from "react-icons/bs";
import { useCallback, useRef } from "react";
import { useLiveQuery } from "dexie-react-hooks";
import { useCopyToClipboard } from "react-use";

import { ChatCraftChat } from "../lib/ChatCraftChat";
import { useUser } from "../hooks/use-user";
import { useAlert } from "../hooks/use-alert";
import ShareModal from "./ShareModal";
import { download } from "../lib/utils";

function ShareMenuItem({ chat }: { chat?: ChatCraftChat }) {
const supportsWebShare = !!navigator.share;
const { user } = useUser();
const { error } = useAlert();
const { isOpen, onOpen, onClose } = useDisclosure();

const handleWebShare = useCallback(async () => {
if (!chat || !user) {
return;
}

try {
const { url } = await chat.share(user);
if (!url) {
throw new Error("Unable to create share URL for chat");
}

navigator.share({ title: "ChatCraft Chat", text: chat.summary, url });
} catch (err: any) {
console.error(err);
error({ title: "Unable to share chat", message: err.message });
}
}, [chat, user, error]);

// Nothing to share, disable the menu item
if (!chat) {
return (
<>
<MenuDivider />
<MenuItem icon={<TbShare2 />} isDisabled={true}>
Share
</MenuItem>
</>
);
}

return (
<>
<MenuItem icon={<TbShare2 />} onClick={supportsWebShare ? handleWebShare : onOpen}>
Share
</MenuItem>
<ShareModal chat={chat} isOpen={isOpen} onClose={onClose} />
</>
);
}

type OptionsButtonProps = {
forkUrl?: string;
variant?: "outline" | "solid" | "ghost";
iconOnly?: boolean;
// Optional until we support on mobile...
onFileSelected?: (base64: string) => void;
isDisabled?: boolean;
};

function OptionsButton({
forkUrl,
variant = "outline",
onFileSelected,
iconOnly = false,
isDisabled = false,
}: OptionsButtonProps) {
const fetcher = useFetcher();
const { info } = useAlert();
const [, copyToClipboard] = useCopyToClipboard();
const chatId = useLoaderData() as string;
const chat = useLiveQuery<ChatCraftChat | undefined>(() => {
if (chatId) {
return Promise.resolve(ChatCraftChat.find(chatId));
}
}, [chatId]);
const fileInputRef = useRef<HTMLInputElement>(null);

const handleFileChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
if (!onFileSelected) {
return;
}

const files = event.target.files;
if (files) {
for (let i = 0; i < files.length; i++) {
const reader = new FileReader();
reader.onload = (e) => {
onFileSelected(e.target?.result as string);
};
reader.readAsDataURL(files[i]);
}
// Reset the input value after file read
event.target.value = "";
}
},
[onFileSelected]
);

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

const handleCopyClick = useCallback(() => {
if (!chat) {
return;
}

const text = chat.toMarkdown();
copyToClipboard(text);
info({
title: "Chat copied to clipboard",
});
}, [chat, copyToClipboard, info]);

const handleDownloadClick = useCallback(() => {
if (!chat) {
return;
}

const text = chat.toMarkdown();
download(text, "chat.md", "text/markdown");
info({
title: "Chat downloaded as Markdown",
});
}, [chat, info]);

const handleDeleteClick = useCallback(() => {
if (!chat) {
return;
}

fetcher.submit({}, { method: "post", action: `/c/${chat.id}/delete` });
}, [chat, fetcher]);

return (
<Menu>
{iconOnly ? (
<MenuButton
isDisabled={isDisabled}
as={IconButton}
size="lg"
variant="outline"
icon={<PiGearBold />}
isRound
/>
) : (
<MenuButton
isDisabled={isDisabled}
as={Button}
size="sm"
variant={variant}
leftIcon={<PiGearBold />}
>
Options
</MenuButton>
)}
<MenuList>
<MenuItem as={ReactRouterLink} to="/new">
Clear
</MenuItem>
<MenuItem as={ReactRouterLink} to="/new" target="_blank">
New Window
</MenuItem>
{!!forkUrl && (
<MenuItem as={ReactRouterLink} to={forkUrl} target="_blank">
Duplicate...
</MenuItem>
)}
<MenuDivider />
<MenuItem isDisabled={!chat} icon={<TbCopy />} onClick={() => handleCopyClick()}>
Copy
</MenuItem>
<MenuItem icon={<TbDownload />} isDisabled={!chat} onClick={() => handleDownloadClick()}>
Download
</MenuItem>
<ShareMenuItem chat={chat} />
<MenuDivider />
{!!onFileSelected && (
<>
<Input
multiple
type="file"
ref={fileInputRef}
hidden
onChange={handleFileChange}
accept="image/*"
/>
<MenuItem icon={<BsPaperclip />} onClick={handleAttachFiles}>
Attach Files...
</MenuItem>
<MenuDivider />
</>
)}
<MenuItem
color="red.400"
icon={<TbTrash />}
isDisabled={!chat}
onClick={() => handleDeleteClick()}
>
Delete Chat
</MenuItem>
</MenuList>
</Menu>
);
}

export default OptionsButton;
61 changes: 0 additions & 61 deletions src/components/PromptForm/AttachFileButton.tsx

This file was deleted.

Loading

0 comments on commit 68e0ff0

Please sign in to comment.