import * as React from 'react'; import { useState, useEffect, useRef } from 'react'; import { Image, Accordion, Avatar, Box, Center, Container, CopyButton, Divider, Flex, Popover, Stack, Textarea, px, useMantineTheme } from '@mantine/core'; import { Tooltip } from '@mantine/core'; import { ScrollArea } from '@mantine/core'; import { createStyles, keyframes } from '@mantine/core'; import { ActionIcon } from '@mantine/core'; import { Button, Text } from '@mantine/core'; import { useListState, useResizeObserver, useTimeout, useViewportSize } from '@mantine/hooks'; import { IconBulb, IconCheck, IconColumnInsertRight, IconCopy, IconFileDiff, IconGitCommit, IconMessagePlus, IconPlayerStop, IconReplace, IconSend, IconSquareRoundedPlus, IconTerminal2, IconUserCircle, IconX } from '@tabler/icons-react'; import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import okaidia from 'react-syntax-highlighter/dist/esm/styles/prism/okaidia'; import messageUtil from '../../util/MessageUtil'; // @ts-ignore import SvgAvatarDevChat from './avatar_devchat.svg'; // @ts-ignore import SvgAvatarUser from './avatar_spaceman.png'; import { IconMouseRightClick, IconBook, IconGitBranch, IconGitBranchChecked, IconShellCommand } from './Icons'; const blink = keyframes({ '50%': { opacity: 0 }, }); const chatPanel = () => { const theme = useMantineTheme(); const [chatContainerRef, chatContainerRect] = useResizeObserver(); const scrollViewport = useRef(null); 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 [commandMenusNode, setCommandMenusNode] = useState(null); const [currentMenuIndex, setCurrentMenuIndex] = useState(0); const [contexts, contextsHandlers] = useListState([]); const [currentMessage, setCurrentMessage] = useState(''); const [generating, setGenerating] = useState(false); const [responsed, setResponsed] = useState(false); const [registed, setRegisted] = useState(false); const [input, setInput] = useState(''); const [menuOpend, setMenuOpend] = useState(false); const [menuType, setMenuType] = useState(''); // contexts or commands const { height, width } = useViewportSize(); const [inputRef, inputRect] = useResizeObserver(); const [scrollPosition, onScrollPositionChange] = useState({ x: 0, y: 0 }); const [stopScrolling, setStopScrolling] = useState(false); const messageCount = 10; const handlePlusClick = (event: React.MouseEvent) => { setMenuType('contexts'); setMenuOpend(!menuOpend); inputRef.current.focus(); event.stopPropagation(); }; const handleSendClick = (event: React.MouseEvent) => { if (input) { // Add the user's message to the chat UI 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(''); contexts.length = 0; // start generating setGenerating(true); setResponsed(false); 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' }); const timer = useTimeout(() => { // console.log(`stopScrolling:${stopScrolling}`); if (!stopScrolling) { scrollToBottom(); } }, 1000); useEffect(() => { inputRef.current.focus(); messageUtil.sendMessage({ command: 'regContextList' }); messageUtil.sendMessage({ command: 'regCommandList' }); messageUtil.sendMessage({ command: 'historyMessages' }); timer.start(); return () => { timer.clear(); }; }, []); 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]); useEffect(() => { if (generating) { // new a bot message messageHandlers.append({ type: 'bot', message: currentMessage }); } }, [generating]); // Add the received message to the chat UI as a bot message useEffect(() => { const lastIndex = messages?.length - 1; const lastMessage = messages[lastIndex]; if (currentMessage && lastMessage?.type === 'bot') { // update the last one bot message messageHandlers.setItem(lastIndex, { type: 'bot', message: currentMessage }); } timer.start(); }, [currentMessage]); useEffect(() => { if (messages.length > messageCount * 2) { messageHandlers.remove(0, 1); } timer.start(); }, [messages]); // Add the received message to the chat UI as a bot message useEffect(() => { if (registed) return; setRegisted(true); messageUtil.registerHandler('receiveMessagePartial', (message: { text: string; }) => { setCurrentMessage(message.text); setResponsed(true); }); messageUtil.registerHandler('receiveMessage', (message: { text: string; }) => { setCurrentMessage(message.text); setGenerating(false); setResponsed(true); }); 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, }); } }); messageUtil.registerHandler('loadHistoryMessages', (message: { command: string; entries: [{ hash: '', user: '', date: '', request: '', response: '', context: [{ content: '', role: '' }] }] }) => { message.entries?.forEach(({ hash, user, date, request, response, context }, index) => { if (index < message.entries.length - messageCount) return; const contexts = context.map(({ content, role }) => ({ context: JSON.parse(content) })); messageHandlers.append({ type: 'user', message: request, contexts: contexts }); messageHandlers.append({ type: 'bot', message: response }); }); }); }, [registed]); const commandMenuIcon = (pattern: string) => { if (pattern === 'code') { return (); } if (pattern === 'commit_message') { return (); } }; useEffect(() => { let filtered = commandMenus; if (input) { filtered = commandMenus.filter((item) => `/${item.pattern}`.startsWith(input)); } const node = filtered.map(({ pattern, description, name }, index) => { return ( { setInput(`/${pattern} `); setMenuOpend(false); }} aria-checked={index === currentMenuIndex} data-pattern={pattern} > {commandMenuIcon(pattern)} /{pattern} {description} ); }); setCommandMenusNode(node); if (node.length === 0) { setMenuOpend(false); } }, [input, commandMenus, currentMenuIndex]); const handleInputChange = (event: React.ChangeEvent) => { const value = event.target.value; // if value start with '/' command show menu if (value.startsWith('/')) { setMenuOpend(true); setMenuType('commands'); setCurrentMenuIndex(0); } else { setMenuOpend(false); } setInput(value); }; const handleKeyDown = (event: React.KeyboardEvent) => { if (menuOpend) { if (event.key === 'Escape') { setMenuOpend(false); } if (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); } } }; const defaultMessages = (
No messages yet
); const contextMenuIcon = (name: string) => { if (name === 'git diff --cached') { return (); } if (name === 'git diff HEAD') { return (); } if (name === '') { return (); } }; const contextMenusNode = contextMenus.map(({ pattern, description, name }, index) => { return ( { handleContextClick(name); setMenuOpend(false); }} > {contextMenuIcon(name)} {name} {description} ); }); const messageList = messages.map(({ message: messageText, type: messageType, contexts }, index) => { // setMessage(messageText); return (<> { messageType === 'bot' ? : } {messageType === 'bot' ? 'DevChat' : 'User'} {contexts && { contexts?.map(({ context }, index) => { return ( {'command' in context ? context.command : context.path} { context.content ? context.content :
No content
}
); }) }
}
{match[1] && (
{match[1]}
)}
{({ copied, copy }) => ( {copied ? : } )} {match[1] && match[1] === 'commitmsg' ? (<> { messageUtil.sendMessage({ command: 'doCommit', content: value }); setCommited(true); setTimeout(() => { setCommited(false); }, 2000); }}> {commited ? : } ) : (<> { messageUtil.sendMessage({ command: 'show_diff', content: value }); }}> { messageUtil.sendMessage({ command: 'code_apply', content: value }); }}> { messageUtil.sendMessage({ command: 'code_file_apply', content: value }); }}> )} {value} ) : ( {children} ); } }} > {messageText}
{(generating && messageType === 'bot' && index === messages.length - 1) ? | : ''}
{index !== messages.length - 1 && } ); }); return ( {messageList.length > 0 ? messageList : defaultMessages} {generating &&
} {contexts && contexts.length > 0 && { contexts.map(({ context }, index) => { return ( {'command' in context ? context.command : context.path} { contextsHandlers.remove(index); }}> { context.content ?
{context.content}
:
No content
}
); }) }
} { setMenuOpend(!menuOpend); inputRef.current.focus(); }} onClose={() => setMenuType('')} onOpen={() => menuType !== '' ? setMenuOpend(true) : setMenuOpend(false)} returnFocus={true} >