Skip to content

Commit

Permalink
feat(attach file): allow the user to attach a context file to their q…
Browse files Browse the repository at this point in the history
…uestion
  • Loading branch information
MarcMcIntosh committed Jan 4, 2024
1 parent d1d94e5 commit 6edaa9e
Show file tree
Hide file tree
Showing 15 changed files with 281 additions and 36 deletions.
9 changes: 7 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,18 @@
[x] handle long requests

[x] scroll lags a bit
[x] attach file (this will be different between docker and IDE's)
[x] use the event bus to handle the file upload in the browser this can be done with the file system api using `window.showOpenFilePicker()`
[ ] should we allow multiple context files?
[ ] context file display could be an accordion button

[ ] confirm if the lsp only responds with assistant deltas

### TBD

[ ] build an asset for docker (saves installing node on ubunto through nvm)
[ ] automate adding the chat to docker

[ ] in docker it seems that it maybe be posable to use the chat as a plugin or web-component
[ ] in the self hosted docker image it seems that it maybe be posable to use the chat as a plugin or web-component, of function

### EVENTS TODO FOR IDEs

Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<title>Refact.ai Chat</title>
</head>
<body>
<div id="root"></div>
<div id="refact-chat"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
7 changes: 7 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/wicg-file-system-access": "^2023.10.4",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vitejs/plugin-react-swc": "^3.5.0",
Expand Down
30 changes: 22 additions & 8 deletions src/components/ChatContent/ChatContent.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import React, { useEffect } from "react";
import { ChatMessages } from "../../services/refact";
import { ChatMessages, isChatContextFileMessage } from "../../services/refact";
import { Markdown } from "../Markdown";
import { UserInput } from "./UserInput";
import { ScrollArea } from "../ScrollArea";
import { Spinner } from "../Spinner";

import { Box, Flex, Text } from "@radix-ui/themes";
import styles from "./ChatContent.module.css";

const ContextFile: React.FC<{ children: string }> = (props) => {
// TODO how should the context file look?
return <Markdown>{props.children}</Markdown>;
const ContextFile: React.FC<{ name: string; children: string }> = ({
name,
...props
}) => {
return (
<Text size="2" title={props.children}>
<pre>📎 {name}</pre>
</Text>
);
};

const PlaceHolderText: React.FC = () => (
Expand Down Expand Up @@ -50,7 +55,18 @@ export const ChatContent: React.FC<{
<ScrollArea scrollbars="vertical">
<Flex grow="1" direction="column" className={styles.content}>
{messages.length === 0 && <PlaceHolderText />}
{messages.map(([role, text], index) => {
{messages.map((message, index) => {
if (isChatContextFileMessage(message)) {
const [, file] = message;
return (
<ContextFile key={index} name={file.file_name}>
{file.file_content}
</ContextFile>
);
}

const [role, text] = message;

if (role === "user") {
const handleRetry = (question: string) => {
const toSend = messages
Expand All @@ -63,8 +79,6 @@ export const ChatContent: React.FC<{
{text}
</UserInput>
);
} else if (role === "context_file") {
return <ContextFile key={index}>{text}</ContextFile>;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (role === "assistant") {
return <ChatInput key={index}>{text}</ChatInput>;
Expand Down
23 changes: 16 additions & 7 deletions src/components/ChatForm/ChatForm.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";

import { Box, Flex } from "@radix-ui/themes";
import { Box, Flex, Text } from "@radix-ui/themes";
import styles from "./ChatForm.module.css";

import { PaperPlaneButton, BackToSideBarButton } from "../Buttons/Buttons";
Expand All @@ -14,6 +14,7 @@ import {
import { ErrorCallout, Callout } from "../Callout";

import { Select } from "../Select/Select";
import { FileUpload } from "../FileUpload";
import { Button } from "@radix-ui/themes";

const CapsSelect: React.FC<{
Expand All @@ -22,12 +23,15 @@ const CapsSelect: React.FC<{
options: string[];
}> = ({ options, value, onChange }) => {
return (
<Select
title="chat model"
options={options}
value={value}
onChange={onChange}
></Select>
<Flex gap="2" align="center">
<Text size="2">Use model:</Text>
<Select
title="chat model"
options={options}
value={value}
onChange={onChange}
></Select>
</Flex>
);
};

Expand All @@ -43,6 +47,8 @@ export const ChatForm: React.FC<{
canChangeModel: boolean;
isStreaming: boolean;
onStopStreaming: () => void;
handleContextFile: () => void;
hasContextFile: boolean;
}> = ({
onSubmit,
onClose,
Expand All @@ -55,6 +61,8 @@ export const ChatForm: React.FC<{
canChangeModel,
isStreaming,
onStopStreaming,
handleContextFile,
hasContextFile,
}) => {
const [value, setValue] = React.useState("");
const isOnline = useIsOnline();
Expand All @@ -78,6 +86,7 @@ export const ChatForm: React.FC<{

return (
<Box mt="1" position="relative">
<FileUpload onClick={handleContextFile} checked={hasContextFile} />
{!isOnline && <Callout type="info">Offline</Callout>}
<Flex>
{canChangeModel && (
Expand Down
22 changes: 22 additions & 0 deletions src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { Checkbox, Flex, Text } from "@radix-ui/themes";

export const FileUpload: React.FC<{
onClick: () => void;
fileName?: string;
checked: boolean;
}> = ({ onClick, fileName, checked }) => {
return (
<Text as="label" size="2">
<Flex gap="2">
<Checkbox
checked={checked}
onCheckedChange={() => {
onClick();
}}
/>{" "}
Attach {fileName ?? "a file"}
</Flex>
</Text>
);
};
1 change: 1 addition & 0 deletions src/components/FileUpload/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FileUpload } from "./FileUpload";
41 changes: 41 additions & 0 deletions src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {
ChatResponse,
CapsResponse,
isCapsResponse,
ChatContextFile,
} from "../services/refact";

export enum EVENT_NAMES_FROM_CHAT {
SAVE_CHAT = "save_chat_to_history",
ASK_QUESTION = "chat_question",
REQUEST_CAPS = "chat_request_caps",
STOP_STREAMING = "chat_stop_streaming",
REQUEST_FILE = "chat_request_for_file",
}

export enum EVENT_NAMES_TO_CHAT {
Expand All @@ -24,6 +26,8 @@ export enum EVENT_NAMES_TO_CHAT {
RECEIVE_CAPS_ERROR = "receive_caps_error",
SET_CHAT_MODEL = "chat_set_chat_model",
SET_DISABLE_CHAT = "set_disable_chat",
RECEIVE_FILE = "receive_context_file",
REMOVE_FILE = "remove_context_file",
}

export type ChatThread = {
Expand All @@ -50,6 +54,18 @@ export function isActionFromChat(action: unknown): action is ActionFromChat {
return Object.values(ALL_EVENT_NAMES).includes(action.type);
}

export interface RequestForFileFromChat extends ActionFromChat {
type: EVENT_NAMES_FROM_CHAT.REQUEST_FILE;
payload: { id: string };
}

export function isRequestForFileFromChat(
action: unknown,
): action is RequestForFileFromChat {
if (!isActionFromChat(action)) return false;
return action.type === EVENT_NAMES_FROM_CHAT.REQUEST_FILE;
}

export interface QuestionFromChat extends ActionFromChat {
type: EVENT_NAMES_FROM_CHAT.ASK_QUESTION;
payload: ChatThread;
Expand Down Expand Up @@ -111,6 +127,31 @@ export function isActionToChat(action: unknown): action is ActionToChat {
return Object.values(EVENT_NAMES).includes(action.type);
}

export interface ReceiveContextFile extends ActionToChat {
type: EVENT_NAMES_TO_CHAT.RECEIVE_FILE;
payload: {
id: string;
file: ChatContextFile;
};
}

export function isReceiveContextFile(
action: unknown,
): action is ReceiveContextFile {
if (!isActionToChat(action)) return false;
return action.type === EVENT_NAMES_TO_CHAT.RECEIVE_FILE;
}

export interface RemoveContextFile extends ActionToChat {
type: EVENT_NAMES_TO_CHAT.REMOVE_FILE;
payload: { id: string };
}

export function isRemoveContext(action: unknown): action is RemoveContextFile {
if (!isActionToChat(action)) return false;
return action.type === EVENT_NAMES_TO_CHAT.REMOVE_FILE;
}

export interface SetChatDisable extends ActionToChat {
type: EVENT_NAMES_TO_CHAT.SET_DISABLE_CHAT;
payload: { id: string; disable: boolean };
Expand Down
11 changes: 10 additions & 1 deletion src/features/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChatForm } from "../components/ChatForm";
import { useEventBusForChat } from "../hooks/useEventBusForChat";
import { ChatContent } from "../components/ChatContent";
import { Flex } from "@radix-ui/themes";
import { isChatContextFileMessage } from "../services/refact";

export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {
const {
Expand All @@ -12,6 +13,8 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {
clearError,
setChatModel,
stopStreaming,
handleContextFile,
hasContextFile,
} = useEventBusForChat();

return (
Expand All @@ -35,7 +38,11 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {

<ChatForm
isStreaming={state.streaming}
canChangeModel={state.chat.messages.length === 0 && !state.streaming}
canChangeModel={
state.chat.messages.filter(
(message) => !isChatContextFileMessage(message),
).length === 0 && !state.streaming
}
error={state.error}
clearError={clearError}
onSubmit={(value) => {
Expand All @@ -45,6 +52,8 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {
onSetChatModel={setChatModel}
caps={state.caps}
onStopStreaming={stopStreaming}
handleContextFile={handleContextFile}
hasContextFile={hasContextFile}
/>
</Flex>
);
Expand Down
Loading

0 comments on commit 6edaa9e

Please sign in to comment.