commit
c43753bc67
@ -32,7 +32,7 @@ class MessageUtil {
|
||||
}
|
||||
|
||||
// Register a message handler for a specific message type
|
||||
registerHandler(messageType: string, handler: { (message: { text: string; }): void; (message: { text: string; }): void; }) {
|
||||
registerHandler(messageType: string, handler: any) {
|
||||
if (!this.handlers[messageType]) {
|
||||
this.handlers[messageType] = [];
|
||||
}
|
||||
@ -57,7 +57,7 @@ class MessageUtil {
|
||||
}
|
||||
|
||||
// Send a message to the VSCode API
|
||||
sendMessage(message: { command: string; text: string; }) {
|
||||
sendMessage(message: any) {
|
||||
vscodeApi.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Avatar, Center, Container, CopyButton, Divider, Flex, Grid, Stack, Textarea, TypographyStylesProvider, px, rem, useMantineTheme } from '@mantine/core';
|
||||
import { Accordion, AccordionControlProps, Avatar, Box, Center, Container, CopyButton, Divider, Flex, Grid, Stack, Textarea, TypographyStylesProvider, px, rem, useMantineTheme } from '@mantine/core';
|
||||
import { Input, Tooltip } from '@mantine/core';
|
||||
import { List } from '@mantine/core';
|
||||
import { ScrollArea } from '@mantine/core';
|
||||
@ -8,7 +8,7 @@ import { createStyles, keyframes } from '@mantine/core';
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { Menu, Button, Text } from '@mantine/core';
|
||||
import { useElementSize, useListState, useResizeObserver, useViewportSize } from '@mantine/hooks';
|
||||
import { IconCheck, IconClick, IconCopy, IconEdit, IconFolder, IconGitCompare, IconMessageDots, IconRobot, IconSend, IconSquareRoundedPlus, IconUser } from '@tabler/icons-react';
|
||||
import { IconAdjustments, IconBulb, IconCameraSelfie, IconCheck, IconClick, IconColumnInsertRight, IconCopy, IconDots, IconEdit, IconFileDiff, IconFolder, IconGitCompare, IconMessageDots, IconMessagePlus, IconPrinter, IconPrompt, IconReplace, IconRobot, IconSend, IconSquareRoundedPlus, IconTerminal2, IconUser, IconX } from '@tabler/icons-react';
|
||||
import { IconSettings, IconSearch, IconPhoto, IconMessageCircle, IconTrash, IconArrowsLeftRight } from '@tabler/icons-react';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
@ -22,34 +22,8 @@ const blink = keyframes({
|
||||
});
|
||||
|
||||
const useStyles = createStyles((theme, _params, classNames) => ({
|
||||
plusMenu: {
|
||||
top: 'unset !important',
|
||||
left: '31px !important',
|
||||
bottom: 60,
|
||||
},
|
||||
commandMenu: {
|
||||
top: 'unset !important',
|
||||
left: '31px !important',
|
||||
bottom: 60,
|
||||
},
|
||||
commandText: {
|
||||
fontSize: '1.0rem',
|
||||
fontWeight: 'bolder',
|
||||
},
|
||||
commandDesc: {
|
||||
fontSize: '0.8rem',
|
||||
color: theme.colors.gray[6],
|
||||
},
|
||||
responseContent: {
|
||||
marginTop: 0,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
width: 'calc(100% - 62px)',
|
||||
},
|
||||
icon: {
|
||||
pointerEvents: 'all',
|
||||
menu: {
|
||||
|
||||
},
|
||||
avatar: {
|
||||
marginTop: 8,
|
||||
@ -62,38 +36,46 @@ const chatPanel = () => {
|
||||
const theme = useMantineTheme();
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollViewport = useRef<HTMLDivElement>(null);
|
||||
const [messages, handlers] = useListState<{ type: string; message: string; }>([]);
|
||||
const [messages, messageHandlers] = useListState<{ type: string; message: string; contexts?: any[] }>([]);
|
||||
const [commandMenus, commandMenusHandlers] = useListState<{ pattern: string; description: string; name: string }>([]);
|
||||
const [contextMenus, contextMenusHandlers] = useListState<{ pattern: string; description: string; name: string }>([]);
|
||||
const [contexts, contextsHandlers] = useListState<any>([]);
|
||||
const [currentMessage, setCurrentMessage] = useState('');
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [responsed, setResponsed] = useState(false);
|
||||
const [registed, setRegisted] = useState(false);
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [input, setInput] = useState('');
|
||||
const [commandOpened, setCommandOpened] = useState(false);
|
||||
const [menuOpend, setMenuOpend] = useState(false);
|
||||
const [menuType, setMenuType] = useState(''); // contexts or commands
|
||||
const { classes } = useStyles();
|
||||
const { height, width } = useViewportSize();
|
||||
const [inputRef, inputRect] = useResizeObserver();
|
||||
|
||||
const handlePlusClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setOpened(!opened);
|
||||
setMenuType('contexts');
|
||||
setMenuOpend(!menuOpend);
|
||||
event.stopPropagation();
|
||||
};
|
||||
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (opened) { setOpened(false); }
|
||||
};
|
||||
|
||||
const handleSendClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (input) {
|
||||
// Add the user's message to the chat UI
|
||||
handlers.append({ type: 'user', message: input });
|
||||
messageHandlers.append({ type: 'user', message: input, contexts: contexts ? [...contexts].map((item) => ({ ...item })) : undefined });
|
||||
|
||||
// Process and send the message to the extension
|
||||
const contextStrs = contexts.map(({ file, context }, index) => {
|
||||
return `[context|${file}]`;
|
||||
});
|
||||
const text = input + contextStrs.join(' ');
|
||||
console.log(`message text: ${text}`);
|
||||
messageUtil.sendMessage({
|
||||
command: 'sendMessage',
|
||||
text: text
|
||||
});
|
||||
|
||||
// Clear the input field
|
||||
setInput('');
|
||||
|
||||
// Process and send the message to the extension
|
||||
messageUtil.sendMessage({
|
||||
command: 'sendMessage',
|
||||
text: input
|
||||
});
|
||||
contexts.length = 0;
|
||||
|
||||
// start generating
|
||||
setGenerating(true);
|
||||
@ -101,17 +83,28 @@ const chatPanel = () => {
|
||||
setCurrentMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextClick = (contextName: string) => {
|
||||
// Process and send the message to the extension
|
||||
messageUtil.sendMessage({
|
||||
command: 'addContext',
|
||||
selected: contextName
|
||||
});
|
||||
};
|
||||
|
||||
const scrollToBottom = () =>
|
||||
scrollViewport?.current?.scrollTo({ top: scrollViewport.current.scrollHeight, behavior: 'smooth' });
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current.focus();
|
||||
messageUtil.sendMessage({ command: 'regContextList' });
|
||||
messageUtil.sendMessage({ command: 'regCommandList' });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (generating) {
|
||||
// new a bot message
|
||||
handlers.append({ type: 'bot', message: currentMessage });
|
||||
messageHandlers.append({ type: 'bot', message: currentMessage });
|
||||
}
|
||||
}, [generating]);
|
||||
|
||||
@ -121,7 +114,7 @@ const chatPanel = () => {
|
||||
const lastMessage = messages[lastIndex];
|
||||
if (currentMessage && lastMessage?.type === 'bot') {
|
||||
// update the last one bot message
|
||||
handlers.setItem(lastIndex, { type: 'bot', message: currentMessage });
|
||||
messageHandlers.setItem(lastIndex, { type: 'bot', message: currentMessage });
|
||||
}
|
||||
}, [currentMessage]);
|
||||
|
||||
@ -140,15 +133,53 @@ const chatPanel = () => {
|
||||
setResponsed(true);
|
||||
scrollToBottom();
|
||||
});
|
||||
messageUtil.registerHandler('regCommandList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
||||
commandMenusHandlers.append(...message.result);
|
||||
});
|
||||
messageUtil.registerHandler('regContextList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
||||
contextMenusHandlers.append(...message.result);
|
||||
});
|
||||
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
|
||||
messageUtil.sendMessage({
|
||||
command: 'contextDetail',
|
||||
file: match && match[1],
|
||||
});
|
||||
});
|
||||
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) {
|
||||
contextsHandlers.append({
|
||||
file: message.file,
|
||||
context: context,
|
||||
});
|
||||
console.log(context);
|
||||
}
|
||||
});
|
||||
}, [registed]);
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = event.target.value;
|
||||
// if value start with '/' command show menu
|
||||
if (value === '/') {
|
||||
setCommandOpened(true);
|
||||
setMenuOpend(true);
|
||||
setMenuType('commands');
|
||||
} else {
|
||||
setCommandOpened(false);
|
||||
setMenuOpend(false);
|
||||
}
|
||||
setInput(value);
|
||||
};
|
||||
@ -163,12 +194,57 @@ const chatPanel = () => {
|
||||
<Text size="lg" color="gray" weight={500}>No messages yet</Text>
|
||||
</Center>);
|
||||
|
||||
const messageList = messages.map(({ message: messageText, type: messageType }, index) => {
|
||||
const commandMenusNode = commandMenus.map(({ pattern, description, name }, index) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => { setInput(`/${pattern} `); }}
|
||||
icon={<IconTerminal2 size={16} />}
|
||||
>
|
||||
<Text sx={{
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bolder',
|
||||
}}>
|
||||
/{pattern}
|
||||
</Text>
|
||||
<Text sx={{
|
||||
fontSize: 'sm',
|
||||
color: theme.colors.gray[6],
|
||||
}}>
|
||||
{description}
|
||||
</Text>
|
||||
</Menu.Item>);
|
||||
});
|
||||
|
||||
const contextMenusNode = contextMenus.map(({ pattern, description, name }, index) => {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
handleContextClick(name);
|
||||
}}
|
||||
icon={<IconMessagePlus size={16} />}
|
||||
>
|
||||
<Text sx={{
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'bolder',
|
||||
}}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text sx={{
|
||||
fontSize: 'sm',
|
||||
color: theme.colors.gray[6],
|
||||
}}>
|
||||
{description}
|
||||
</Text>
|
||||
</Menu.Item>);
|
||||
});
|
||||
|
||||
const messageList = messages.map(({ message: messageText, type: messageType, contexts }, index) => {
|
||||
// setMessage(messageText);
|
||||
return (<>
|
||||
<Flex
|
||||
key={`message-${index}`}
|
||||
mih={50}
|
||||
miw={300}
|
||||
gap="md"
|
||||
justify="flex-start"
|
||||
align="flex-start"
|
||||
@ -181,7 +257,44 @@ const chatPanel = () => {
|
||||
: <Avatar color="cyan" size='md' radius="xl" className={classes.avatar}><IconUser size="1.5rem" /></Avatar>
|
||||
}
|
||||
|
||||
<Container className={classes.responseContent}>
|
||||
<Container sx={{
|
||||
marginTop: 0,
|
||||
marginLeft: 0,
|
||||
marginRight: 0,
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
width: 'calc(100% - 62px)',
|
||||
pre: {
|
||||
whiteSpace: 'break-spaces'
|
||||
}
|
||||
}}>
|
||||
{contexts &&
|
||||
<Accordion variant="contained" chevronPosition="left" style={{ backgroundColor: '#FFF' }}>
|
||||
{
|
||||
contexts?.map(({ context }, index) => {
|
||||
return (
|
||||
<Accordion.Item value={`item-${index}`} mah='200'>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Accordion.Control >
|
||||
{'command' in context ? context.command : context.path}
|
||||
</Accordion.Control>
|
||||
</Box>
|
||||
<Accordion.Panel>
|
||||
{
|
||||
context.content
|
||||
? context.content
|
||||
: <Center>
|
||||
<Text c='gray.3'>No content</Text>
|
||||
</Center>
|
||||
}
|
||||
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Accordion>
|
||||
}
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
@ -211,17 +324,38 @@ const chatPanel = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ position: 'absolute', top: 3, right: 5 }}>
|
||||
<Flex
|
||||
gap="5px"
|
||||
justify="flex-start"
|
||||
align="flex-start"
|
||||
direction="row"
|
||||
wrap="wrap"
|
||||
style={{ position: 'absolute', top: 8, right: 10 }}>
|
||||
<CopyButton value={value} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip label={copied ? 'Copied' : 'Copy'} withArrow position="right">
|
||||
<Tooltip label={copied ? 'Copied' : 'Copy'} withArrow position="left" color="gray">
|
||||
<ActionIcon color={copied ? 'teal' : 'gray'} onClick={copy}>
|
||||
{copied ? <IconCheck size="1rem" /> : <IconCopy size="1rem" />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
</div>
|
||||
<Tooltip label='View-diff' withArrow position="left" color="gray">
|
||||
<ActionIcon>
|
||||
<IconFileDiff size="1.125rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Insert Code' withArrow position="left" color="gray">
|
||||
<ActionIcon>
|
||||
<IconColumnInsertRight size="1.125rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Replace' withArrow position="left" color="gray">
|
||||
<ActionIcon>
|
||||
<IconReplace size="1.125rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<SyntaxHighlighter {...props} language={match[1]} customStyle={{ padding: '2em 1em 1em 2em' }} style={okaidia} PreTag="div">
|
||||
{value}
|
||||
</SyntaxHighlighter>
|
||||
@ -243,28 +377,79 @@ const chatPanel = () => {
|
||||
backgroundColor: 'black'
|
||||
}}>|</Text> : ''}
|
||||
</Container>
|
||||
</Flex>
|
||||
{index !== messages.length - 1 && <Divider my="sm" />}
|
||||
</Flex >
|
||||
{index !== messages.length - 1 && <Divider my="sm" />
|
||||
}
|
||||
</>);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container
|
||||
id='chat-container'
|
||||
ref={chatContainerRef}
|
||||
sx={{
|
||||
height: '100%',
|
||||
paddingTop: 10,
|
||||
backgroundColor: theme.colors.gray[0],
|
||||
}}
|
||||
onClick={handleContainerClick}>
|
||||
}}>
|
||||
<ScrollArea
|
||||
id='chat-scroll-area'
|
||||
h={height - px('5rem')}
|
||||
type="never"
|
||||
viewportRef={scrollViewport}>
|
||||
{messageList.length > 0 ? messageList : defaultMessages}
|
||||
</ScrollArea>
|
||||
<Stack
|
||||
sx={{ position: 'absolute', bottom: 10, width: scrollViewport.current?.clientWidth }}>
|
||||
{contexts &&
|
||||
<Accordion variant="contained" chevronPosition="left" style={{ backgroundColor: '#FFF' }}>
|
||||
{
|
||||
contexts.map(({ context }, index) => {
|
||||
return (
|
||||
<Accordion.Item value={`item-${index}`} mah='200'>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Accordion.Control >
|
||||
{'command' in context ? context.command : context.path}
|
||||
</Accordion.Control>
|
||||
<ActionIcon
|
||||
mr={8}
|
||||
size="lg"
|
||||
onClick={() => {
|
||||
contextsHandlers.remove(index);
|
||||
}}>
|
||||
<IconX size="1rem" />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
<Accordion.Panel>
|
||||
{
|
||||
context.content
|
||||
? context.content
|
||||
: <Center>
|
||||
<Text c='gray.3'>No content</Text>
|
||||
</Center>
|
||||
}
|
||||
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Accordion>
|
||||
}
|
||||
<Menu
|
||||
id='commandMenu'
|
||||
position='top-start'
|
||||
closeOnClickOutside={true}
|
||||
shadow="xs"
|
||||
width={scrollViewport.current?.clientWidth}
|
||||
opened={menuOpend}
|
||||
onChange={setMenuOpend}
|
||||
onClose={() => setMenuType('')}
|
||||
onOpen={() => menuType !== '' ? setMenuOpend(true) : setMenuOpend(false)}
|
||||
returnFocus={true}>
|
||||
<Menu.Target>
|
||||
<Textarea
|
||||
id='chat-textarea'
|
||||
disabled={generating}
|
||||
value={input}
|
||||
ref={inputRef}
|
||||
@ -275,7 +460,7 @@ const chatPanel = () => {
|
||||
maxRows={10}
|
||||
radius="md"
|
||||
size="md"
|
||||
sx={{ pointerEvents: 'all', position: 'absolute', bottom: 10, width: scrollViewport.current?.clientWidth }}
|
||||
sx={{ pointerEvents: 'all' }}
|
||||
placeholder="Ctrl + Enter Send a message."
|
||||
styles={{ icon: { alignItems: 'flex-start', paddingTop: '9px' }, rightSection: { alignItems: 'flex-start', paddingTop: '9px' } }}
|
||||
icon={
|
||||
@ -289,67 +474,32 @@ const chatPanel = () => {
|
||||
</ActionIcon>
|
||||
}
|
||||
/>
|
||||
</Menu.Target>
|
||||
{
|
||||
menuType === 'contexts'
|
||||
? (<Menu.Dropdown>
|
||||
<Text
|
||||
c="dimmed"
|
||||
ta="left"
|
||||
fz='sm'
|
||||
m='12px'>
|
||||
<IconBulb size={14} style={{ marginTop: '2px', marginRight: '2px' }} />
|
||||
Tips: Select code or file & right click
|
||||
</Text>
|
||||
<Divider />
|
||||
<Menu.Label>DevChat Contexts</Menu.Label>
|
||||
{contextMenusNode}
|
||||
</Menu.Dropdown>)
|
||||
: menuType === 'commands'
|
||||
? <Menu.Dropdown>
|
||||
<Menu.Label>DevChat Commands</Menu.Label>
|
||||
{commandMenusNode}
|
||||
</Menu.Dropdown>
|
||||
: <></>
|
||||
}
|
||||
</Menu>
|
||||
</Stack>
|
||||
</Container >
|
||||
<Menu id='plusMenu' shadow="md" width={300} opened={opened} onChange={setOpened} >
|
||||
<Menu.Dropdown className={classes.plusMenu}>
|
||||
<Menu.Item icon={<IconClick size={14} />}>Select code or file & right click</Menu.Item>
|
||||
<Menu.Item icon={<IconGitCompare size={14} />}>Add `git diff --cached`</Menu.Item>
|
||||
<Menu.Item icon={<IconGitCompare size={14} />}>Add `git diff HEAD`</Menu.Item>
|
||||
<Menu.Item icon={<IconFolder size={14} />}>Add folder structure</Menu.Item>
|
||||
<Menu.Item icon={<IconMessageDots size={14} />}>Select previous chat</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<Menu id='commandMenu' shadow="md" width={500} opened={commandOpened} onChange={setCommandOpened} returnFocus={true}>
|
||||
<Menu.Dropdown className={classes.commandMenu}>
|
||||
<Menu.Label>Context Commands</Menu.Label>
|
||||
<Menu.Item onClick={() => { setInput('/ref ') }}>
|
||||
<Text className={classes.commandText}>
|
||||
/ref
|
||||
</Text>
|
||||
<Text className={classes.commandDesc}>
|
||||
Run a local CLI and add its output to the context (e.g., pytest .).
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => { setInput('/local ') }}>
|
||||
<Text className={classes.commandText}>
|
||||
/local
|
||||
</Text>
|
||||
<Text className={classes.commandDesc}>
|
||||
Bypass AI and run a local CLI to check its output (e.g., git status).
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Label>DevChat Bots</Menu.Label>
|
||||
|
||||
<Menu.Item onClick={() => { setInput('/code ') }}>
|
||||
<Text className={classes.commandText}>
|
||||
/code
|
||||
</Text>
|
||||
<Text className={classes.commandDesc}>
|
||||
Generate or update code.
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => { setInput('/commit_message ') }}>
|
||||
<Text className={classes.commandText}>
|
||||
/commit_message
|
||||
</Text>
|
||||
<Text className={classes.commandDesc}>
|
||||
Write a commit message.
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={() => { setInput('/doc ') }}>
|
||||
<Text className={classes.commandText}>
|
||||
/doc
|
||||
</Text>
|
||||
<Text className={classes.commandDesc}>
|
||||
Write a doc for reference, wiki, or discussion.
|
||||
</Text>
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user