541 lines
16 KiB
TypeScript
Raw Normal View History

2023-12-12 18:03:17 +08:00
import {
useMantineTheme,
Flex,
Stack,
ActionIcon,
ScrollArea,
Popover,
Textarea,
Text,
Indicator,
Drawer,
Group,
Button,
Menu,
createStyles,
} from "@mantine/core";
2023-12-12 15:13:17 +08:00
import { useDisclosure, useResizeObserver } from "@mantine/hooks";
2023-12-12 18:03:17 +08:00
import {
IconSend,
IconPaperclip,
IconChevronDown,
IconRobot,
} from "@tabler/icons-react";
import React, { useState, useEffect, useRef } from "react";
2023-12-12 18:03:17 +08:00
import messageUtil from "@/util/MessageUtil";
import InputContexts from "./InputContexts";
import Topic from "./Topic";
import AddTopic from "./AddTopic";
2023-12-12 15:13:17 +08:00
import { observer } from "mobx-react-lite";
import { useMst } from "@/views/stores/RootStore";
import { ChatContext } from "@/views/stores/InputStore";
2023-12-21 16:40:23 +08:00
import { Trans, useTranslation } from "react-i18next";
import getModelShowName from "@/util/getModelShowName";
2023-12-12 15:13:17 +08:00
const useStyles = createStyles(() => ({
2023-12-12 18:03:17 +08:00
actionIcon: {
color: "var(--vscode-dropdown-foreground)",
borderColor: "var(--vscode-dropdown-border)",
backgroundColor: "var(--vscode-dropdown-background)",
"&:hover": {
color: "var(--vscode-dropdown-foreground)",
borderColor: "var(--vscode-dropdown-border)",
backgroundColor: "var(--vscode-dropdown-background)",
},
"&[data-disabled]": {
borderColor: "transparent",
backgroundColor: "#e9ecef",
color: "#adb5bd",
cursor: "not-allowed",
backgroundImage: "none",
pointervents: "none",
},
},
}));
2023-12-12 15:13:17 +08:00
const InputMessage = observer((props: any) => {
2023-12-12 18:03:17 +08:00
const { classes } = useStyles();
const { input, chat, config } = useMst();
const { t } = useTranslation();
2023-12-12 18:03:17 +08:00
const {
contexts,
menuOpend,
menuType,
currentMenuIndex,
contextMenus,
commandMenus
2023-12-12 18:03:17 +08:00
} = input;
const { generating } = chat;
const viewport = useRef<HTMLDivElement>(null);
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
const [drawerOpened, { open: openDrawer, close: closeDrawer }] =
useDisclosure(false);
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
const theme = useMantineTheme();
const [commandMenusNode, setCommandMenusNode] = useState<any>(null);
const [inputRef, inputRect] = useResizeObserver();
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = event.target.value;
// if value start with '/' command show menu
if (value.startsWith("/")) {
input.openMenu("commands");
input.setCurrentMenuIndex(0);
} else {
input.closeMenu();
}
input.setValue(value);
};
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
const handleSendClick = (event: React.MouseEvent<HTMLButtonElement>) => {
const inputValue = input.value;
if (inputValue) {
if (inputValue.trim() === "/help") {
chat.helpMessage();
} else {
const text = inputValue;
const chatContexts = contexts
? [...contexts].map((item) => ({ ...item }))
: undefined;
if (inputValue.trim().startsWith("/ask-code")) {
chat.devchatAsk(text, chatContexts);
2023-12-12 15:13:17 +08:00
} else {
2023-12-12 18:03:17 +08:00
chat.commonMessage(text, chatContexts);
2023-12-12 15:13:17 +08:00
}
2023-12-12 18:03:17 +08:00
}
// Clear the input field
input.setValue("");
input.clearContexts();
// event.preventDefault();
2023-12-12 18:03:17 +08:00
}
};
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
const handleContextClick = (contextName: string) => {
// Process and send the message to the extension
messageUtil.sendMessage({
command: "addContext",
selected: contextName,
});
};
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (menuOpend) {
if (event.key === "Escape") {
input.closeMenu();
}
if (menuType === "commands") {
if (event.key === "ArrowDown") {
event.preventDefault();
2023-12-12 18:03:17 +08:00
const newIndex = currentMenuIndex + 1;
input.setCurrentMenuIndex(
newIndex < commandMenusNode.length ? newIndex : 0
);
viewport.current
2024-01-15 17:31:57 +08:00
?.querySelectorAll("[data-list-item]")
?.[newIndex]?.scrollIntoView({ block: "nearest" });
2023-12-12 15:13:17 +08:00
}
2023-12-12 18:03:17 +08:00
if (event.key === "ArrowUp") {
event.preventDefault();
2023-12-12 18:03:17 +08:00
const newIndex = currentMenuIndex - 1;
input.setCurrentMenuIndex(
newIndex < 0 ? commandMenusNode.length - 1 : newIndex
);
viewport.current
2024-01-15 17:31:57 +08:00
?.querySelectorAll("[data-list-item]")
?.[newIndex]?.scrollIntoView({ block: "nearest" });
2023-12-12 15:13:17 +08:00
}
2023-12-12 18:03:17 +08:00
if ((event.key === "Enter" || event.key === "Tab") && !event.shiftKey) {
const commandNode = commandMenusNode[currentMenuIndex];
const commandPattern = commandNode.props["data-pattern"];
if (commandPattern === "help") {
chat.helpMessage();
input.setValue("");
} else {
input.setValue(`/${commandPattern} `);
}
input.closeMenu();
event.preventDefault();
2023-12-12 15:13:17 +08:00
}
2023-12-12 18:03:17 +08:00
}
} else {
if (
event.key === "Enter" &&
!event.shiftKey &&
!event.nativeEvent.isComposing
) {
handleSendClick(event as any);
}
}
};
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
useEffect(() => {
messageUtil.registerHandler(
"chatWithDevChat",
(message: { command: string; message: string }) => {
chat.commonMessage(message.message, []);
}
);
messageUtil.registerHandler(
"focusDevChatInput",
(message: { command: string; message: string }) => {
inputRef.current.focus();
}
);
2023-12-12 18:03:17 +08:00
messageUtil.registerHandler(
"appendContext",
(message: { command: string; context: string }) => {
// context is a temp file path
const match = /\|([^]+?)\]/.exec(message.context);
// Process and send the message to the extension
2023-12-12 15:13:17 +08:00
messageUtil.sendMessage({
2023-12-12 18:03:17 +08:00
command: "contextDetail",
file: match && match[1],
2023-12-12 15:13:17 +08:00
});
2023-12-12 18:03:17 +08:00
}
);
messageUtil.registerHandler(
"contextDetailResponse",
(message: { command: string; file: string; result: string }) => {
//result is a content json
// 1. diff json structure
// {
// languageId: languageId,
// path: fileSelected,
// content: codeSelected
// };
// 2. command json structure
// {
// command: commandString,
// content: stdout
// };
const context = JSON.parse(message.result);
if (typeof context !== "undefined" && context) {
const chatContext = ChatContext.create({
file: message.file,
path: context.path,
command: context.command,
content: context.content,
});
input.newContext(chatContext);
2023-12-12 15:13:17 +08:00
}
2023-12-12 18:03:17 +08:00
}
);
inputRef.current.focus();
}, []);
2023-12-12 15:13:17 +08:00
2023-12-12 18:03:17 +08:00
useEffect(() => {
let filtered;
if (input.value) {
filtered = commandMenus.filter((item) =>
`/${item.pattern}`.startsWith(input.value)
);
} else {
filtered = commandMenus;
}
const node = filtered.map(({ pattern, description, name }, index) => {
return (
<Flex
key={`command-menus-${index}`}
data-list-item
2023-12-12 18:03:17 +08:00
mih={40}
gap="md"
justify="flex-start"
align="flex-start"
direction="row"
wrap="wrap"
sx={{
padding: "5px 0",
"&:hover,&[aria-checked=true]": {
cursor: "pointer",
color: "var(--vscode-commandCenter-activeForeground)",
backgroundColor: "var(--vscode-commandCenter-activeBackground)",
},
}}
onClick={() => {
input.setValue(`/${pattern} `);
input.closeMenu();
}}
aria-checked={index === currentMenuIndex}
data-pattern={pattern}
>
<Stack
spacing={0}
2023-12-12 15:13:17 +08:00
sx={{
2023-12-12 18:03:17 +08:00
paddingLeft: 10,
2023-12-12 15:13:17 +08:00
}}
2023-12-12 18:03:17 +08:00
>
<Text
sx={{
fontSize: "sm",
fontWeight: "bolder",
color: "var(--vscode-menu-foreground)",
}}
>
/{pattern}
</Text>
<Text
sx={{
fontSize: "sm",
color: theme.colors.gray[6],
}}
>
2023-12-21 16:40:23 +08:00
<Trans>{description}</Trans>
2023-12-12 18:03:17 +08:00
</Text>
</Stack>
</Flex>
);
});
setCommandMenusNode(node);
if (node.length === 0) {
input.closeMenu();
}
}, [input.value, commandMenus, currentMenuIndex]);
useEffect(() => {
if (drawerOpened && (!contexts || contexts.length === 0)) {
closeDrawer();
}
}, [contexts.length]);
const changeModel = (value) => {
config.setConfigValue("default_model", value);
2023-12-12 18:03:17 +08:00
};
const menuStyles = {
arrow: {
borderColor: "var(--vscode-menu-border)",
},
dropdown: {
borderColor: "var(--vscode-menu-border)",
backgroundColor: "var(--vscode-menu-background)",
},
itemLabel: {
color: "var(--vscode-menu-foreground)",
},
item: {
padding: 5,
backgroundColor: "var(--vscode-menu-background)",
"&:hover,&[data-hovered=true]": {
color: "var(--vscode-commandCenter-activeForeground)",
borderColor: "var(--vscode-commandCenter-border)",
backgroundColor: "var(--vscode-commandCenter-activeBackground)",
},
},
};
const buttonStyles = {
root: {
color: "var(--vscode-dropdown-foreground)",
borderColor: "var(--vscode-dropdown-border)",
backgroundColor: "var(--vscode-dropdown-background)",
"&:hover": {
color: "var(--vscode-dropdown-foreground)",
borderColor: "var(--vscode-dropdown-border)",
backgroundColor: "var(--vscode-dropdown-background)",
},
},
};
// DC.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJvcmdfaWQiOjI1MTYwMzg1MzQ2LCJqdGkiOjczMjc0OTkzMjcwMzc1MjkyNjB9.e-KAl0QM0Ooe6NdLAV3vaJAiA4Zr_DAgqkE0cl7MKZw
2023-12-12 18:03:17 +08:00
return (
<Stack
spacing={0}
sx={{
padding: "0 5px",
}}
>
<Group
spacing={5}
sx={{
marginTop: 5,
}}
>
<Menu
position="bottom-start"
withArrow
shadow="md"
styles={menuStyles}
disabled={config.chatModels.length === 0}
2023-12-12 18:03:17 +08:00
>
<Menu.Target>
<Button
disabled={generating || chat.disabled}
variant="default"
size="xs"
radius="xl"
leftIcon={<IconRobot size="1rem" />}
styles={buttonStyles}
>
{getModelShowName(config.defaultModel)}
2023-12-12 18:03:17 +08:00
</Button>
</Menu.Target>
<Menu.Dropdown>
2024-01-15 17:31:57 +08:00
<ScrollArea.Autosize placeholder="" type="always" mah={240}>
{config.chatModels.map((model) => {
2024-01-15 17:31:57 +08:00
return (
<Menu.Item
key={model.name}
title={model.input_price!== -1? (t("input price:") + `${model.currency !== 'CNY' ? '$' : ''}${model.input_price}`+t("/1M tokens") + "\n" + t("output price:") + `${model.currency !== 'CNY' ? '$' : ''}${model.output_price}`+ t("/1M tokens")) : ""}
onClick={() => changeModel(model.name)}
2024-01-15 17:31:57 +08:00
>
{getModelShowName(model.name)}
2024-01-15 17:31:57 +08:00
</Menu.Item>
);
})}
</ScrollArea.Autosize>
2023-12-12 18:03:17 +08:00
</Menu.Dropdown>
</Menu>
2024-02-04 17:47:03 +08:00
<>
<AddTopic
buttonStyles={buttonStyles}
disabled={generating || chat.disabled}
/>
<Topic
styleName={classes.actionIcon}
disabled={generating || chat.disabled}
/>
</>
2023-12-12 18:03:17 +08:00
</Group>
{contexts && contexts.length > 0 && (
<Drawer
opened={drawerOpened}
onClose={closeDrawer}
position="bottom"
title="DevChat Contexts"
overlayProps={{ opacity: 0.5, blur: 4 }}
closeButtonProps={{ children: <IconChevronDown size="1rem" /> }}
styles={{
content: {
background: "var(--vscode-sideBar-background)",
color: "var(--vscode-editor-foreground)",
},
header: {
background: "var(--vscode-sideBar-background)",
color: "var(--vscode-editor-foreground)",
},
}}
>
<InputContexts />
</Drawer>
)}
<Popover
position="top-start"
shadow="sm"
width={chat.chatPanelWidth - 10}
opened={menuOpend}
onChange={() => {
input.closeMenu();
inputRef.current.focus();
}}
>
<Popover.Target>
<Textarea
id="chat-textarea"
disabled={generating || chat.disabled}
value={input.value}
ref={inputRef}
onKeyDown={handleKeyDown}
onChange={handleInputChange}
autosize
minRows={1}
maxRows={10}
radius="md"
size="xs"
sx={{
pointerEvents: "all",
marginTop: 5,
marginBottom: 5,
}}
placeholder={t("Ask DevChat a question or type / for workflow")}
2023-12-12 18:03:17 +08:00
styles={{
rightSection: {
alignItems: "flex-end",
marginBottom: "6px",
marginRight: contexts.length > 0 ? "24px" : "10px",
},
input: {
fontSize: "var(--vscode-editor-font-size)",
backgroundColor: "var(--vscode-input-background)",
borderColor: "var(--vscode-input-border)",
color: "var(--vscode-input-foreground)",
"&[data-disabled]": {
color: "var(--vscode-disabledForeground)",
},
},
}}
rightSection={
<>
{contexts.length > 0 && (
<Indicator label={contexts.length} size={12}>
<ActionIcon
size="md"
radius="md"
variant="default"
disabled={generating || chat.disabled}
onClick={openDrawer}
className={classes.actionIcon}
sx={{
pointerEvents: "all",
"&[data-disabled]": {
borderColor: "var(--vscode-input-border)",
backgroundColor:
"var(--vscode-toolbar-activeBackground)",
2023-12-12 15:13:17 +08:00
},
2023-12-12 18:03:17 +08:00
}}
>
<IconPaperclip size="1rem" />
</ActionIcon>
</Indicator>
)}
<ActionIcon
size="md"
radius="md"
variant="default"
disabled={generating || chat.disabled}
onClick={handleSendClick}
className={classes.actionIcon}
sx={{
marginLeft: "10px",
pointerEvents: "all",
backgroundColor: "#ED6A45",
border: "0",
color: "#FFFFFF",
"&:hover": {
backgroundColor: "#ED6A45",
color: "#FFFFFF",
opacity: 0.7,
},
}}
>
<IconSend size="1rem" />
</ActionIcon>
</>
2023-12-12 15:13:17 +08:00
}
2023-12-12 18:03:17 +08:00
/>
</Popover.Target>
<Popover.Dropdown
sx={{
padding: 0,
color: "var(--vscode-menu-foreground)",
borderColor: "var(--vscode-menu-border)",
backgroundColor: "var(--vscode-menu-background)",
}}
>
<Text sx={{ padding: "5px 5px 5px 10px" }}>DevChat Workflows</Text>
2024-01-15 17:31:57 +08:00
<ScrollArea.Autosize
mah={240}
type="always"
placeholder=""
viewportRef={viewport}
>
2023-12-12 18:03:17 +08:00
{commandMenusNode}
</ScrollArea.Autosize>
</Popover.Dropdown>
</Popover>
</Stack>
);
2023-12-12 15:13:17 +08:00
});
2023-12-12 18:03:17 +08:00
export default InputMessage;