Skip to content

Commit

Permalink
feat(vscode): add buttons for creating an new file and diff paste back
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcMcIntosh committed Jan 22, 2024
1 parent 8ddcc2e commit ebeb826
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 43 deletions.
17 changes: 16 additions & 1 deletion src/components/Buttons/Buttons.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { IconButton, Button } from "@radix-ui/themes";
import { IconButton, Button, Flex } from "@radix-ui/themes";
import { PaperPlaneIcon, ExitIcon, Cross1Icon } from "@radix-ui/react-icons";
import classNames from "classnames";
import styles from "./button.module.css";
Expand Down Expand Up @@ -39,3 +39,18 @@ export const RightButton: React.FC<ButtonProps & { className?: string }> = (
/>
);
};

export const RightButtonGroup: React.FC<
React.PropsWithChildren & {
className?: string;
direction?: "row" | "column";
}
> = (props) => {
return (
<Flex
{...props}
gap="1"
className={classNames(styles.rightButtonGroup, props.className)}
/>
);
};
5 changes: 5 additions & 0 deletions src/components/Buttons/button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
right: 20px;
min-width: 50px;
}

.rightButtonGroup {
position: absolute;
right: 20px;
}
7 changes: 6 additions & 1 deletion src/components/Buttons/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export { PaperPlaneButton, BackToSideBarButton } from "./Buttons";
export {
PaperPlaneButton,
BackToSideBarButton,
RightButton,
RightButtonGroup,
} from "./Buttons";
59 changes: 46 additions & 13 deletions src/components/ChatContent/ChatContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ChatMessages,
isChatContextFileMessage,
} from "../../services/refact";
import { Markdown } from "../Markdown";
import { Markdown, MarkdownProps } from "../Markdown";
import { UserInput } from "./UserInput";
import { ScrollArea } from "../ScrollArea";
import { Spinner } from "../Spinner";
Expand All @@ -26,11 +26,15 @@ const ContextFiles: React.FC<{ files: ChatContextFile[] }> = ({ files }) => {
return (
<pre>
<Flex gap="4" wrap="wrap">
{files.map((file, index) => (
<ContextFile key={index} name={file.file_name}>
{file.file_content}
</ContextFile>
))}
{files.map((file, index) => {
const lineText =
file.line1 && file.line2 ? `:${file.line1}-${file.line2}` : "";
return (
<ContextFile key={index} name={file.file_name + lineText}>
{file.file_content}
</ContextFile>
);
})}
</Flex>
</pre>
);
Expand All @@ -40,7 +44,14 @@ const PlaceHolderText: React.FC = () => (
<Text>Welcome to Refact chat! How can I assist you today?</Text>
);

const ChatInput: React.FC<{ children: string }> = (props) => {
type ChatInputProps = Pick<
MarkdownProps,
"onNewFileClick" | "onPasteClick" | "canPaste"
> & {
children: string;
};

const ChatInput: React.FC<ChatInputProps> = (props) => {
// TODO: new file button?
return (
<Box p="2" position="relative" width="100%" style={{ maxWidth: "100%" }}>
Expand All @@ -51,18 +62,31 @@ const ChatInput: React.FC<{ children: string }> = (props) => {
console.log("failed to copy to clipboard");
});
}}
onNewFileClick={props.onNewFileClick}
onPasteClick={props.onPasteClick}
canPaste={props.canPaste}
>
{props.children}
</Markdown>
</Box>
);
};

export const ChatContent: React.FC<{
messages: ChatMessages;
onRetry: (question: ChatMessages) => void;
isWaiting: boolean;
}> = ({ messages, onRetry, isWaiting }) => {
export const ChatContent: React.FC<
{
messages: ChatMessages;
onRetry: (question: ChatMessages) => void;
isWaiting: boolean;
canPaste: boolean;
} & Pick<MarkdownProps, "onNewFileClick" | "onPasteClick">
> = ({
messages,
onRetry,
isWaiting,
onNewFileClick,
onPasteClick,
canPaste,
}) => {
const ref = React.useRef<HTMLDivElement>(null);
useEffect(() => {
ref.current?.scrollIntoView &&
Expand Down Expand Up @@ -95,7 +119,16 @@ export const ChatContent: React.FC<{
);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
} else if (role === "assistant") {
return <ChatInput key={index}>{text}</ChatInput>;
return (
<ChatInput
onNewFileClick={onNewFileClick}
onPasteClick={onPasteClick}
canPaste={canPaste}
key={index}
>
{text}
</ChatInput>
);
} else {
return <Markdown key={index}>{text}</Markdown>;
}
Expand Down
109 changes: 89 additions & 20 deletions src/components/Markdown/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,88 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import SyntaxHighlighter from "react-syntax-highlighter";
import { Code } from "@radix-ui/themes";
import { RightButton } from "../Buttons/Buttons";
import { Code, Button, Flex } from "@radix-ui/themes";
import { RightButton, RightButtonGroup } from "../Buttons/";
import { ScrollArea } from "../ScrollArea";
import remarkBreaks from "remark-breaks";
import classNames from "classnames";
import "./highlightjs.css";
import styles from "./Markdown.module.css";
import { useConfig } from "../../contexts/config-context";

const PreTagWithCopyButton: React.FC<
const PreTagWithButtons: React.FC<
React.PropsWithChildren<{
onClick?: () => void;
onCopyClick: () => void;
onNewFileClick: () => void;
onPasteClick: () => void;
canPaste?: boolean;
}>
> = ({ children, onClick, ...props }) => {
if (!onClick) return <pre {...props}>{children}</pre>;
> = ({
children,
onCopyClick,
onNewFileClick,
onPasteClick,
canPaste,
...props
}) => {
const config = useConfig();

return (
<ScrollArea scrollbars="horizontal">
<pre {...props}>
<RightButton onClick={onClick}>Copy</RightButton>
{config.host === "web" ? (
<RightButton onClick={onCopyClick}>Copy</RightButton>
) : (
<RightButtonGroup direction="column">
<Flex gap="1" justify="end">
<Button variant="surface" size="1" onClick={onNewFileClick}>
New File
</Button>
<Button size="1" variant="surface" onClick={onCopyClick}>
Copy
</Button>
</Flex>
{canPaste && (
<Button variant="surface" size="1" onClick={onPasteClick}>
Paste
</Button>
)}
</RightButtonGroup>
)}
{children}
</pre>
</ScrollArea>
);
};

export const Markdown: React.FC<
Pick<React.ComponentProps<typeof ReactMarkdown>, "children"> & {
onCopyClick?: (str: string) => void;
}
> = ({ children, onCopyClick }) => {
const PreTagWithoutButtons: React.FC<React.PropsWithChildren> = (props) => {
return (
<ScrollArea scrollbars="horizontal">
<pre {...props} />
</ScrollArea>
);
};

type MarkdownWithControls = {
onCopyClick?: (str: string) => void;
onNewFileClick?: (str: string) => void;
onPasteClick?: (str: string) => void;
canPaste?: boolean;
};

export type MarkdownProps = Pick<
React.ComponentProps<typeof ReactMarkdown>,
"children"
> &
MarkdownWithControls;

export const Markdown: React.FC<MarkdownProps> = ({
children,
onCopyClick,
onNewFileClick,
onPasteClick,
canPaste,
}) => {
return (
<ReactMarkdown
remarkPlugins={[remarkBreaks]}
Expand Down Expand Up @@ -58,14 +110,31 @@ export const Markdown: React.FC<
return acc;
}, "");

const PreTag: React.FC<React.PropsWithChildren> = (props) => (
<PreTagWithCopyButton
onClick={() => {
if (renderedText && onCopyClick) onCopyClick(renderedText);
}}
{...props}
/>
);
const PreTag: React.FC<React.PropsWithChildren> = (props) => {
if (!onCopyClick || !onNewFileClick || !onPasteClick)
return <PreTagWithoutButtons {...props} />;
return (
<PreTagWithButtons
canPaste={canPaste}
onCopyClick={() => {
if (renderedText) {
onCopyClick(renderedText);
}
}}
onNewFileClick={() => {
if (renderedText) {
onNewFileClick(renderedText);
}
}}
onPasteClick={() => {
if (renderedText) {
onPasteClick(renderedText);
}
}}
{...props}
/>
);
};

return match ? (
<SyntaxHighlighter
Expand Down
1 change: 1 addition & 0 deletions src/components/Markdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { Markdown } from "./Markdown";
export type { MarkdownProps } from "./Markdown";
28 changes: 25 additions & 3 deletions src/events/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export enum EVENT_NAMES_FROM_CHAT {
OPEN_IN_CHAT_IN_TAB = "open_chat_in_new_tab",
SEND_TO_SIDE_BAR = "chat_send_to_sidebar",
READY = "chat_ready",
// TODO
NEW_FILE = "chat_create_new_file",
PASTE_DIFF = "chat_paste_diff",
}
Expand All @@ -35,7 +34,6 @@ export enum EVENT_NAMES_TO_CHAT {
SET_DISABLE_CHAT = "set_disable_chat",
RECEIVE_FILES = "receive_context_file",
REMOVE_FILES = "remove_context_file",
// TODO
ACTIVE_FILE_INFO = "chat_active_file_info",
TOGGLE_ACTIVE_FILE = "chat_toggle_active_file",
}
Expand Down Expand Up @@ -65,6 +63,30 @@ export function isActionFromChat(action: unknown): action is ActionFromChat {
return Object.values(ALL_EVENT_NAMES).includes(action.type);
}

export interface NewFileFromChat extends ActionFromChat {
type: EVENT_NAMES_FROM_CHAT.NEW_FILE;
payload: {
id: string;
content: string;
};
}
export function isNewFileFromChat(action: unknown): action is NewFileFromChat {
if (!isActionFromChat(action)) return false;
return action.type === EVENT_NAMES_FROM_CHAT.NEW_FILE;
}

export interface PasteDiffFromChat extends ActionFromChat {
type: EVENT_NAMES_FROM_CHAT.PASTE_DIFF;
payload: { id: string; content: string };
}

export function isPasteDiffFromChat(
action: unknown,
): action is PasteDiffFromChat {
if (!isActionFromChat(action)) return false;
return action.type === EVENT_NAMES_FROM_CHAT.PASTE_DIFF;
}

export interface RequestForFileFromChat extends ActionFromChat {
type: EVENT_NAMES_FROM_CHAT.REQUEST_FILES;
payload: { id: string };
Expand Down Expand Up @@ -140,7 +162,7 @@ export function isActionToChat(action: unknown): action is ActionToChat {

export interface ToggleActiveFile extends ActionToChat {
type: EVENT_NAMES_TO_CHAT.TOGGLE_ACTIVE_FILE;
payload: { id: string; attach: boolean };
payload: { id: string; attach_file: boolean };
}

export function isToggleActiveFile(
Expand Down
5 changes: 5 additions & 0 deletions src/features/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {
openChatInNewTab,
sendToSideBar,
sendReadyMessage,
handleNewFileClick,
handlePasteDiffClick,
} = useEventBusForChat();

const maybeSendToSideBar =
Expand Down Expand Up @@ -87,6 +89,9 @@ export const Chat: React.FC<{ style?: React.CSSProperties }> = (props) => {
messages={state.chat.messages}
onRetry={(messages) => sendMessages(messages)}
isWaiting={state.waiting_for_response}
onNewFileClick={handleNewFileClick}
onPasteClick={handlePasteDiffClick}
canPaste={state.active_file.can_paste}
/>

<ChatForm
Expand Down
Loading

0 comments on commit ebeb826

Please sign in to comment.