2023-05-05 01:10:02 +08:00
|
|
|
import * as React from 'react';
|
2023-05-07 01:19:48 +08:00
|
|
|
import { useState, useEffect, useRef } from 'react';
|
2023-05-18 10:31:22 +08:00
|
|
|
import { Accordion, AccordionControlProps, Avatar, Box, Center, Code, Container, CopyButton, Divider, Flex, Grid, Popover, Stack, Textarea, TypographyStylesProvider, px, rem, useMantineTheme } from '@mantine/core';
|
2023-05-05 01:10:02 +08:00
|
|
|
import { Input, Tooltip } from '@mantine/core';
|
|
|
|
import { List } from '@mantine/core';
|
|
|
|
import { ScrollArea } from '@mantine/core';
|
2023-05-07 00:50:03 +08:00
|
|
|
import { createStyles, keyframes } from '@mantine/core';
|
2023-05-05 01:10:02 +08:00
|
|
|
import { ActionIcon } from '@mantine/core';
|
|
|
|
import { Menu, Button, Text } from '@mantine/core';
|
2023-05-18 08:27:04 +08:00
|
|
|
import { useElementSize, useInterval, useListState, useResizeObserver, useTimeout, useViewportSize, useWindowScroll } from '@mantine/hooks';
|
2023-05-11 14:06:06 +08:00
|
|
|
import { IconAdjustments, IconBulb, IconCameraSelfie, IconCheck, IconClick, IconColumnInsertRight, IconCopy, IconDots, IconEdit, IconFileDiff, IconFolder, IconGitCommit, IconGitCompare, IconMessageDots, IconMessagePlus, IconPlayerStop, IconPrinter, IconPrompt, IconReplace, IconRobot, IconSend, IconSquareRoundedPlus, IconTerminal2, IconUser, IconX } from '@tabler/icons-react';
|
2023-05-05 01:10:02 +08:00
|
|
|
import { IconSettings, IconSearch, IconPhoto, IconMessageCircle, IconTrash, IconArrowsLeftRight } from '@tabler/icons-react';
|
2023-05-05 09:48:05 +08:00
|
|
|
import { Prism } from '@mantine/prism';
|
2023-05-07 18:27:01 +08:00
|
|
|
import ReactMarkdown from 'react-markdown';
|
|
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
|
|
import okaidia from 'react-syntax-highlighter/dist/esm/styles/prism/okaidia';
|
2023-05-17 20:41:31 +08:00
|
|
|
import messageUtil from '../../util/MessageUtil';
|
2023-05-18 13:09:19 +08:00
|
|
|
// @ts-ignore
|
|
|
|
import avatarDevChat from './avatar_devchat.svg';
|
|
|
|
// @ts-ignore
|
|
|
|
import avatarUser from './avatar_user.svg';
|
2023-05-07 00:50:03 +08:00
|
|
|
|
|
|
|
const blink = keyframes({
|
|
|
|
'50%': { opacity: 0 },
|
|
|
|
});
|
2023-05-05 01:10:02 +08:00
|
|
|
|
|
|
|
const useStyles = createStyles((theme, _params, classNames) => ({
|
2023-05-09 19:10:46 +08:00
|
|
|
menu: {
|
|
|
|
|
2023-05-05 01:10:02 +08:00
|
|
|
},
|
2023-05-06 00:00:25 +08:00
|
|
|
avatar: {
|
2023-05-18 13:09:19 +08:00
|
|
|
marginTop: 10,
|
|
|
|
marginLeft: 3,
|
2023-05-06 00:00:25 +08:00
|
|
|
},
|
2023-05-05 01:10:02 +08:00
|
|
|
}));
|
|
|
|
|
|
|
|
const chatPanel = () => {
|
|
|
|
|
2023-05-09 12:13:22 +08:00
|
|
|
const theme = useMantineTheme();
|
|
|
|
const chatContainerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const scrollViewport = useRef<HTMLDivElement>(null);
|
2023-05-10 17:38:24 +08:00
|
|
|
const [messages, messageHandlers] = useListState<{ type: string; message: string; contexts?: any[] }>([]);
|
2023-05-10 17:38:24 +08:00
|
|
|
const [commandMenus, commandMenusHandlers] = useListState<{ pattern: string; description: string; name: string }>([]);
|
|
|
|
const [contextMenus, contextMenusHandlers] = useListState<{ pattern: string; description: string; name: string }>([]);
|
2023-05-18 11:34:00 +08:00
|
|
|
const [commandMenusNode, setCommandMenusNode] = useState<any>(null);
|
|
|
|
const [currentMenuIndex, setCurrentMenuIndex] = useState<number>(0);
|
2023-05-10 17:38:24 +08:00
|
|
|
const [contexts, contextsHandlers] = useListState<any>([]);
|
2023-05-09 09:44:51 +08:00
|
|
|
const [currentMessage, setCurrentMessage] = useState('');
|
2023-05-09 10:38:28 +08:00
|
|
|
const [generating, setGenerating] = useState(false);
|
|
|
|
const [responsed, setResponsed] = useState(false);
|
2023-05-07 00:50:03 +08:00
|
|
|
const [registed, setRegisted] = useState(false);
|
2023-05-06 00:32:53 +08:00
|
|
|
const [input, setInput] = useState('');
|
2023-05-09 19:10:46 +08:00
|
|
|
const [menuOpend, setMenuOpend] = useState(false);
|
|
|
|
const [menuType, setMenuType] = useState(''); // contexts or commands
|
2023-05-05 01:10:02 +08:00
|
|
|
const { classes } = useStyles();
|
|
|
|
const { height, width } = useViewportSize();
|
2023-05-09 12:13:22 +08:00
|
|
|
const [inputRef, inputRect] = useResizeObserver();
|
2023-05-18 08:27:04 +08:00
|
|
|
const [scrollPosition, onScrollPositionChange] = useState({ x: 0, y: 0 });
|
|
|
|
const [stopScrolling, setStopScrolling] = useState(false);
|
|
|
|
const messageCount = 10;
|
2023-05-06 00:00:25 +08:00
|
|
|
|
2023-05-07 00:50:03 +08:00
|
|
|
const handlePlusClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
2023-05-09 19:10:46 +08:00
|
|
|
setMenuType('contexts');
|
|
|
|
setMenuOpend(!menuOpend);
|
2023-05-05 01:10:02 +08:00
|
|
|
event.stopPropagation();
|
|
|
|
};
|
2023-05-09 19:10:46 +08:00
|
|
|
|
2023-05-06 00:32:53 +08:00
|
|
|
const handleSendClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
2023-05-07 00:50:03 +08:00
|
|
|
if (input) {
|
2023-05-06 00:32:53 +08:00
|
|
|
// Add the user's message to the chat UI
|
2023-05-10 17:38:24 +08:00
|
|
|
messageHandlers.append({ type: 'user', message: input, contexts: contexts ? [...contexts].map((item) => ({ ...item })) : undefined });
|
2023-05-06 00:32:53 +08:00
|
|
|
|
|
|
|
// Process and send the message to the extension
|
2023-05-10 17:38:24 +08:00
|
|
|
const contextStrs = contexts.map(({ file, context }, index) => {
|
|
|
|
return `[context|${file}]`;
|
|
|
|
});
|
|
|
|
const text = input + contextStrs.join(' ');
|
2023-05-18 08:27:04 +08:00
|
|
|
// console.log(`message text: ${text}`);
|
2023-05-06 00:32:53 +08:00
|
|
|
messageUtil.sendMessage({
|
|
|
|
command: 'sendMessage',
|
2023-05-10 17:38:24 +08:00
|
|
|
text: text
|
2023-05-06 00:32:53 +08:00
|
|
|
});
|
2023-05-09 10:38:28 +08:00
|
|
|
|
2023-05-10 17:38:24 +08:00
|
|
|
// Clear the input field
|
|
|
|
setInput('');
|
|
|
|
contexts.length = 0;
|
|
|
|
|
2023-05-09 10:38:28 +08:00
|
|
|
// start generating
|
|
|
|
setGenerating(true);
|
|
|
|
setResponsed(false);
|
|
|
|
setCurrentMessage('');
|
2023-05-06 00:32:53 +08:00
|
|
|
}
|
|
|
|
};
|
2023-05-10 17:38:24 +08:00
|
|
|
|
|
|
|
const handleContextClick = (contextName: string) => {
|
|
|
|
// Process and send the message to the extension
|
|
|
|
messageUtil.sendMessage({
|
|
|
|
command: 'addContext',
|
|
|
|
selected: contextName
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-05-09 12:13:22 +08:00
|
|
|
const scrollToBottom = () =>
|
|
|
|
scrollViewport?.current?.scrollTo({ top: scrollViewport.current.scrollHeight, behavior: 'smooth' });
|
2023-05-06 00:32:53 +08:00
|
|
|
|
2023-05-18 08:27:04 +08:00
|
|
|
const timer = useTimeout(() => {
|
|
|
|
// console.log(`stopScrolling:${stopScrolling}`);
|
|
|
|
if (!stopScrolling) {
|
|
|
|
scrollToBottom();
|
|
|
|
}
|
|
|
|
}, 1000);
|
|
|
|
|
2023-05-09 12:27:35 +08:00
|
|
|
useEffect(() => {
|
|
|
|
inputRef.current.focus();
|
2023-05-09 19:10:46 +08:00
|
|
|
messageUtil.sendMessage({ command: 'regContextList' });
|
|
|
|
messageUtil.sendMessage({ command: 'regCommandList' });
|
2023-05-11 14:06:06 +08:00
|
|
|
messageUtil.sendMessage({ command: 'historyMessages' });
|
2023-05-18 08:27:04 +08:00
|
|
|
timer.start();
|
|
|
|
return () => {
|
|
|
|
timer.clear();
|
|
|
|
};
|
2023-05-09 12:27:35 +08:00
|
|
|
}, []);
|
|
|
|
|
2023-05-18 08:27:04 +08:00
|
|
|
useEffect(() => {
|
|
|
|
const sh = scrollViewport.current?.scrollHeight || 0;
|
|
|
|
const vh = scrollViewport.current?.clientHeight || 0;
|
|
|
|
const isBottom = sh < vh ? true : sh - vh - scrollPosition.y < 3;
|
|
|
|
if (isBottom) {
|
|
|
|
setStopScrolling(false);
|
|
|
|
} else {
|
|
|
|
setStopScrolling(true);
|
|
|
|
}
|
|
|
|
}, [scrollPosition]);
|
|
|
|
|
2023-05-07 01:19:48 +08:00
|
|
|
useEffect(() => {
|
2023-05-09 10:38:28 +08:00
|
|
|
if (generating) {
|
|
|
|
// new a bot message
|
2023-05-09 19:10:46 +08:00
|
|
|
messageHandlers.append({ type: 'bot', message: currentMessage });
|
2023-05-09 10:38:28 +08:00
|
|
|
}
|
|
|
|
}, [generating]);
|
2023-05-09 09:44:51 +08:00
|
|
|
|
|
|
|
// Add the received message to the chat UI as a bot message
|
|
|
|
useEffect(() => {
|
|
|
|
const lastIndex = messages?.length - 1;
|
|
|
|
const lastMessage = messages[lastIndex];
|
2023-05-09 10:38:28 +08:00
|
|
|
if (currentMessage && lastMessage?.type === 'bot') {
|
|
|
|
// update the last one bot message
|
2023-05-09 19:10:46 +08:00
|
|
|
messageHandlers.setItem(lastIndex, { type: 'bot', message: currentMessage });
|
2023-05-09 09:44:51 +08:00
|
|
|
}
|
2023-05-18 08:27:04 +08:00
|
|
|
timer.start();
|
2023-05-09 09:44:51 +08:00
|
|
|
}, [currentMessage]);
|
|
|
|
|
2023-05-12 10:08:38 +08:00
|
|
|
useEffect(() => {
|
|
|
|
if (messages.length > messageCount * 2) {
|
|
|
|
messageHandlers.remove(0, 1);
|
|
|
|
}
|
2023-05-18 08:27:04 +08:00
|
|
|
timer.start();
|
2023-05-12 10:08:38 +08:00
|
|
|
}, [messages]);
|
|
|
|
|
2023-05-09 09:44:51 +08:00
|
|
|
// Add the received message to the chat UI as a bot message
|
2023-05-07 00:50:03 +08:00
|
|
|
useEffect(() => {
|
|
|
|
if (registed) return;
|
2023-05-09 09:44:51 +08:00
|
|
|
setRegisted(true);
|
2023-05-07 18:27:01 +08:00
|
|
|
messageUtil.registerHandler('receiveMessagePartial', (message: { text: string; }) => {
|
2023-05-09 09:44:51 +08:00
|
|
|
setCurrentMessage(message.text);
|
2023-05-09 10:38:28 +08:00
|
|
|
setResponsed(true);
|
2023-05-09 09:44:51 +08:00
|
|
|
});
|
|
|
|
messageUtil.registerHandler('receiveMessage', (message: { text: string; }) => {
|
2023-05-09 10:38:28 +08:00
|
|
|
setCurrentMessage(message.text);
|
|
|
|
setGenerating(false);
|
|
|
|
setResponsed(true);
|
2023-05-07 00:50:03 +08:00
|
|
|
});
|
2023-05-09 19:10:46 +08:00
|
|
|
messageUtil.registerHandler('regCommandList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
2023-05-10 17:38:24 +08:00
|
|
|
commandMenusHandlers.append(...message.result);
|
2023-05-09 19:10:46 +08:00
|
|
|
});
|
|
|
|
messageUtil.registerHandler('regContextList', (message: { result: { pattern: string; description: string; name: string }[] }) => {
|
2023-05-10 17:38:24 +08:00
|
|
|
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) {
|
2023-05-10 17:38:24 +08:00
|
|
|
contextsHandlers.append({
|
|
|
|
file: message.file,
|
|
|
|
context: context,
|
|
|
|
});
|
2023-05-10 17:38:24 +08:00
|
|
|
}
|
2023-05-09 19:10:46 +08:00
|
|
|
});
|
2023-05-11 14:06:06 +08:00
|
|
|
messageUtil.registerHandler('loadHistoryMessages', (message: { command: string; entries: [{ hash: '', user: '', date: '', request: '', response: '', context: [{ content: '', role: '' }] }] }) => {
|
2023-05-11 15:10:05 +08:00
|
|
|
message.entries?.forEach(({ hash, user, date, request, response, context }, index) => {
|
2023-05-12 10:08:38 +08:00
|
|
|
if (index < message.entries.length - messageCount) return;
|
2023-05-11 14:06:06 +08:00
|
|
|
const contexts = context.map(({ content, role }) => ({ context: JSON.parse(content) }));
|
|
|
|
messageHandlers.append({ type: 'user', message: request, contexts: contexts });
|
|
|
|
messageHandlers.append({ type: 'bot', message: response });
|
|
|
|
});
|
|
|
|
});
|
2023-05-07 00:50:03 +08:00
|
|
|
}, [registed]);
|
2023-05-06 00:32:53 +08:00
|
|
|
|
2023-05-18 11:34:00 +08:00
|
|
|
useEffect(() => {
|
|
|
|
let filtered = commandMenus;
|
|
|
|
if (input) {
|
|
|
|
filtered = commandMenus.filter((item) => `/${item.pattern}`.startsWith(input));
|
|
|
|
}
|
|
|
|
const node = filtered.map(({ pattern, description, name }, index) => {
|
|
|
|
return (
|
|
|
|
<Flex
|
|
|
|
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={() => {
|
|
|
|
setInput(`/${pattern} `);
|
|
|
|
setMenuOpend(false);
|
|
|
|
}}
|
|
|
|
aria-checked={index === currentMenuIndex}
|
|
|
|
data-pattern={pattern}
|
|
|
|
>
|
|
|
|
<IconTerminal2
|
|
|
|
size={16}
|
|
|
|
color='var(--vscode-menu-foreground)'
|
|
|
|
style={{
|
|
|
|
marginTop: 8,
|
|
|
|
marginLeft: 8,
|
|
|
|
}} />
|
|
|
|
<Stack spacing={0}>
|
|
|
|
<Text sx={{
|
|
|
|
fontSize: 'sm',
|
|
|
|
fontWeight: 'bolder',
|
|
|
|
color: 'var(--vscode-menu-foreground)'
|
|
|
|
}}>
|
|
|
|
/{pattern}
|
|
|
|
</Text>
|
|
|
|
<Text sx={{
|
|
|
|
fontSize: 'sm',
|
|
|
|
color: theme.colors.gray[6],
|
|
|
|
}}>
|
|
|
|
{description}
|
|
|
|
</Text>
|
|
|
|
</Stack>
|
|
|
|
</Flex>);
|
|
|
|
});
|
|
|
|
setCommandMenusNode(node);
|
|
|
|
if (node.length === 0) {
|
|
|
|
setMenuOpend(false);
|
|
|
|
}
|
|
|
|
}, [input, commandMenus, currentMenuIndex]);
|
|
|
|
|
2023-05-09 09:44:51 +08:00
|
|
|
const handleInputChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
2023-05-06 00:32:53 +08:00
|
|
|
const value = event.target.value;
|
2023-05-05 07:41:56 +08:00
|
|
|
// if value start with '/' command show menu
|
2023-05-18 11:34:00 +08:00
|
|
|
if (value.startsWith('/')) {
|
2023-05-09 19:10:46 +08:00
|
|
|
setMenuOpend(true);
|
|
|
|
setMenuType('commands');
|
2023-05-18 11:34:00 +08:00
|
|
|
setCurrentMenuIndex(0);
|
2023-05-05 07:41:56 +08:00
|
|
|
} else {
|
2023-05-09 19:10:46 +08:00
|
|
|
setMenuOpend(false);
|
2023-05-05 07:41:56 +08:00
|
|
|
}
|
2023-05-06 00:32:53 +08:00
|
|
|
setInput(value);
|
|
|
|
};
|
2023-05-05 01:10:02 +08:00
|
|
|
|
2023-05-09 09:44:51 +08:00
|
|
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
2023-05-18 11:34:00 +08:00
|
|
|
if (menuOpend && menuType === 'commands') {
|
|
|
|
if (event.key === 'ArrowDown') {
|
|
|
|
const newIndex = currentMenuIndex + 1;
|
|
|
|
setCurrentMenuIndex(newIndex < commandMenusNode.length ? newIndex : 0);
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
if (event.key === 'ArrowUp') {
|
|
|
|
const newIndex = currentMenuIndex - 1;
|
|
|
|
setCurrentMenuIndex(newIndex < 0 ? commandMenusNode.length - 1 : newIndex);
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
|
|
const commandNode = commandMenusNode[currentMenuIndex];
|
|
|
|
setInput(`/${commandNode.props['data-pattern']} `);
|
|
|
|
setMenuOpend(false);
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (event.key === 'Enter' && !event.shiftKey) {
|
|
|
|
handleSendClick(event as any);
|
|
|
|
}
|
2023-05-07 00:50:03 +08:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const defaultMessages = (<Center>
|
|
|
|
<Text size="lg" color="gray" weight={500}>No messages yet</Text>
|
|
|
|
</Center>);
|
|
|
|
|
2023-05-10 17:38:24 +08:00
|
|
|
const contextMenusNode = contextMenus.map(({ pattern, description, name }, index) => {
|
2023-05-09 19:15:28 +08:00
|
|
|
return (
|
2023-05-18 10:31:22 +08:00
|
|
|
<Flex
|
|
|
|
mih={40}
|
|
|
|
gap="md"
|
|
|
|
justify="flex-start"
|
|
|
|
align="flex-start"
|
|
|
|
direction="row"
|
|
|
|
wrap="wrap"
|
2023-05-16 21:25:37 +08:00
|
|
|
sx={{
|
2023-05-18 10:31:22 +08:00
|
|
|
padding: '5px 0',
|
|
|
|
'&:hover': {
|
|
|
|
cursor: 'pointer',
|
|
|
|
color: 'var(--vscode-commandCenter-activeForeground)',
|
|
|
|
backgroundColor: 'var(--vscode-commandCenter-activeBackground)'
|
2023-05-16 21:25:37 +08:00
|
|
|
}
|
|
|
|
}}
|
2023-05-10 17:38:24 +08:00
|
|
|
onClick={() => {
|
|
|
|
handleContextClick(name);
|
2023-05-18 10:31:22 +08:00
|
|
|
setMenuOpend(false);
|
2023-05-10 17:38:24 +08:00
|
|
|
}}
|
2023-05-09 19:10:46 +08:00
|
|
|
>
|
2023-05-18 10:31:22 +08:00
|
|
|
<IconMessagePlus
|
|
|
|
size={16}
|
|
|
|
color='var(--vscode-menu-foreground)'
|
|
|
|
style={{
|
|
|
|
marginTop: 8,
|
|
|
|
marginLeft: 8,
|
|
|
|
}} />
|
|
|
|
<Stack spacing={0}>
|
|
|
|
<Text sx={{
|
|
|
|
fontSize: 'sm',
|
|
|
|
fontWeight: 'bolder',
|
|
|
|
color: 'var(--vscode-menu-foreground)'
|
|
|
|
}}>
|
|
|
|
{name}
|
|
|
|
</Text>
|
|
|
|
<Text sx={{
|
|
|
|
fontSize: 'sm',
|
|
|
|
color: theme.colors.gray[6],
|
|
|
|
}}>
|
|
|
|
{description}
|
|
|
|
</Text>
|
|
|
|
</Stack>
|
|
|
|
</Flex>);
|
2023-05-09 19:10:46 +08:00
|
|
|
});
|
|
|
|
|
2023-05-10 17:38:24 +08:00
|
|
|
const messageList = messages.map(({ message: messageText, type: messageType, contexts }, index) => {
|
2023-05-07 00:50:03 +08:00
|
|
|
// setMessage(messageText);
|
|
|
|
return (<>
|
|
|
|
<Flex
|
2023-05-07 20:00:51 +08:00
|
|
|
key={`message-${index}`}
|
2023-05-07 00:50:03 +08:00
|
|
|
mih={50}
|
2023-05-15 19:11:58 +08:00
|
|
|
w={scrollViewport.current?.clientWidth}
|
2023-05-18 13:09:19 +08:00
|
|
|
gap="xs"
|
2023-05-07 00:50:03 +08:00
|
|
|
justify="flex-start"
|
|
|
|
align="flex-start"
|
|
|
|
direction="row"
|
|
|
|
wrap="wrap"
|
|
|
|
>
|
|
|
|
{
|
|
|
|
messageType === 'bot'
|
2023-05-18 13:09:19 +08:00
|
|
|
? <Avatar color="indigo" size='sm' radius="xl" className={classes.avatar} src={avatarDevChat} />
|
|
|
|
: <Avatar color="cyan" size='sm' radius="xl" className={classes.avatar} src={avatarUser} />
|
2023-05-07 00:50:03 +08:00
|
|
|
}
|
|
|
|
|
2023-05-09 19:10:46 +08:00
|
|
|
<Container sx={{
|
|
|
|
marginTop: 0,
|
|
|
|
marginLeft: 0,
|
|
|
|
marginRight: 0,
|
|
|
|
paddingLeft: 0,
|
|
|
|
paddingRight: 0,
|
|
|
|
width: 'calc(100% - 62px)',
|
2023-05-10 17:38:24 +08:00
|
|
|
pre: {
|
|
|
|
whiteSpace: 'break-spaces'
|
2023-05-11 12:35:14 +08:00
|
|
|
},
|
2023-05-09 19:10:46 +08:00
|
|
|
}}>
|
2023-05-10 17:38:24 +08:00
|
|
|
{contexts &&
|
2023-05-16 21:25:37 +08:00
|
|
|
<Accordion variant="contained" chevronPosition="left"
|
|
|
|
sx={{
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
}}
|
|
|
|
styles={{
|
|
|
|
item: {
|
2023-05-18 10:31:22 +08:00
|
|
|
borderColor: 'var(--vscode-menu-border)',
|
2023-05-16 21:25:37 +08:00
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
},
|
|
|
|
control: {
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
}
|
|
|
|
},
|
|
|
|
chevron: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
},
|
|
|
|
icon: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
},
|
|
|
|
label: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
},
|
|
|
|
panel: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
},
|
|
|
|
content: {
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
2023-05-10 17:38:24 +08:00
|
|
|
{
|
|
|
|
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>
|
|
|
|
}
|
2023-05-10 17:38:24 +08:00
|
|
|
|
2023-05-10 17:38:24 +08:00
|
|
|
</Accordion.Panel>
|
|
|
|
</Accordion.Item>
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
</Accordion>
|
|
|
|
}
|
2023-05-07 18:27:01 +08:00
|
|
|
<ReactMarkdown
|
|
|
|
components={{
|
|
|
|
code({ node, inline, className, children, ...props }) {
|
2023-05-07 20:00:51 +08:00
|
|
|
|
2023-05-07 18:27:01 +08:00
|
|
|
const match = /language-(\w+)/.exec(className || '');
|
2023-05-07 20:00:51 +08:00
|
|
|
const value = String(children).replace(/\n$/, '');
|
2023-05-11 12:35:14 +08:00
|
|
|
const [commited, setCommited] = useState(false);
|
2023-05-07 20:00:51 +08:00
|
|
|
|
2023-05-07 18:27:01 +08:00
|
|
|
return !inline && match ? (
|
2023-05-07 20:00:51 +08:00
|
|
|
<div style={{ position: 'relative' }}>
|
|
|
|
<div style={{ position: 'absolute', top: 0, left: 0 }}>
|
|
|
|
{match[1] && (
|
|
|
|
<div
|
|
|
|
style={{
|
|
|
|
backgroundColor: '#333',
|
|
|
|
color: '#fff',
|
|
|
|
padding: '0.2rem 0.5rem',
|
|
|
|
borderRadius: '0.2rem',
|
|
|
|
fontSize: '0.8rem',
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{match[1]}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
2023-05-10 17:38:24 +08:00
|
|
|
<Flex
|
|
|
|
gap="5px"
|
|
|
|
justify="flex-start"
|
|
|
|
align="flex-start"
|
|
|
|
direction="row"
|
|
|
|
wrap="wrap"
|
|
|
|
style={{ position: 'absolute', top: 8, right: 10 }}>
|
2023-05-07 20:00:51 +08:00
|
|
|
<CopyButton value={value} timeout={2000}>
|
|
|
|
{({ copied, copy }) => (
|
2023-05-10 17:38:24 +08:00
|
|
|
<Tooltip label={copied ? 'Copied' : 'Copy'} withArrow position="left" color="gray">
|
2023-05-07 20:00:51 +08:00
|
|
|
<ActionIcon color={copied ? 'teal' : 'gray'} onClick={copy}>
|
|
|
|
{copied ? <IconCheck size="1rem" /> : <IconCopy size="1rem" />}
|
|
|
|
</ActionIcon>
|
|
|
|
</Tooltip>
|
|
|
|
)}
|
|
|
|
</CopyButton>
|
2023-05-11 12:35:14 +08:00
|
|
|
{match[1] && match[1] === 'commitmsg'
|
|
|
|
? (<>
|
|
|
|
<Tooltip label={commited ? 'Committing' : 'Commit'} withArrow position="left" color="gray">
|
|
|
|
<ActionIcon
|
|
|
|
color={commited ? 'teal' : 'gray'}
|
|
|
|
onClick={() => {
|
|
|
|
messageUtil.sendMessage({
|
|
|
|
command: 'doCommit',
|
|
|
|
content: value
|
|
|
|
});
|
|
|
|
setCommited(true);
|
|
|
|
setTimeout(() => { setCommited(false); }, 2000);
|
|
|
|
}}>
|
|
|
|
{commited ? <IconCheck size="1rem" /> : <IconGitCommit size="1rem" />}
|
|
|
|
</ActionIcon>
|
|
|
|
</Tooltip>
|
|
|
|
</>)
|
|
|
|
: (<>
|
|
|
|
<Tooltip label='View Diff' withArrow position="left" color="gray">
|
|
|
|
<ActionIcon onClick={() => {
|
|
|
|
messageUtil.sendMessage({
|
|
|
|
command: 'show_diff',
|
|
|
|
content: value
|
|
|
|
});
|
|
|
|
}}>
|
|
|
|
<IconFileDiff size="1.125rem" />
|
|
|
|
</ActionIcon>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip label='Insert Code' withArrow position="left" color="gray">
|
|
|
|
<ActionIcon onClick={() => {
|
|
|
|
messageUtil.sendMessage({
|
|
|
|
command: 'code_apply',
|
|
|
|
content: value
|
|
|
|
});
|
|
|
|
}}>
|
|
|
|
<IconColumnInsertRight size="1.125rem" />
|
|
|
|
</ActionIcon>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip label='Replace' withArrow position="left" color="gray">
|
|
|
|
<ActionIcon onClick={() => {
|
|
|
|
messageUtil.sendMessage({
|
|
|
|
command: 'code_file_apply',
|
|
|
|
content: value
|
|
|
|
});
|
|
|
|
}}>
|
|
|
|
<IconReplace size="1.125rem" />
|
|
|
|
</ActionIcon>
|
|
|
|
</Tooltip>
|
|
|
|
</>)}
|
2023-05-10 17:38:24 +08:00
|
|
|
</Flex>
|
2023-05-11 12:35:14 +08:00
|
|
|
<SyntaxHighlighter {...props} language={match[1]} customStyle={{ padding: '2em 1em 1em 2em', }} style={okaidia} PreTag="div">
|
2023-05-07 20:00:51 +08:00
|
|
|
{value}
|
|
|
|
</SyntaxHighlighter>
|
2023-05-11 12:35:14 +08:00
|
|
|
</div >
|
2023-05-07 18:27:01 +08:00
|
|
|
) : (
|
|
|
|
<code {...props} className={className}>
|
|
|
|
{children}
|
|
|
|
</code>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{messageText}
|
2023-05-11 12:35:14 +08:00
|
|
|
</ReactMarkdown >
|
2023-05-09 10:38:28 +08:00
|
|
|
{(generating && messageType === 'bot' && index === messages.length - 1) ? <Text sx={{
|
|
|
|
animation: `${blink} 0.5s infinite;`,
|
|
|
|
width: 5,
|
|
|
|
marginTop: responsed ? 0 : '1em',
|
2023-05-11 15:10:05 +08:00
|
|
|
backgroundColor: 'black',
|
|
|
|
display: 'block'
|
|
|
|
|
2023-05-09 10:38:28 +08:00
|
|
|
}}>|</Text> : ''}
|
2023-05-11 12:35:14 +08:00
|
|
|
</Container >
|
2023-05-09 19:10:46 +08:00
|
|
|
</Flex >
|
2023-05-18 13:09:19 +08:00
|
|
|
{index !== messages.length - 1 && <Divider my={3} />}
|
2023-05-07 00:50:03 +08:00
|
|
|
</>);
|
|
|
|
});
|
|
|
|
|
2023-05-05 01:10:02 +08:00
|
|
|
return (
|
2023-05-09 19:10:46 +08:00
|
|
|
<Container
|
|
|
|
id='chat-container'
|
|
|
|
ref={chatContainerRef}
|
|
|
|
sx={{
|
|
|
|
height: '100%',
|
|
|
|
paddingTop: 10,
|
2023-05-16 21:25:37 +08:00
|
|
|
background: 'var(--vscode-editor-background)',
|
|
|
|
color: 'var(--vscode-editor-foreground)'
|
2023-05-09 19:10:46 +08:00
|
|
|
}}>
|
|
|
|
<ScrollArea
|
|
|
|
id='chat-scroll-area'
|
2023-05-11 15:10:05 +08:00
|
|
|
h={generating ? height - px('8rem') : height - px('5rem')}
|
2023-05-15 19:11:58 +08:00
|
|
|
w={width - px('2rem')}
|
2023-05-09 19:10:46 +08:00
|
|
|
type="never"
|
2023-05-18 08:27:04 +08:00
|
|
|
onScrollPositionChange={onScrollPositionChange}
|
2023-05-09 19:10:46 +08:00
|
|
|
viewportRef={scrollViewport}>
|
|
|
|
{messageList.length > 0 ? messageList : defaultMessages}
|
|
|
|
</ScrollArea>
|
2023-05-10 17:38:24 +08:00
|
|
|
<Stack
|
|
|
|
sx={{ position: 'absolute', bottom: 10, width: scrollViewport.current?.clientWidth }}>
|
2023-05-11 15:10:05 +08:00
|
|
|
{generating &&
|
|
|
|
<Center>
|
|
|
|
<Button
|
2023-05-16 21:25:37 +08:00
|
|
|
leftIcon={<IconPlayerStop color='var(--vscode-button-foreground)' />}
|
|
|
|
sx={{
|
|
|
|
backgroundColor: 'var(--vscode-button-background)',
|
|
|
|
}}
|
|
|
|
styles={{
|
|
|
|
icon: {
|
|
|
|
color: 'var(--vscode-button-foreground)'
|
|
|
|
},
|
|
|
|
label: {
|
|
|
|
color: 'var(--vscode-button-foreground)'
|
|
|
|
}
|
|
|
|
}}
|
2023-05-11 15:10:05 +08:00
|
|
|
variant="white"
|
|
|
|
onClick={() => {
|
|
|
|
messageUtil.sendMessage({
|
|
|
|
command: 'stopDevChat'
|
|
|
|
});
|
|
|
|
setGenerating(false);
|
|
|
|
}}>
|
|
|
|
Stop generating
|
|
|
|
</Button>
|
|
|
|
</Center>
|
|
|
|
}
|
|
|
|
{contexts && contexts.length > 0 &&
|
2023-05-16 21:25:37 +08:00
|
|
|
<Accordion variant="contained" chevronPosition="left"
|
|
|
|
sx={{
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
}}
|
|
|
|
styles={{
|
|
|
|
item: {
|
2023-05-18 10:31:22 +08:00
|
|
|
borderColor: 'var(--vscode-menu-border)',
|
2023-05-16 21:25:37 +08:00
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
},
|
|
|
|
control: {
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
}
|
|
|
|
},
|
|
|
|
chevron: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
},
|
|
|
|
icon: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
},
|
|
|
|
label: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
},
|
|
|
|
panel: {
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
},
|
|
|
|
content: {
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
2023-05-10 17:38:24 +08:00
|
|
|
{
|
|
|
|
contexts.map(({ context }, index) => {
|
|
|
|
return (
|
2023-05-11 15:20:22 +08:00
|
|
|
<Accordion.Item value={`item-${index}`} >
|
2023-05-16 21:25:37 +08:00
|
|
|
<Box sx={{
|
|
|
|
display: 'flex', alignItems: 'center',
|
|
|
|
backgroundColor: 'var(--vscode-menu-background)',
|
|
|
|
}}>
|
2023-05-11 15:20:22 +08:00
|
|
|
<Accordion.Control w={'calc(100% - 40px)'}>
|
2023-05-10 17:38:24 +08:00
|
|
|
{'command' in context ? context.command : context.path}
|
|
|
|
</Accordion.Control>
|
|
|
|
<ActionIcon
|
|
|
|
mr={8}
|
|
|
|
size="lg"
|
2023-05-16 21:25:37 +08:00
|
|
|
sx={{
|
|
|
|
color: 'var(--vscode-menu-foreground)',
|
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: 'var(--vscode-toolbar-activeBackground)'
|
|
|
|
}
|
|
|
|
}}
|
2023-05-10 17:38:24 +08:00
|
|
|
onClick={() => {
|
|
|
|
contextsHandlers.remove(index);
|
|
|
|
}}>
|
|
|
|
<IconX size="1rem" />
|
|
|
|
</ActionIcon>
|
|
|
|
</Box>
|
2023-05-11 15:24:32 +08:00
|
|
|
<Accordion.Panel mah={300}>
|
|
|
|
<ScrollArea h={300} type="never">
|
|
|
|
{
|
|
|
|
context.content
|
2023-05-11 15:41:59 +08:00
|
|
|
? <pre style={{ overflowWrap: 'normal' }}>{context.content}</pre>
|
2023-05-11 15:24:32 +08:00
|
|
|
: <Center>
|
|
|
|
<Text c='gray.3'>No content</Text>
|
|
|
|
</Center>
|
|
|
|
}
|
|
|
|
</ScrollArea>
|
2023-05-10 17:38:24 +08:00
|
|
|
</Accordion.Panel>
|
|
|
|
</Accordion.Item>
|
|
|
|
);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
</Accordion>
|
|
|
|
}
|
2023-05-18 10:31:22 +08:00
|
|
|
<Popover
|
2023-05-10 17:38:24 +08:00
|
|
|
id='commandMenu'
|
|
|
|
position='top-start'
|
|
|
|
closeOnClickOutside={true}
|
|
|
|
shadow="xs"
|
|
|
|
width={scrollViewport.current?.clientWidth}
|
|
|
|
opened={menuOpend}
|
2023-05-10 20:27:43 +08:00
|
|
|
onChange={() => {
|
|
|
|
setMenuOpend(!menuOpend);
|
|
|
|
inputRef.current.focus();
|
|
|
|
}}
|
2023-05-10 17:38:24 +08:00
|
|
|
onClose={() => setMenuType('')}
|
|
|
|
onOpen={() => menuType !== '' ? setMenuOpend(true) : setMenuOpend(false)}
|
2023-05-16 21:25:37 +08:00
|
|
|
returnFocus={true}
|
|
|
|
>
|
2023-05-18 10:31:22 +08:00
|
|
|
<Popover.Target>
|
2023-05-10 17:38:24 +08:00
|
|
|
<Textarea
|
|
|
|
id='chat-textarea'
|
|
|
|
disabled={generating}
|
|
|
|
value={input}
|
|
|
|
ref={inputRef}
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
onChange={handleInputChange}
|
|
|
|
autosize
|
|
|
|
minRows={1}
|
|
|
|
maxRows={10}
|
|
|
|
radius="md"
|
|
|
|
size="md"
|
|
|
|
sx={{ pointerEvents: 'all' }}
|
2023-05-16 16:57:20 +08:00
|
|
|
placeholder="Send a message."
|
2023-05-16 21:25:37 +08:00
|
|
|
styles={{
|
|
|
|
icon: { alignItems: 'flex-start', paddingTop: '9px' },
|
|
|
|
rightSection: { alignItems: 'flex-start', paddingTop: '9px' },
|
|
|
|
input: {
|
|
|
|
backgroundColor: 'var(--vscode-input-background)',
|
2023-05-18 10:31:22 +08:00
|
|
|
borderColor: 'var(--vscode-input-border)',
|
2023-05-16 21:25:37 +08:00
|
|
|
color: 'var(--vscode-input-foreground)',
|
|
|
|
'&[data-disabled]': {
|
|
|
|
color: 'var(--vscode-disabledForeground)'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}}
|
2023-05-10 17:38:24 +08:00
|
|
|
icon={
|
2023-05-16 21:25:37 +08:00
|
|
|
<ActionIcon
|
|
|
|
disabled={generating}
|
|
|
|
onClick={handlePlusClick}
|
|
|
|
sx={{
|
|
|
|
pointerEvents: 'all',
|
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: 'var(--vscode-toolbar-activeBackground)'
|
|
|
|
},
|
|
|
|
'&[data-disabled]': {
|
2023-05-18 10:31:22 +08:00
|
|
|
borderColor: 'var(--vscode-input-border)',
|
2023-05-16 21:25:37 +08:00
|
|
|
backgroundColor: 'var(--vscode-toolbar-activeBackground)'
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
2023-05-10 17:38:24 +08:00
|
|
|
<IconSquareRoundedPlus size="1rem" />
|
|
|
|
</ActionIcon>
|
|
|
|
}
|
|
|
|
rightSection={
|
2023-05-16 21:25:37 +08:00
|
|
|
<ActionIcon
|
|
|
|
disabled={generating}
|
|
|
|
onClick={handleSendClick}
|
|
|
|
sx={{
|
|
|
|
pointerEvents: 'all',
|
|
|
|
'&:hover': {
|
|
|
|
backgroundColor: 'var(--vscode-toolbar-activeBackground)'
|
|
|
|
},
|
|
|
|
'&[data-disabled]': {
|
2023-05-18 10:31:22 +08:00
|
|
|
borderColor: 'var(--vscode-input-border)',
|
2023-05-16 21:25:37 +08:00
|
|
|
backgroundColor: 'var(--vscode-toolbar-activeBackground)'
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
>
|
2023-05-10 17:38:24 +08:00
|
|
|
<IconSend size="1rem" />
|
|
|
|
</ActionIcon>
|
|
|
|
}
|
|
|
|
/>
|
2023-05-18 10:31:22 +08:00
|
|
|
</Popover.Target>
|
2023-05-10 17:38:24 +08:00
|
|
|
{
|
|
|
|
menuType === 'contexts'
|
2023-05-18 10:31:22 +08:00
|
|
|
? (<Popover.Dropdown
|
2023-05-16 21:25:37 +08:00
|
|
|
sx={{
|
2023-05-18 10:31:22 +08:00
|
|
|
padding: 0,
|
2023-05-16 21:25:37 +08:00
|
|
|
color: 'var(--vscode-menu-foreground)',
|
2023-05-18 10:31:22 +08:00
|
|
|
borderColor: 'var(--vscode-menu-border)',
|
2023-05-16 21:25:37 +08:00
|
|
|
backgroundColor: 'var(--vscode-menu-background)'
|
|
|
|
}}>
|
2023-05-10 17:38:24 +08:00
|
|
|
<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 />
|
2023-05-18 10:31:22 +08:00
|
|
|
<Text sx={{ padding: '5px 5px 5px 10px' }}>DevChat Contexts</Text>
|
2023-05-10 17:38:24 +08:00
|
|
|
{contextMenusNode}
|
2023-05-18 10:31:22 +08:00
|
|
|
</Popover.Dropdown>)
|
2023-05-18 11:34:00 +08:00
|
|
|
: menuType === 'commands' && commandMenusNode.length > 0
|
2023-05-18 10:31:22 +08:00
|
|
|
? <Popover.Dropdown
|
2023-05-16 21:25:37 +08:00
|
|
|
sx={{
|
2023-05-18 10:31:22 +08:00
|
|
|
padding: 0,
|
2023-05-16 21:25:37 +08:00
|
|
|
color: 'var(--vscode-menu-foreground)',
|
2023-05-18 10:31:22 +08:00
|
|
|
borderColor: 'var(--vscode-menu-border)',
|
2023-05-16 21:25:37 +08:00
|
|
|
backgroundColor: 'var(--vscode-menu-background)'
|
|
|
|
}}>
|
2023-05-18 10:31:22 +08:00
|
|
|
<Text sx={{ padding: '5px 5px 5px 10px' }}>DevChat Commands</Text>
|
2023-05-10 17:38:24 +08:00
|
|
|
{commandMenusNode}
|
2023-05-18 10:31:22 +08:00
|
|
|
</Popover.Dropdown>
|
2023-05-10 17:38:24 +08:00
|
|
|
: <></>
|
|
|
|
}
|
2023-05-18 10:31:22 +08:00
|
|
|
</Popover>
|
2023-05-10 17:38:24 +08:00
|
|
|
</Stack>
|
2023-05-09 19:10:46 +08:00
|
|
|
</Container >
|
2023-05-05 01:10:02 +08:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default chatPanel;
|