feat: Add context menu to DevChat
Added a context menu to DevChat that allows users to select code or file and right-click to add it as a context. The context is then added to the list of contexts in the chat panel.
This commit is contained in:
parent
dd3e6a9fea
commit
26fb027c6c
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useState, useEffect, useRef } 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 { Input, Tooltip } from '@mantine/core';
|
||||||
import { List } from '@mantine/core';
|
import { List } from '@mantine/core';
|
||||||
import { ScrollArea } from '@mantine/core';
|
import { ScrollArea } from '@mantine/core';
|
||||||
@ -8,7 +8,7 @@ import { createStyles, keyframes } from '@mantine/core';
|
|||||||
import { ActionIcon } from '@mantine/core';
|
import { ActionIcon } from '@mantine/core';
|
||||||
import { Menu, Button, Text } from '@mantine/core';
|
import { Menu, Button, Text } from '@mantine/core';
|
||||||
import { useElementSize, useListState, useResizeObserver, useViewportSize } from '@mantine/hooks';
|
import { useElementSize, useListState, useResizeObserver, useViewportSize } from '@mantine/hooks';
|
||||||
import { IconAdjustments, IconBulb, IconCheck, IconClick, IconColumnInsertRight, IconCopy, IconEdit, IconFileDiff, IconFolder, IconGitCompare, IconMessageDots, IconMessagePlus, IconPrompt, IconRobot, IconSend, IconSquareRoundedPlus, IconTerminal2, 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 { IconSettings, IconSearch, IconPhoto, IconMessageCircle, IconTrash, IconArrowsLeftRight } from '@tabler/icons-react';
|
||||||
import { Prism } from '@mantine/prism';
|
import { Prism } from '@mantine/prism';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
@ -37,8 +37,9 @@ const chatPanel = () => {
|
|||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollViewport = useRef<HTMLDivElement>(null);
|
const scrollViewport = useRef<HTMLDivElement>(null);
|
||||||
const [messages, messageHandlers] = useListState<{ type: string; message: string; }>([]);
|
const [messages, messageHandlers] = useListState<{ type: string; message: string; }>([]);
|
||||||
const [commands, commandHandlers] = useListState<{ pattern: string; description: string; name: string }>([]);
|
const [commandMenus, commandMenusHandlers] = useListState<{ pattern: string; description: string; name: string }>([]);
|
||||||
const [contexts, contextHandlers] = 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 [currentMessage, setCurrentMessage] = useState('');
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
const [responsed, setResponsed] = useState(false);
|
const [responsed, setResponsed] = useState(false);
|
||||||
@ -76,6 +77,15 @@ const chatPanel = () => {
|
|||||||
setCurrentMessage('');
|
setCurrentMessage('');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleContextClick = (contextName: string) => {
|
||||||
|
// Process and send the message to the extension
|
||||||
|
messageUtil.sendMessage({
|
||||||
|
command: 'addContext',
|
||||||
|
selected: contextName
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const scrollToBottom = () =>
|
const scrollToBottom = () =>
|
||||||
scrollViewport?.current?.scrollTo({ top: scrollViewport.current.scrollHeight, behavior: 'smooth' });
|
scrollViewport?.current?.scrollTo({ top: scrollViewport.current.scrollHeight, behavior: 'smooth' });
|
||||||
|
|
||||||
@ -118,12 +128,38 @@ const chatPanel = () => {
|
|||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
messageUtil.registerHandler('regCommandList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
messageUtil.registerHandler('regCommandList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
||||||
commandHandlers.append(...message.result);
|
commandMenusHandlers.append(...message.result);
|
||||||
console.log(`commands:${commands}`);
|
|
||||||
});
|
});
|
||||||
messageUtil.registerHandler('regContextList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
messageUtil.registerHandler('regContextList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
||||||
contextHandlers.append(...message.result);
|
contextMenusHandlers.append(...message.result);
|
||||||
console.log(`contexts:${commands}`);
|
});
|
||||||
|
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(context);
|
||||||
|
console.log(context);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [registed]);
|
}, [registed]);
|
||||||
|
|
||||||
@ -149,7 +185,7 @@ const chatPanel = () => {
|
|||||||
<Text size="lg" color="gray" weight={500}>No messages yet</Text>
|
<Text size="lg" color="gray" weight={500}>No messages yet</Text>
|
||||||
</Center>);
|
</Center>);
|
||||||
|
|
||||||
const commandMenus = commands.map(({ pattern, description, name }, index) => {
|
const commandMenusNode = commandMenus.map(({ pattern, description, name }, index) => {
|
||||||
return (
|
return (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={() => { setInput(`/${pattern} `); }}
|
onClick={() => { setInput(`/${pattern} `); }}
|
||||||
@ -170,17 +206,19 @@ const chatPanel = () => {
|
|||||||
</Menu.Item>);
|
</Menu.Item>);
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextMenus = contexts.map(({ pattern, description, name }, index) => {
|
const contextMenusNode = contextMenus.map(({ pattern, description, name }, index) => {
|
||||||
return (
|
return (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={() => { setInput(`/${name} `); }}
|
onClick={() => {
|
||||||
|
handleContextClick(name);
|
||||||
|
}}
|
||||||
icon={<IconMessagePlus size={16} />}
|
icon={<IconMessagePlus size={16} />}
|
||||||
>
|
>
|
||||||
<Text sx={{
|
<Text sx={{
|
||||||
fontSize: 'sm',
|
fontSize: 'sm',
|
||||||
fontWeight: 'bolder',
|
fontWeight: 'bolder',
|
||||||
}}>
|
}}>
|
||||||
/{name}
|
{name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text sx={{
|
<Text sx={{
|
||||||
fontSize: 'sm',
|
fontSize: 'sm',
|
||||||
@ -191,7 +229,6 @@ const chatPanel = () => {
|
|||||||
</Menu.Item>);
|
</Menu.Item>);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const messageList = messages.map(({ message: messageText, type: messageType }, index) => {
|
const messageList = messages.map(({ message: messageText, type: messageType }, index) => {
|
||||||
// setMessage(messageText);
|
// setMessage(messageText);
|
||||||
return (<>
|
return (<>
|
||||||
@ -268,11 +305,16 @@ const chatPanel = () => {
|
|||||||
<IconFileDiff size="1.125rem" />
|
<IconFileDiff size="1.125rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label='Insert' withArrow position="left" color="gray">
|
<Tooltip label='Insert Code' withArrow position="left" color="gray">
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
<IconColumnInsertRight size="1.125rem" />
|
<IconColumnInsertRight size="1.125rem" />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<Tooltip label='Replace' withArrow position="left" color="gray">
|
||||||
|
<ActionIcon>
|
||||||
|
<IconReplace size="1.125rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
<SyntaxHighlighter {...props} language={match[1]} customStyle={{ padding: '2em 1em 1em 2em' }} style={okaidia} PreTag="div">
|
<SyntaxHighlighter {...props} language={match[1]} customStyle={{ padding: '2em 1em 1em 2em' }} style={okaidia} PreTag="div">
|
||||||
{value}
|
{value}
|
||||||
@ -317,69 +359,103 @@ const chatPanel = () => {
|
|||||||
viewportRef={scrollViewport}>
|
viewportRef={scrollViewport}>
|
||||||
{messageList.length > 0 ? messageList : defaultMessages}
|
{messageList.length > 0 ? messageList : defaultMessages}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<Menu
|
<Stack sx={{ position: 'absolute', bottom: 10, width: scrollViewport.current?.clientWidth }}>
|
||||||
id='commandMenu'
|
<Accordion variant="contained" chevronPosition="left" style={{ backgroundColor: '#FFF' }}>
|
||||||
position='top-start'
|
{
|
||||||
closeOnClickOutside={true}
|
contexts.map((context, index) => {
|
||||||
shadow="xs"
|
return (
|
||||||
width={scrollViewport.current?.clientWidth}
|
<Accordion.Item value={`item-${index}`} mah='200'>
|
||||||
opened={menuOpend}
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
onChange={setMenuOpend}
|
<Accordion.Control >
|
||||||
onClose={() => setMenuType('')}
|
{'command' in context ? context.command : context.path}
|
||||||
onOpen={() => menuType !== '' ? setMenuOpend(true) : setMenuOpend(false)}
|
</Accordion.Control>
|
||||||
returnFocus={true}>
|
<ActionIcon
|
||||||
<Menu.Target>
|
mr={8}
|
||||||
<Textarea
|
size="lg"
|
||||||
id='chat-textarea'
|
onClick={() => {
|
||||||
disabled={generating}
|
contextsHandlers.remove(index);
|
||||||
value={input}
|
}}>
|
||||||
ref={inputRef}
|
<IconX size="1rem" />
|
||||||
onKeyDown={handleKeyDown}
|
</ActionIcon>
|
||||||
onChange={handleInputChange}
|
</Box>
|
||||||
autosize
|
<Accordion.Panel>
|
||||||
minRows={1}
|
{
|
||||||
maxRows={10}
|
context.content
|
||||||
radius="md"
|
? context.content
|
||||||
size="md"
|
: <Center>
|
||||||
sx={{ pointerEvents: 'all', position: 'absolute', bottom: 10, width: scrollViewport.current?.clientWidth }}
|
<Text c='gray.3'>No content</Text>
|
||||||
placeholder="Ctrl + Enter Send a message."
|
</Center>
|
||||||
styles={{ icon: { alignItems: 'flex-start', paddingTop: '9px' }, rightSection: { alignItems: 'flex-start', paddingTop: '9px' } }}
|
}
|
||||||
icon={
|
|
||||||
<ActionIcon onClick={handlePlusClick} sx={{ pointerEvents: 'all' }}>
|
|
||||||
<IconSquareRoundedPlus size="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
}
|
|
||||||
rightSection={
|
|
||||||
<ActionIcon onClick={handleSendClick}>
|
|
||||||
<IconSend size="1rem" />
|
|
||||||
</ActionIcon>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Menu.Target>
|
|
||||||
|
|
||||||
{
|
</Accordion.Panel>
|
||||||
menuType === 'contexts'
|
</Accordion.Item>
|
||||||
? (<Menu.Dropdown>
|
);
|
||||||
<Text
|
})
|
||||||
c="dimmed"
|
}
|
||||||
ta="left"
|
</Accordion>
|
||||||
fz='sm'
|
<Menu
|
||||||
m='12px'>
|
id='commandMenu'
|
||||||
<IconBulb size={14} style={{ marginTop: '2px', marginRight: '2px' }} />
|
position='top-start'
|
||||||
Tips: Select code or file & right click
|
closeOnClickOutside={true}
|
||||||
</Text>
|
shadow="xs"
|
||||||
<Divider />
|
width={scrollViewport.current?.clientWidth}
|
||||||
<Menu.Label>DevChat Contexts</Menu.Label>
|
opened={menuOpend}
|
||||||
{contextMenus}
|
onChange={setMenuOpend}
|
||||||
</Menu.Dropdown>)
|
onClose={() => setMenuType('')}
|
||||||
: menuType === 'commands'
|
onOpen={() => menuType !== '' ? setMenuOpend(true) : setMenuOpend(false)}
|
||||||
? <Menu.Dropdown>
|
returnFocus={true}>
|
||||||
<Menu.Label>DevChat Commands</Menu.Label>
|
<Menu.Target>
|
||||||
{commandMenus}
|
<Textarea
|
||||||
</Menu.Dropdown>
|
id='chat-textarea'
|
||||||
: <></>
|
disabled={generating}
|
||||||
}
|
value={input}
|
||||||
</Menu>
|
ref={inputRef}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autosize
|
||||||
|
minRows={1}
|
||||||
|
maxRows={10}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
sx={{ pointerEvents: 'all' }}
|
||||||
|
placeholder="Ctrl + Enter Send a message."
|
||||||
|
styles={{ icon: { alignItems: 'flex-start', paddingTop: '9px' }, rightSection: { alignItems: 'flex-start', paddingTop: '9px' } }}
|
||||||
|
icon={
|
||||||
|
<ActionIcon onClick={handlePlusClick} sx={{ pointerEvents: 'all' }}>
|
||||||
|
<IconSquareRoundedPlus size="1rem" />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
rightSection={
|
||||||
|
<ActionIcon onClick={handleSendClick}>
|
||||||
|
<IconSend size="1rem" />
|
||||||
|
</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 >
|
</Container >
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user