import { useMantineTheme, Flex, Stack, Accordion, Box, ActionIcon, ScrollArea, Center, Popover, Textarea, Text, Divider, Indicator, HoverCard, Drawer } from "@mantine/core"; import { useDisclosure, useListState, useResizeObserver, useTimeout } from "@mantine/hooks"; import { IconGitBranch, IconBook, IconX, IconSquareRoundedPlus, IconSend, IconPaperclip, IconChevronDown } from "@tabler/icons-react"; import React, { useState, useEffect } from "react"; import { IconGitBranchChecked, IconShellCommand, IconMouseRightClick } from "@/views/components/ChatIcons"; import messageUtil from '@/util/MessageUtil'; import InputContexts from './InputContexts'; import { observer } from "mobx-react-lite"; import { useMst } from "@/views/stores/RootStore"; import { ChatContext } from "@/views/stores/InputStore"; import { Message } from "@/views/stores/ChatStore"; const InputMessage = observer((props: any) => { const { chatPanelWidth } = props; const { input, chat } = useMst(); const { contexts, menuOpend, menuType, currentMenuIndex, contextMenus, commandMenus } = input; const { generating } = chat; const [drawerOpened, { open: openDrawer, close: closeDrawer }] = useDisclosure(false); const theme = useMantineTheme(); const [commandMenusNode, setCommandMenusNode] = useState(null); const [inputRef, inputRect] = useResizeObserver(); const handlePlusClick = (event: React.MouseEvent) => { input.openMenu('contexts'); inputRef.current.focus(); event.stopPropagation(); }; const handleInputChange = (event: React.ChangeEvent) => { 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); }; const handleSendClick = (event: React.MouseEvent) => { const inputValue = input.value; if (inputValue) { if (inputValue.trim() === '/help') { chat.helpMessage(); input.setValue(''); event.preventDefault(); } else{ const text = inputValue; // Add the user's message to the chat UI const chatContexts = contexts ? [...contexts].map((item) => ({ ...item })) : undefined; const newMessage = Message.create({ type: 'user', message: inputValue, contexts: chatContexts }); chat.newMessage(newMessage); // start generating chat.startGenerating(text, chatContexts); // Clear the input field input.setValue(''); input.clearContexts(); setTimeout(() => { chat.goScrollBottom(); }, 1000); } } }; const handleContextClick = (contextName: string) => { // Process and send the message to the extension messageUtil.sendMessage({ command: 'addContext', selected: contextName }); }; const handleKeyDown = (event: React.KeyboardEvent) => { if (menuOpend) { if (event.key === 'Escape') { input.closeMenu(); } if (menuType === 'commands') { if (event.key === 'ArrowDown') { const newIndex = currentMenuIndex + 1; input.setCurrentMenuIndex(newIndex < commandMenusNode.length ? newIndex : 0); event.preventDefault(); } if (event.key === 'ArrowUp') { const newIndex = currentMenuIndex - 1; input.setCurrentMenuIndex(newIndex < 0 ? commandMenusNode.length - 1 : newIndex); event.preventDefault(); } 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(); } } } else { if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) { handleSendClick(event as any); } } }; const contextMenuIcon = (name: string) => { if (name === 'git diff --cached') { return (); } if (name === 'git diff HEAD') { return (); } return (); }; const contextMenusNode = [...contextMenus] .sort((a, b) => { if (a.name === '') { return 1; // Placing '' at the end } else if (b.name === '') { return -1; // Placing '' at the front } else { return (a.name || "").localeCompare(b.name || ""); // Sorting alphabetically for other cases } }) .map(({ pattern, description, name }, index) => { return ( { handleContextClick(name); input.closeMenu(); }}> {contextMenuIcon(name)} {name} {description} ); }); useEffect(() => { input.fetchContextMenus().then(); input.fetchCommandMenus().then(); messageUtil.registerHandler('regCommandList', (message: { result: object[]}) => { input.updateCommands(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) { const chatContext = ChatContext.create({ file: message.file, path: context.path, command: context.command, content: context.content, }); input.newContext(chatContext); } }); inputRef.current.focus(); }, []); 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 ( { input.setValue(`/${pattern} `); input.closeMenu(); }} aria-checked={index === currentMenuIndex} data-pattern={pattern}> /{pattern} {description} ); }); setCommandMenusNode(node); if (node.length === 0) { input.closeMenu(); } }, [input.value, commandMenus, currentMenuIndex]); useEffect(() => { if (drawerOpened && (!contexts || contexts.length === 0)) { closeDrawer(); } }, [contexts.length]); return ( <> {contexts && contexts.length > 0 && }} styles={{ content: { background: 'var(--vscode-sideBar-background)', color: 'var(--vscode-editor-foreground)', }, header: { background: 'var(--vscode-sideBar-background)', color: 'var(--vscode-editor-foreground)', } }}> } { input.closeMenu(); inputRef.current.focus(); }} onClose={() => input.closeMenu()} onOpen={() => menuType !== '' ? input.openMenu(menuType) : input.closeMenu()} returnFocus={true}>