Merge pull request #209 from devchat-ai/52-i-hope-that-chat-messages-in-devchat-can-be-selectively-deleted
52 i hope that chat messages in devchat can be selectively deleted
This commit is contained in:
commit
03a698e38a
@ -93,14 +93,12 @@ export function registerStatusBarItemClickCommand(context: vscode.ExtensionConte
|
||||
|
||||
const topicDeleteCallback = async (item: TopicTreeItem) => {
|
||||
const confirm = 'Delete';
|
||||
const cancel = 'Cancel';
|
||||
const label = typeof item.label === 'string' ? item.label : item.label!.label;
|
||||
const truncatedLabel = label.substring(0, 20) + (label.length > 20 ? '...' : '');
|
||||
const result = await vscode.window.showWarningMessage(
|
||||
`Are you sure you want to delete the topic "${truncatedLabel}"?`,
|
||||
{ modal: true },
|
||||
confirm,
|
||||
cancel
|
||||
confirm
|
||||
);
|
||||
|
||||
if (result === confirm) {
|
||||
|
@ -6,7 +6,7 @@ import { doCommit } from './doCommit';
|
||||
import { historyMessages } from './historyMessages';
|
||||
import { regCommandList } from './regCommandList';
|
||||
import { regContextList } from './regContextList';
|
||||
import { sendMessage, stopDevChat, regeneration } from './sendMessage';
|
||||
import { sendMessage, stopDevChat, regeneration, deleteChatMessage } from './sendMessage';
|
||||
import { blockApply } from './showDiff';
|
||||
import { showDiff } from './showDiff';
|
||||
import { addConext } from './addContext';
|
||||
@ -73,3 +73,6 @@ messageHandler.registerHandler('regActionList', regActionList);
|
||||
// Apply action for code block
|
||||
// Response: none
|
||||
messageHandler.registerHandler('applyAction', applyAction);
|
||||
// Delete chat message
|
||||
// Response: { command: 'deletedChatMessage', result: <message id> }
|
||||
messageHandler.registerHandler('deleteChatMessage', deleteChatMessage);
|
||||
|
@ -2,12 +2,13 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { MessageHandler } from './messageHandler';
|
||||
import { regInMessage, regOutMessage } from '../util/reg_messages';
|
||||
import { stopDevChatBase, sendMessageBase } from './sendMessageBase';
|
||||
import { stopDevChatBase, sendMessageBase, deleteChatMessageBase } from './sendMessageBase';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
|
||||
|
||||
let _lastMessage: any = undefined;
|
||||
|
||||
regInMessage({command: 'sendMessage', text: '', hash: undefined});
|
||||
regInMessage({command: 'sendMessage', text: '', parent_hash: undefined});
|
||||
regOutMessage({ command: 'receiveMessage', text: 'xxxx', hash: 'xxx', user: 'xxx', date: 'xxx'});
|
||||
regOutMessage({ command: 'receiveMessagePartial', text: 'xxxx', user: 'xxx', date: 'xxx'});
|
||||
// message: { command: 'sendMessage', text: 'xxx', hash: 'xxx'}
|
||||
@ -39,5 +40,26 @@ export async function stopDevChat(message: any, panel: vscode.WebviewPanel|vscod
|
||||
stopDevChatBase(message);
|
||||
}
|
||||
|
||||
regInMessage({command: 'deleteChatMessage', hash: 'xxx'});
|
||||
regOutMessage({ command: 'deletedChatMessage', hash: 'xxxx'});
|
||||
export async function deleteChatMessage(message: any, panel: vscode.WebviewPanel|vscode.WebviewView): Promise<void> {
|
||||
// prompt user to confirm
|
||||
const confirm = await vscode.window.showWarningMessage(
|
||||
`Are you sure to delete this message?`,
|
||||
{ modal: true },
|
||||
'Delete'
|
||||
);
|
||||
if (confirm !== 'Delete') {
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await deleteChatMessageBase(message);
|
||||
if (deleted) {
|
||||
MessageHandler.sendMessage(panel, { command: 'deletedChatMessage', hash: message.hash });
|
||||
} else {
|
||||
UiUtilWrapper.showErrorMessage('Delete message failed!');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -104,22 +104,8 @@ export async function parseMessageAndSetOptions(message: any, chatOptions: any):
|
||||
return parsedMessage;
|
||||
}
|
||||
|
||||
// 将处理父哈希的部分提取到一个单独的函数中
|
||||
export function getParentHash(message: any): string|undefined {
|
||||
let parentHash = undefined;
|
||||
logger.channel()?.info(`request message hash: ${message.hash}`);
|
||||
if (message.hash) {
|
||||
const hmessage = messageHistory.find(message.hash);
|
||||
parentHash = hmessage ? hmessage.parentHash : undefined;
|
||||
} else {
|
||||
const hmessage = messageHistory.findLast();
|
||||
parentHash = hmessage ? hmessage.hash : undefined;
|
||||
}
|
||||
logger.channel()?.info(`parent hash: ${parentHash}`);
|
||||
return parentHash;
|
||||
}
|
||||
|
||||
export async function handleTopic(parentHash:string, message: any, chatResponse: ChatResponse) {
|
||||
export async function handleTopic(parentHash:string | undefined, message: any, chatResponse: ChatResponse) {
|
||||
WaitCreateTopic = true;
|
||||
try {
|
||||
if (!chatResponse.isError) {
|
||||
@ -158,10 +144,10 @@ export async function sendMessageBase(message: any, handlePartialData: (data: {
|
||||
const chatOptions: any = {};
|
||||
const parsedMessage = await parseMessageAndSetOptions(message, chatOptions);
|
||||
|
||||
const parentHash = getParentHash(message);
|
||||
if (parentHash) {
|
||||
chatOptions.parent = parentHash;
|
||||
if (message.parent_hash) {
|
||||
chatOptions.parent = message.parent_hash;
|
||||
}
|
||||
logger.channel()?.info(`parent hash: ${chatOptions.parent}`);
|
||||
|
||||
let partialDataText = '';
|
||||
const onData = (partialResponse: ChatResponse) => {
|
||||
@ -170,7 +156,7 @@ export async function sendMessageBase(message: any, handlePartialData: (data: {
|
||||
};
|
||||
|
||||
const chatResponse = await devChat.chat(parsedMessage.text, chatOptions, onData);
|
||||
await handleTopic(parentHash!, message, chatResponse);
|
||||
await handleTopic(message.parent_hash, message, chatResponse);
|
||||
const responseText = await handlerResponseText(partialDataText, chatResponse);
|
||||
if (responseText === undefined) {
|
||||
return;
|
||||
@ -184,3 +170,19 @@ export async function stopDevChatBase(message: any): Promise<void> {
|
||||
userStop = true;
|
||||
devChat.stop();
|
||||
}
|
||||
|
||||
// delete a chat message
|
||||
// each message is identified by hash
|
||||
export async function deleteChatMessageBase(message:{'hash': string}): Promise<boolean> {
|
||||
// if hash is undefined, return
|
||||
if (!message.hash) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// delete the message from messageHistory
|
||||
messageHistory.delete(message.hash);
|
||||
|
||||
// delete the message by devchat
|
||||
const bSuccess = await devChat.delete(message.hash);
|
||||
return bSuccess;
|
||||
}
|
@ -8,6 +8,7 @@ import { CommandRun } from "../util/commonUtil";
|
||||
import ExtensionContextHolder from '../util/extensionContext';
|
||||
import { UiUtilWrapper } from '../util/uiUtil';
|
||||
import { ApiKeyManager } from '../util/apiKey';
|
||||
import { exitCode } from 'process';
|
||||
|
||||
|
||||
|
||||
@ -234,6 +235,43 @@ class DevChat {
|
||||
}
|
||||
}
|
||||
|
||||
async delete(hash: string): Promise<boolean> {
|
||||
const args = ["log", "--delete", hash];
|
||||
const devChat = this.getDevChatPath();
|
||||
const workspaceDir = UiUtilWrapper.workspaceFoldersFirstPath();
|
||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
||||
|
||||
logger.channel()?.info(`Running devchat with arguments: ${args.join(" ")}`);
|
||||
const spawnOptions = {
|
||||
maxBuffer: 10 * 1024 * 1024, // Set maxBuffer to 10 MB
|
||||
cwd: workspaceDir,
|
||||
env: {
|
||||
...process.env,
|
||||
OPENAI_API_KEY: openaiApiKey,
|
||||
},
|
||||
};
|
||||
const { exitCode: code, stdout, stderr } = await this.commandRun.spawnAsync(devChat, args, spawnOptions, undefined, undefined, undefined, undefined);
|
||||
|
||||
logger.channel()?.info(`Finish devchat with arguments: ${args.join(" ")}`);
|
||||
if (stderr) {
|
||||
logger.channel()?.error(`Error: ${stderr}`);
|
||||
logger.channel()?.show();
|
||||
return false;
|
||||
}
|
||||
if (stdout.indexOf('Failed to delete prompt') >= 0) {
|
||||
logger.channel()?.error(`Failed to delete prompt: ${hash}`);
|
||||
logger.channel()?.show();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
logger.channel()?.error(`Exit code: ${code}`);
|
||||
logger.channel()?.show();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async log(options: LogOptions = {}): Promise<LogEntry[]> {
|
||||
const args = this.buildLogArgs(options);
|
||||
const devChat = this.getDevChatPath();
|
||||
|
@ -30,6 +30,18 @@ export class MessageHistory {
|
||||
find(hash: string) {
|
||||
return this.history.find(message => message.hash === hash);
|
||||
}
|
||||
|
||||
delete(hash: string) {
|
||||
const index = this.history.findIndex(message => message.hash === hash);
|
||||
if (index >= 0) {
|
||||
this.history.splice(index, 1);
|
||||
}
|
||||
|
||||
if (this.lastmessage?.hash === hash) {
|
||||
this.lastmessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
findLast() {
|
||||
return this.lastmessage;
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ const StopButton = () => {
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(stopGenerating({ hasDone: false }));
|
||||
dispatch(stopGenerating({ hasDone: false, message: null }));
|
||||
messageUtil.sendMessage({
|
||||
command: 'stopDevChat'
|
||||
});
|
||||
@ -124,8 +124,8 @@ const chatPanel = () => {
|
||||
dispatch(startResponsing(message.text));
|
||||
timer.start();
|
||||
});
|
||||
messageUtil.registerHandler('receiveMessage', (message: { text: string; isError: boolean }) => {
|
||||
dispatch(stopGenerating({ hasDone: true }));
|
||||
messageUtil.registerHandler('receiveMessage', (message: { text: string; isError: boolean, hash }) => {
|
||||
dispatch(stopGenerating({ hasDone: true, message: message }));
|
||||
if (message.isError) {
|
||||
dispatch(happendError(message.text));
|
||||
}
|
||||
|
102
src/views/InputContexts.tsx
Normal file
102
src/views/InputContexts.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { Accordion, Box, ActionIcon, ScrollArea, Center, Text } from "@mantine/core";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import React from "react";
|
||||
import { useAppDispatch, useAppSelector } from '@/views/hooks';
|
||||
|
||||
import {
|
||||
selectContexts,
|
||||
removeContext,
|
||||
} from './inputSlice';
|
||||
|
||||
const InputContexts = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const contexts = useAppSelector(selectContexts);
|
||||
return (<Accordion variant="contained" chevronPosition="left"
|
||||
sx={{
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}}
|
||||
styles={{
|
||||
item: {
|
||||
borderColor: 'var(--vscode-menu-border)',
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
'&[data-active]': {
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}
|
||||
},
|
||||
control: {
|
||||
height: 30,
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
'&[aria-expanded="true"]': {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
'&: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)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
content: {
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}
|
||||
}}>
|
||||
{
|
||||
contexts.map((item: any, index: number) => {
|
||||
const { context } = item;
|
||||
return (
|
||||
<Accordion.Item key={`item-${index}`} value={`item-value-${index}`} >
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'center',
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}}>
|
||||
<Accordion.Control w={'calc(100% - 40px)'}>
|
||||
<Text truncate='end'>{'command' in context ? context.command : context.path}</Text>
|
||||
</Accordion.Control>
|
||||
<ActionIcon
|
||||
mr={8}
|
||||
size="sm"
|
||||
sx={{
|
||||
color: 'var(--vscode-menu-foreground)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--vscode-toolbar-activeBackground)'
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(removeContext(index));
|
||||
}}>
|
||||
<IconX size="1rem" />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
<Accordion.Panel mah={300}>
|
||||
<ScrollArea h={300} type="never">
|
||||
{
|
||||
context.content
|
||||
? <pre style={{ overflowWrap: 'normal' }}>{context.content}</pre>
|
||||
: <Center>
|
||||
<Text c='gray.3'>No content</Text>
|
||||
</Center>
|
||||
}
|
||||
</ScrollArea>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Accordion>);
|
||||
};
|
||||
|
||||
export default InputContexts;
|
@ -5,6 +5,7 @@ import React, { useState, useEffect } from "react";
|
||||
import { IconGitBranchChecked, IconShellCommand, IconMouseRightClick } from "./Icons";
|
||||
import messageUtil from '@/util/MessageUtil';
|
||||
import { useAppDispatch, useAppSelector } from '@/views/hooks';
|
||||
import InputContexts from '@/views/InputContexts';
|
||||
|
||||
import {
|
||||
setValue,
|
||||
@ -16,7 +17,6 @@ import {
|
||||
selectContextMenus,
|
||||
selectCommandMenus,
|
||||
setCurrentMenuIndex,
|
||||
removeContext,
|
||||
clearContexts,
|
||||
newContext,
|
||||
openMenu,
|
||||
@ -30,97 +30,6 @@ import {
|
||||
startGenerating,
|
||||
} from './chatSlice';
|
||||
|
||||
const InputContexts = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const contexts = useAppSelector(selectContexts);
|
||||
return (<Accordion variant="contained" chevronPosition="left"
|
||||
sx={{
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}}
|
||||
styles={{
|
||||
item: {
|
||||
borderColor: 'var(--vscode-menu-border)',
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
'&[data-active]': {
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}
|
||||
},
|
||||
control: {
|
||||
height: 30,
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
'&[aria-expanded="true"]': {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
'&: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)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
content: {
|
||||
borderRadius: 3,
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}
|
||||
}}>
|
||||
{
|
||||
contexts.map((item: any, index: number) => {
|
||||
const { context } = item;
|
||||
return (
|
||||
<Accordion.Item key={`item-${index}`} value={`item-value-${index}`} >
|
||||
<Box sx={{
|
||||
display: 'flex', alignItems: 'center',
|
||||
backgroundColor: 'var(--vscode-menu-background)',
|
||||
}}>
|
||||
<Accordion.Control w={'calc(100% - 40px)'}>
|
||||
<Text truncate='end'>{'command' in context ? context.command : context.path}</Text>
|
||||
</Accordion.Control>
|
||||
<ActionIcon
|
||||
mr={8}
|
||||
size="sm"
|
||||
sx={{
|
||||
color: 'var(--vscode-menu-foreground)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--vscode-toolbar-activeBackground)'
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
dispatch(removeContext(index));
|
||||
}}>
|
||||
<IconX size="1rem" />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
<Accordion.Panel mah={300}>
|
||||
<ScrollArea h={300} type="never">
|
||||
{
|
||||
context.content
|
||||
? <pre style={{ overflowWrap: 'normal' }}>{context.content}</pre>
|
||||
: <Center>
|
||||
<Text c='gray.3'>No content</Text>
|
||||
</Center>
|
||||
}
|
||||
</ScrollArea>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Accordion>);
|
||||
};
|
||||
|
||||
const InputMessage = (props: any) => {
|
||||
const { width } = props;
|
||||
|
||||
|
@ -1,22 +1,13 @@
|
||||
|
||||
import { Center, Text, Flex, Avatar, Accordion, Box, Stack, Container, Divider, ActionIcon, Tooltip, CopyButton } from "@mantine/core";
|
||||
import { Center, Text, Accordion, Box, Stack, Container, Divider } from "@mantine/core";
|
||||
import React from "react";
|
||||
import CodeBlock from "@/views/CodeBlock";
|
||||
import MessageHeader from "@/views/MessageHeader";
|
||||
|
||||
// @ts-ignore
|
||||
import SvgAvatarDevChat from '@/views/avatar_devchat.svg';
|
||||
// @ts-ignore
|
||||
import SvgAvatarUser from '@/views/avatar_spaceman.png';
|
||||
import { IconCheck, IconCopy, Icon360 } from "@tabler/icons-react";
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '@/views/hooks';
|
||||
import { useAppSelector } from '@/views/hooks';
|
||||
import {
|
||||
selectMessages,
|
||||
} from './chatSlice';
|
||||
import {
|
||||
setContexts,
|
||||
setValue,
|
||||
} from './inputSlice';
|
||||
|
||||
|
||||
const MessageContext = (props: any) => {
|
||||
@ -95,55 +86,6 @@ const MessageContext = (props: any) => {
|
||||
);
|
||||
};
|
||||
|
||||
const MessageHeader = (props: any) => {
|
||||
const { type, message, contexts } = props;
|
||||
const dispatch = useAppDispatch();
|
||||
const [done, setDone] = React.useState(false);
|
||||
return (<Flex
|
||||
m='10px 0 10px 0'
|
||||
gap="sm"
|
||||
justify="flex-start"
|
||||
align="center"
|
||||
direction="row"
|
||||
wrap="wrap">
|
||||
{
|
||||
type === 'bot'
|
||||
? <Avatar
|
||||
color="indigo"
|
||||
size={25}
|
||||
radius="xl"
|
||||
src={SvgAvatarDevChat} />
|
||||
: <Avatar
|
||||
color="cyan"
|
||||
size={25}
|
||||
radius="xl"
|
||||
src={SvgAvatarUser} />
|
||||
}
|
||||
<Text weight='bold'>{type === 'bot' ? 'DevChat' : 'User'}</Text>
|
||||
{type === 'user'
|
||||
? <Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={done ? 'Refilled' : 'Refill prompt'} withArrow position="left" color="gray">
|
||||
<ActionIcon size='sm' style={{ marginLeft: 'auto' }}
|
||||
onClick={() => {
|
||||
dispatch(setValue(message));
|
||||
dispatch(setContexts(contexts));
|
||||
setDone(true);
|
||||
setTimeout(() => { setDone(false); }, 2000);
|
||||
}}>
|
||||
{done ? <IconCheck size="1rem" /> : <Icon360 size="1.125rem" />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
: <CopyButton value={message} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={copied ? 'Copied' : 'Copy message'} withArrow position="left" color="gray">
|
||||
<ActionIcon size='xs' color={copied ? 'teal' : 'gray'} onClick={copy} style={{ marginLeft: 'auto' }}>
|
||||
{copied ? <IconCheck size="1rem" /> : <IconCopy size="1rem" />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
}
|
||||
</Flex >);
|
||||
};
|
||||
|
||||
const MessageContainer = (props: any) => {
|
||||
const { width } = props;
|
||||
@ -163,9 +105,8 @@ const MessageContainer = (props: any) => {
|
||||
}}>
|
||||
<MessageHeader
|
||||
key={`message-header-${index}`}
|
||||
type={messageType}
|
||||
message={messageText}
|
||||
contexts={contexts} />
|
||||
showDelete={index === messages.length - 2}
|
||||
item={item} />
|
||||
<Container
|
||||
key={`message-container-${index}`}
|
||||
sx={{
|
||||
|
102
src/views/MessageHeader.tsx
Normal file
102
src/views/MessageHeader.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import { Text, Flex, Avatar, ActionIcon, Tooltip, CopyButton, SimpleGrid } from "@mantine/core";
|
||||
|
||||
// @ts-ignore
|
||||
import SvgAvatarDevChat from '@/views/avatar_devchat.svg';
|
||||
// @ts-ignore
|
||||
import SvgAvatarUser from '@/views/avatar_spaceman.png';
|
||||
import { IconCheck, IconCopy, Icon360, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
|
||||
import { useAppDispatch } from '@/views/hooks';
|
||||
|
||||
import {
|
||||
setContexts,
|
||||
setValue,
|
||||
} from './inputSlice';
|
||||
|
||||
import {
|
||||
deleteMessage,
|
||||
popMessage
|
||||
} from './chatSlice';
|
||||
|
||||
const MessageHeader = (props: any) => {
|
||||
const { item, showEdit = false, showDelete = true } = props;
|
||||
const { contexts, message, type, hash } = item;
|
||||
const dispatch = useAppDispatch();
|
||||
const [done, setDone] = React.useState(false);
|
||||
return (<Flex
|
||||
m='10px 0 10px 0'
|
||||
gap="sm"
|
||||
justify="flex-start"
|
||||
align="center"
|
||||
direction="row"
|
||||
wrap="wrap">
|
||||
{
|
||||
type === 'bot'
|
||||
? <Avatar
|
||||
color="indigo"
|
||||
size={25}
|
||||
radius="xl"
|
||||
src={SvgAvatarDevChat} />
|
||||
: <Avatar
|
||||
color="cyan"
|
||||
size={25}
|
||||
radius="xl"
|
||||
src={SvgAvatarUser} />
|
||||
}
|
||||
<Text weight='bold'>{type === 'bot' ? 'DevChat' : 'User'}</Text>
|
||||
{type === 'user'
|
||||
? <Flex
|
||||
gap="xs"
|
||||
justify="flex-end"
|
||||
align="center"
|
||||
direction="row"
|
||||
wrap="wrap"
|
||||
style={{ marginLeft: 'auto' }}>
|
||||
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={done ? 'Refilled' : 'Refill prompt'} withArrow position="left" color="gray">
|
||||
<ActionIcon size='sm'
|
||||
onClick={() => {
|
||||
dispatch(setValue(message));
|
||||
dispatch(setContexts(contexts));
|
||||
setDone(true);
|
||||
setTimeout(() => { setDone(false); }, 2000);
|
||||
}}>
|
||||
{done ? <IconCheck size="1rem" /> : <Icon360 size="1.125rem" />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{showEdit && <Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label="Edit message" withArrow position="left" color="gray">
|
||||
<ActionIcon size='sm'
|
||||
onClick={() => {
|
||||
|
||||
}}>
|
||||
<IconEdit size="1.125rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip >}
|
||||
{showDelete && hash !== 'message' && <Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label="Delete message" withArrow position="left" color="gray">
|
||||
<ActionIcon size='sm'
|
||||
onClick={() => {
|
||||
if (item.hash) {
|
||||
dispatch(deleteMessage(item));
|
||||
} else {
|
||||
dispatch(popMessage());
|
||||
dispatch(popMessage());
|
||||
}
|
||||
}}>
|
||||
<IconTrash size="1.125rem" />
|
||||
</ActionIcon>
|
||||
</Tooltip >}
|
||||
</Flex >
|
||||
: <CopyButton value={message} timeout={2000}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={copied ? 'Copied' : 'Copy message'} withArrow position="left" color="gray">
|
||||
<ActionIcon size='xs' color={copied ? 'teal' : 'gray'} onClick={copy} style={{ marginLeft: 'auto' }}>
|
||||
{copied ? <IconCheck size="1rem" /> : <IconCopy size="1rem" />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CopyButton>
|
||||
}
|
||||
</Flex >);
|
||||
};
|
||||
|
||||
export default MessageHeader;
|
@ -19,6 +19,22 @@ export const fetchHistoryMessages = createAsyncThunk<{ pageIndex: number, entrie
|
||||
});
|
||||
});
|
||||
|
||||
export const deleteMessage = createAsyncThunk<{ hash }, { hash }>('chat/deleteMessage', async (params) => {
|
||||
const { hash } = params;
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
messageUtil.sendMessage({ command: 'deleteChatMessage', hash: hash });
|
||||
messageUtil.registerHandler('deletedChatMessage', (message) => {
|
||||
resolve({
|
||||
hash: message.hash
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const chatSlice = createSlice({
|
||||
name: 'chat',
|
||||
initialState: {
|
||||
@ -41,9 +57,17 @@ export const chatSlice = createSlice({
|
||||
state.hasDone = false;
|
||||
state.errorMessage = '';
|
||||
state.currentMessage = '';
|
||||
let lastNonEmptyHash;
|
||||
for (let i = state.messages.length - 1; i >= 0; i--) {
|
||||
if (state.messages[i].hash) {
|
||||
lastNonEmptyHash = state.messages[i].hash;
|
||||
break;
|
||||
}
|
||||
}
|
||||
messageUtil.sendMessage({
|
||||
command: 'sendMessage',
|
||||
text: action.payload
|
||||
text: action.payload,
|
||||
parent_hash: lastNonEmptyHash
|
||||
});
|
||||
},
|
||||
reGenerating: (state) => {
|
||||
@ -61,6 +85,17 @@ export const chatSlice = createSlice({
|
||||
state.generating = false;
|
||||
state.responsed = false;
|
||||
state.hasDone = action.payload.hasDone;
|
||||
if (action.payload.hasDone) {
|
||||
const { hash } = action.payload.message;
|
||||
const messagesLength = state.messages.length;
|
||||
|
||||
if (messagesLength > 1) {
|
||||
state.messages[messagesLength - 2].hash = hash;
|
||||
state.messages[messagesLength - 1].hash = hash;
|
||||
} else if (messagesLength > 0) {
|
||||
state.messages[messagesLength - 1].hash = hash;
|
||||
}
|
||||
}
|
||||
},
|
||||
startResponsing: (state, action) => {
|
||||
state.responsed = true;
|
||||
@ -110,8 +145,8 @@ export const chatSlice = createSlice({
|
||||
const { hash, user, date, request, response, context } = item;
|
||||
const contexts = context?.map(({ content, role }) => ({ context: JSON.parse(content) }));
|
||||
return [
|
||||
{ type: 'user', message: request, contexts: contexts },
|
||||
{ type: 'bot', message: response },
|
||||
{ type: 'user', message: request, contexts: contexts, date: date, hash: hash },
|
||||
{ type: 'bot', message: response, date: date, hash: hash },
|
||||
];
|
||||
})
|
||||
.flat();
|
||||
@ -123,6 +158,13 @@ export const chatSlice = createSlice({
|
||||
} else {
|
||||
state.isLastPage = true;
|
||||
}
|
||||
})
|
||||
.addCase(deleteMessage.fulfilled, (state, action) => {
|
||||
const { hash } = action.payload;
|
||||
const index = state.messages.findIndex((item: any) => item.hash === hash);
|
||||
if (index > -1) {
|
||||
state.messages.splice(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import { describe, it } from 'mocha';
|
||||
import { Context } from 'mocha';
|
||||
import sinon from 'sinon';
|
||||
import * as path from 'path';
|
||||
import { parseMessage, getInstructionFiles, parseMessageAndSetOptions, getParentHash, handleTopic, handlerResponseText, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase';
|
||||
import { parseMessage, getInstructionFiles, parseMessageAndSetOptions, handleTopic, handlerResponseText, sendMessageBase, stopDevChatBase } from '../../src/handler/sendMessageBase';
|
||||
import DevChat, { ChatResponse } from '../../src/toolwrapper/devchat';
|
||||
import CommandManager from '../../src/command/commandManager';
|
||||
import messageHistory from '../../src/util/messageHistory';
|
||||
@ -68,76 +68,6 @@ describe('sendMessageBase', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getParentHash', () => {
|
||||
beforeEach(() => {
|
||||
messageHistory.clear();
|
||||
});
|
||||
|
||||
it('should return parent hash when message hash is provided and found in history', () => {
|
||||
const message1 = {
|
||||
hash: 'somehash1',
|
||||
parentHash: 'parentHash1'
|
||||
};
|
||||
const message2 = {
|
||||
hash: 'somehash2',
|
||||
parentHash: 'parentHash2'
|
||||
};
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
|
||||
const message = {
|
||||
hash: 'somehash1'
|
||||
};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.equal('parentHash1');
|
||||
});
|
||||
|
||||
it('should return undefined when message hash is provided but not found in history', () => {
|
||||
const message1 = {
|
||||
hash: 'somehash1',
|
||||
parentHash: 'parentHash1'
|
||||
};
|
||||
const message2 = {
|
||||
hash: 'somehash2',
|
||||
parentHash: 'parentHash2'
|
||||
};
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
|
||||
const message = {
|
||||
hash: 'nonexistenthash'
|
||||
};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
|
||||
it('should return last message hash when message hash is not provided', () => {
|
||||
const message1 = {
|
||||
hash: 'somehash1',
|
||||
parentHash: 'parentHash1'
|
||||
};
|
||||
const message2 = {
|
||||
hash: 'somehash2',
|
||||
parentHash: 'parentHash2'
|
||||
};
|
||||
messageHistory.add(message1);
|
||||
messageHistory.add(message2);
|
||||
|
||||
const message = {};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.equal('somehash2');
|
||||
});
|
||||
|
||||
it('should return undefined when message hash is not provided and history is empty', () => {
|
||||
const message = {};
|
||||
|
||||
const result = getParentHash(message);
|
||||
expect(result).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleTopic', () => {
|
||||
it('should handle topic correctly', async () => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user