add more chat utils (retry, edit, delete)
This commit is contained in:
parent
35a7437714
commit
4cd5a56070
@ -154,5 +154,8 @@
|
|||||||
"Restart": "重启",
|
"Restart": "重启",
|
||||||
"API Chat Model Name": "API聊天模型名",
|
"API Chat Model Name": "API聊天模型名",
|
||||||
"API Completion Model Name": "API补全模型名",
|
"API Completion Model Name": "API补全模型名",
|
||||||
"Localhost": "本地"
|
"Localhost": "本地",
|
||||||
|
"Retry": "重试",
|
||||||
|
"Delete": "删除",
|
||||||
|
"Edit": "编辑"
|
||||||
}
|
}
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { ClipboardSetText } from '../../wailsjs/runtime';
|
import { ClipboardSetText } from '../../wailsjs/runtime';
|
||||||
import { ToolTipButton } from './ToolTipButton';
|
import { ToolTipButton } from './ToolTipButton';
|
||||||
|
|
||||||
export const CopyButton: FC<{ content: string }> = ({ content }) => {
|
export const CopyButton: FC<{ content: string, showDelay?: number, }> = ({ content, showDelay = 0 }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
@ -19,7 +19,8 @@ export const CopyButton: FC<{ content: string }> = ({ content }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolTipButton desc={t('Copy')} size="small" appearance="subtle" icon={copied ? <CheckIcon /> : <CopyIcon />}
|
<ToolTipButton desc={t('Copy')} size="small" appearance="subtle" showDelay={showDelay}
|
||||||
|
icon={copied ? <CheckIcon /> : <CopyIcon />}
|
||||||
onClick={onClick} />
|
onClick={onClick} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -7,13 +7,28 @@ import { observer } from 'mobx-react-lite';
|
|||||||
|
|
||||||
const synth = window.speechSynthesis;
|
const synth = window.speechSynthesis;
|
||||||
|
|
||||||
export const ReadButton: FC<{ content: string }> = observer(({ content }) => {
|
export const ReadButton: FC<{
|
||||||
|
content: string,
|
||||||
|
inSpeaking?: boolean,
|
||||||
|
showDelay?: number,
|
||||||
|
setSpeakingOuter?: (speaking: boolean) => void
|
||||||
|
}> = observer(({
|
||||||
|
content,
|
||||||
|
inSpeaking = false,
|
||||||
|
showDelay = 0,
|
||||||
|
setSpeakingOuter
|
||||||
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [speaking, setSpeaking] = useState(false);
|
const [speaking, setSpeaking] = useState(inSpeaking);
|
||||||
let lang: string = commonStore.settings.language;
|
let lang: string = commonStore.settings.language;
|
||||||
if (lang === 'dev')
|
if (lang === 'dev')
|
||||||
lang = 'en';
|
lang = 'en';
|
||||||
|
|
||||||
|
const setSpeakingInner = (speaking: boolean) => {
|
||||||
|
setSpeakingOuter?.(speaking);
|
||||||
|
setSpeaking(speaking);
|
||||||
|
};
|
||||||
|
|
||||||
const startSpeak = () => {
|
const startSpeak = () => {
|
||||||
synth.cancel();
|
synth.cancel();
|
||||||
|
|
||||||
@ -31,22 +46,22 @@ export const ReadButton: FC<{ content: string }> = observer(({ content }) => {
|
|||||||
Object.assign(utterance, {
|
Object.assign(utterance, {
|
||||||
rate: 1,
|
rate: 1,
|
||||||
volume: 1,
|
volume: 1,
|
||||||
onend: () => setSpeaking(false),
|
onend: () => setSpeakingInner(false),
|
||||||
onerror: () => setSpeaking(false),
|
onerror: () => setSpeakingInner(false),
|
||||||
voice: voice
|
voice: voice
|
||||||
});
|
});
|
||||||
|
|
||||||
synth.speak(utterance);
|
synth.speak(utterance);
|
||||||
setSpeaking(true);
|
setSpeakingInner(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopSpeak = () => {
|
const stopSpeak = () => {
|
||||||
synth.cancel();
|
synth.cancel();
|
||||||
setSpeaking(false);
|
setSpeakingInner(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolTipButton desc={t('Read Aloud')} size="small" appearance="subtle"
|
<ToolTipButton desc={t('Read Aloud')} size="small" appearance="subtle" showDelay={showDelay}
|
||||||
icon={speaking ? <MuteIcon /> : <UnmuteIcon />}
|
icon={speaking ? <MuteIcon /> : <UnmuteIcon />}
|
||||||
onClick={speaking ? stopSpeak : startSpeak} />
|
onClick={speaking ? stopSpeak : startSpeak} />
|
||||||
);
|
);
|
||||||
|
@ -11,6 +11,7 @@ export const ToolTipButton: FC<{
|
|||||||
appearance?: 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent';
|
appearance?: 'secondary' | 'primary' | 'outline' | 'subtle' | 'transparent';
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
onClick?: MouseEventHandler
|
onClick?: MouseEventHandler
|
||||||
|
showDelay?: number,
|
||||||
}> = ({
|
}> = ({
|
||||||
text,
|
text,
|
||||||
desc,
|
desc,
|
||||||
@ -20,10 +21,11 @@ export const ToolTipButton: FC<{
|
|||||||
shape,
|
shape,
|
||||||
appearance,
|
appearance,
|
||||||
disabled,
|
disabled,
|
||||||
onClick
|
onClick,
|
||||||
|
showDelay = 0
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Tooltip content={desc} showDelay={0} hideDelay={0} relationship="label">
|
<Tooltip content={desc} showDelay={showDelay} hideDelay={0} relationship="label">
|
||||||
<Button className={className} disabled={disabled} icon={icon} onClick={onClick} size={size} shape={shape}
|
<Button className={className} disabled={disabled} icon={icon} onClick={onClick} size={size} shape={shape}
|
||||||
appearance={appearance}>{text}</Button>
|
appearance={appearance}>{text}</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import React, { FC, useEffect, useRef } from 'react';
|
import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Avatar, PresenceBadge, Textarea } from '@fluentui/react-components';
|
import { Avatar, Button, Menu, MenuPopover, MenuTrigger, PresenceBadge, Textarea } from '@fluentui/react-components';
|
||||||
import commonStore, { ModelStatus } from '../stores/commonStore';
|
import commonStore, { ModelStatus } from '../stores/commonStore';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||||
import { ConversationPair, getConversationPairs, Record } from '../utils/get-conversation-pairs';
|
import { KebabHorizontalIcon, PencilIcon, SyncIcon, TrashIcon } from '@primer/octicons-react';
|
||||||
|
import { ConversationPair } from '../utils/get-conversation-pairs';
|
||||||
import logo from '../assets/images/logo.jpg';
|
import logo from '../assets/images/logo.jpg';
|
||||||
import MarkdownRender from '../components/MarkdownRender';
|
import MarkdownRender from '../components/MarkdownRender';
|
||||||
import { ToolTipButton } from '../components/ToolTipButton';
|
import { ToolTipButton } from '../components/ToolTipButton';
|
||||||
@ -22,6 +23,8 @@ import { toastWithButton } from '../utils';
|
|||||||
export const userName = 'M E';
|
export const userName = 'M E';
|
||||||
export const botName = 'A I';
|
export const botName = 'A I';
|
||||||
|
|
||||||
|
export const welcomeUuid = 'welcome';
|
||||||
|
|
||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
Normal,
|
Normal,
|
||||||
Error
|
Error
|
||||||
@ -48,6 +51,122 @@ export type Conversation = {
|
|||||||
|
|
||||||
let chatSseController: AbortController | null = null;
|
let chatSseController: AbortController | null = null;
|
||||||
|
|
||||||
|
const MoreUtilsButton: FC<{ uuid: string, setEditing: (editing: boolean) => void }> = observer(({
|
||||||
|
uuid,
|
||||||
|
setEditing
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [speaking, setSpeaking] = useState(false);
|
||||||
|
|
||||||
|
const messageItem = commonStore.conversation[uuid];
|
||||||
|
|
||||||
|
return <Menu>
|
||||||
|
<MenuTrigger disableButtonEnhancement>
|
||||||
|
<Button icon={<KebabHorizontalIcon />} size="small" appearance="subtle" />
|
||||||
|
</MenuTrigger>
|
||||||
|
<MenuPopover style={{ minWidth: 0 }}>
|
||||||
|
<ReadButton content={messageItem.content} inSpeaking={speaking} showDelay={500} setSpeakingOuter={setSpeaking} />
|
||||||
|
<ToolTipButton desc={t('Edit')} icon={<PencilIcon />} showDelay={500} size="small" appearance="subtle"
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(true);
|
||||||
|
}} />
|
||||||
|
<ToolTipButton desc={t('Delete')} icon={<TrashIcon />} showDelay={500} size="small" appearance="subtle"
|
||||||
|
onClick={() => {
|
||||||
|
commonStore.conversationOrder.splice(commonStore.conversationOrder.indexOf(uuid), 1);
|
||||||
|
delete commonStore.conversation[uuid];
|
||||||
|
}} />
|
||||||
|
</MenuPopover>
|
||||||
|
</Menu>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChatMessageItem: FC<{
|
||||||
|
uuid: string, onSubmit: (message: string | null, answerId: string | null,
|
||||||
|
startUuid: string | null, endUuid: string | null, includeEndUuid: boolean) => void
|
||||||
|
}> = observer(({ uuid, onSubmit }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const messageItem = commonStore.conversation[uuid];
|
||||||
|
|
||||||
|
console.log(uuid);
|
||||||
|
|
||||||
|
const setEditingInner = (editing: boolean) => {
|
||||||
|
setEditing(editing);
|
||||||
|
if (editing) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const textarea = textareaRef.current;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.focus();
|
||||||
|
textarea.selectionStart = textarea.value.length;
|
||||||
|
textarea.selectionEnd = textarea.value.length;
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <div
|
||||||
|
className={classnames(
|
||||||
|
'flex gap-2 mb-2 overflow-hidden',
|
||||||
|
messageItem.side === 'left' ? 'flex-row' : 'flex-row-reverse'
|
||||||
|
)}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
const utils = document.getElementById('utils-' + uuid);
|
||||||
|
if (utils) utils.classList.remove('invisible');
|
||||||
|
}}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
const utils = document.getElementById('utils-' + uuid);
|
||||||
|
if (utils) utils.classList.add('invisible');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
color={messageItem.color}
|
||||||
|
name={messageItem.sender}
|
||||||
|
image={messageItem.avatarImg ? { src: messageItem.avatarImg } : undefined}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={classnames(
|
||||||
|
'flex p-2 rounded-lg overflow-hidden',
|
||||||
|
editing ? 'grow' : '',
|
||||||
|
messageItem.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
|
||||||
|
messageItem.side === 'left' ? 'text-gray-600' : 'text-white'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!editing ?
|
||||||
|
<MarkdownRender>{messageItem.content}</MarkdownRender> :
|
||||||
|
<Textarea ref={textareaRef}
|
||||||
|
className="grow"
|
||||||
|
style={{ minWidth: 0 }}
|
||||||
|
value={messageItem.content}
|
||||||
|
onChange={(e) => {
|
||||||
|
messageItem.content = e.target.value;
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setEditingInner(false);
|
||||||
|
}} />}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1 items-start">
|
||||||
|
<div className="grow" />
|
||||||
|
{(messageItem.type === MessageType.Error || !messageItem.done) &&
|
||||||
|
<PresenceBadge size="extra-small" status={
|
||||||
|
messageItem.type === MessageType.Error ? 'busy' : 'away'
|
||||||
|
} />
|
||||||
|
}
|
||||||
|
<div className="flex invisible" id={'utils-' + uuid}>
|
||||||
|
{
|
||||||
|
messageItem.sender === botName && uuid !== welcomeUuid &&
|
||||||
|
<ToolTipButton desc={t('Retry')} size="small" appearance="subtle"
|
||||||
|
icon={<SyncIcon />} onClick={() => {
|
||||||
|
onSubmit(null, uuid, null, uuid, false);
|
||||||
|
}} />
|
||||||
|
}
|
||||||
|
<CopyButton content={messageItem.content} />
|
||||||
|
<MoreUtilsButton uuid={uuid} setEditing={setEditingInner} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
const ChatPanel: FC = observer(() => {
|
const ChatPanel: FC = observer(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const bodyRef = useRef<HTMLDivElement>(null);
|
const bodyRef = useRef<HTMLDivElement>(null);
|
||||||
@ -71,9 +190,9 @@ const ChatPanel: FC = observer(() => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (commonStore.conversationOrder.length === 0) {
|
if (commonStore.conversationOrder.length === 0) {
|
||||||
commonStore.setConversationOrder(['welcome']);
|
commonStore.setConversationOrder([welcomeUuid]);
|
||||||
commonStore.setConversation({
|
commonStore.setConversation({
|
||||||
'welcome': {
|
[welcomeUuid]: {
|
||||||
sender: botName,
|
sender: botName,
|
||||||
type: MessageType.Normal,
|
type: MessageType.Normal,
|
||||||
color: 'colorful',
|
color: 'colorful',
|
||||||
@ -106,38 +225,48 @@ const ChatPanel: FC = observer(() => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = (message: string) => {
|
// if message is not null, create a user message;
|
||||||
const newId = uuid();
|
// if answerId is not null, override the answer with new response;
|
||||||
commonStore.conversation[newId] = {
|
// if startUuid is null, start generating api body messages from first message;
|
||||||
sender: userName,
|
// if endUuid is null, generate api body messages until last message;
|
||||||
type: MessageType.Normal,
|
const onSubmit = useCallback((message: string | null = null, answerId: string | null = null,
|
||||||
color: 'brand',
|
startUuid: string | null = null, endUuid: string | null = null, includeEndUuid: boolean = false) => {
|
||||||
time: new Date().toISOString(),
|
if (message) {
|
||||||
content: message,
|
const newId = uuid();
|
||||||
side: 'right',
|
commonStore.conversation[newId] = {
|
||||||
done: true
|
sender: userName,
|
||||||
};
|
type: MessageType.Normal,
|
||||||
commonStore.setConversation(commonStore.conversation);
|
color: 'brand',
|
||||||
commonStore.conversationOrder.push(newId);
|
time: new Date().toISOString(),
|
||||||
commonStore.setConversationOrder(commonStore.conversationOrder);
|
content: message,
|
||||||
|
side: 'right',
|
||||||
|
done: true
|
||||||
|
};
|
||||||
|
commonStore.setConversation(commonStore.conversation);
|
||||||
|
commonStore.conversationOrder.push(newId);
|
||||||
|
commonStore.setConversationOrder(commonStore.conversationOrder);
|
||||||
|
}
|
||||||
|
|
||||||
const records: Record[] = [];
|
let startIndex = startUuid ? commonStore.conversationOrder.indexOf(startUuid) : 0;
|
||||||
commonStore.conversationOrder.forEach((uuid, index) => {
|
let endIndex = endUuid ? (commonStore.conversationOrder.indexOf(endUuid) + (includeEndUuid ? 1 : 0)) : commonStore.conversationOrder.length;
|
||||||
|
let targetRange = commonStore.conversationOrder.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const messages: ConversationPair[] = [];
|
||||||
|
targetRange.forEach((uuid, index) => {
|
||||||
|
if (uuid === welcomeUuid)
|
||||||
|
return;
|
||||||
const messageItem = commonStore.conversation[uuid];
|
const messageItem = commonStore.conversation[uuid];
|
||||||
if (messageItem.done && messageItem.type === MessageType.Normal && messageItem.sender === botName) {
|
if (messageItem.done && messageItem.type === MessageType.Normal && messageItem.sender === userName) {
|
||||||
if (index > 0) {
|
messages.push({ role: 'user', content: messageItem.content });
|
||||||
const questionId = commonStore.conversationOrder[index - 1];
|
} else if (messageItem.done && messageItem.type === MessageType.Normal && messageItem.sender === botName) {
|
||||||
const question = commonStore.conversation[questionId];
|
messages.push({ role: 'assistant', content: messageItem.content });
|
||||||
if (question.done && question.type === MessageType.Normal && question.sender === userName) {
|
|
||||||
records.push({ question: question.content, answer: messageItem.content });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const messages = getConversationPairs(records, false);
|
|
||||||
(messages as ConversationPair[]).push({ role: 'user', content: message });
|
|
||||||
|
|
||||||
const answerId = uuid();
|
if (answerId === null) {
|
||||||
|
answerId = uuid();
|
||||||
|
commonStore.conversationOrder.push(answerId);
|
||||||
|
}
|
||||||
commonStore.conversation[answerId] = {
|
commonStore.conversation[answerId] = {
|
||||||
sender: botName,
|
sender: botName,
|
||||||
type: MessageType.Normal,
|
type: MessageType.Normal,
|
||||||
@ -149,7 +278,6 @@ const ChatPanel: FC = observer(() => {
|
|||||||
done: false
|
done: false
|
||||||
};
|
};
|
||||||
commonStore.setConversation(commonStore.conversation);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.conversationOrder.push(answerId);
|
|
||||||
commonStore.setConversationOrder(commonStore.conversationOrder);
|
commonStore.setConversationOrder(commonStore.conversationOrder);
|
||||||
setTimeout(scrollToBottom);
|
setTimeout(scrollToBottom);
|
||||||
let answer = '';
|
let answer = '';
|
||||||
@ -171,11 +299,10 @@ const ChatPanel: FC = observer(() => {
|
|||||||
}),
|
}),
|
||||||
signal: chatSseController?.signal,
|
signal: chatSseController?.signal,
|
||||||
onmessage(e) {
|
onmessage(e) {
|
||||||
console.log('sse message', e);
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
if (e.data === '[DONE]') {
|
if (e.data === '[DONE]') {
|
||||||
commonStore.conversation[answerId].done = true;
|
commonStore.conversation[answerId!].done = true;
|
||||||
commonStore.conversation[answerId].content = commonStore.conversation[answerId].content.trim();
|
commonStore.conversation[answerId!].content = commonStore.conversation[answerId!].content.trim();
|
||||||
commonStore.setConversation(commonStore.conversation);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
return;
|
return;
|
||||||
@ -189,14 +316,14 @@ const ChatPanel: FC = observer(() => {
|
|||||||
}
|
}
|
||||||
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
|
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
|
||||||
answer += data.choices[0]?.delta?.content || '';
|
answer += data.choices[0]?.delta?.content || '';
|
||||||
commonStore.conversation[answerId].content = answer;
|
commonStore.conversation[answerId!].content = answer;
|
||||||
commonStore.setConversation(commonStore.conversation);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onopen(response) {
|
async onopen(response) {
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
commonStore.conversation[answerId].content += '\n[ERROR]\n```\n' + response.statusText + '\n' + (await response.text()) + '\n```';
|
commonStore.conversation[answerId!].content += '\n[ERROR]\n```\n' + response.statusText + '\n' + (await response.text()) + '\n```';
|
||||||
commonStore.setConversation(commonStore.conversation);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
setTimeout(scrollToBottom);
|
setTimeout(scrollToBottom);
|
||||||
@ -206,67 +333,25 @@ const ChatPanel: FC = observer(() => {
|
|||||||
console.log('Connection closed');
|
console.log('Connection closed');
|
||||||
},
|
},
|
||||||
onerror(err) {
|
onerror(err) {
|
||||||
commonStore.conversation[answerId].type = MessageType.Error;
|
commonStore.conversation[answerId!].type = MessageType.Error;
|
||||||
commonStore.conversation[answerId].done = true;
|
commonStore.conversation[answerId!].done = true;
|
||||||
err = err.message || err;
|
err = err.message || err;
|
||||||
if (err && !err.includes('ReadableStreamDefaultReader'))
|
if (err && !err.includes('ReadableStreamDefaultReader'))
|
||||||
commonStore.conversation[answerId].content += '\n[ERROR]\n```\n' + err + '\n```';
|
commonStore.conversation[answerId!].content += '\n[ERROR]\n```\n' + err + '\n```';
|
||||||
commonStore.setConversation(commonStore.conversation);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
setTimeout(scrollToBottom);
|
setTimeout(scrollToBottom);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full grow gap-4 pt-4 overflow-hidden">
|
<div className="flex flex-col w-full grow gap-4 pt-4 overflow-hidden">
|
||||||
<div ref={bodyRef} className="grow overflow-y-scroll overflow-x-hidden pr-2">
|
<div ref={bodyRef} className="grow overflow-y-scroll overflow-x-hidden pr-2">
|
||||||
{commonStore.conversationOrder.map((uuid, index) => {
|
{commonStore.conversationOrder.map(uuid =>
|
||||||
const messageItem = commonStore.conversation[uuid];
|
<ChatMessageItem key={uuid} uuid={uuid} onSubmit={onSubmit} />
|
||||||
return <div
|
)}
|
||||||
key={uuid}
|
|
||||||
className={classnames(
|
|
||||||
'flex gap-2 mb-2 overflow-hidden',
|
|
||||||
messageItem.side === 'left' ? 'flex-row' : 'flex-row-reverse'
|
|
||||||
)}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
const utils = document.getElementById('utils-' + uuid);
|
|
||||||
if (utils) utils.classList.remove('invisible');
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
const utils = document.getElementById('utils-' + uuid);
|
|
||||||
if (utils) utils.classList.add('invisible');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
color={messageItem.color}
|
|
||||||
name={messageItem.sender}
|
|
||||||
image={messageItem.avatarImg ? { src: messageItem.avatarImg } : undefined}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={classnames(
|
|
||||||
'p-2 rounded-lg overflow-hidden',
|
|
||||||
messageItem.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
|
|
||||||
messageItem.side === 'left' ? 'text-gray-600' : 'text-white'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MarkdownRender>{messageItem.content}</MarkdownRender>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 items-start">
|
|
||||||
<div className="grow" />
|
|
||||||
{(messageItem.type === MessageType.Error || !messageItem.done) &&
|
|
||||||
<PresenceBadge size="extra-small" status={
|
|
||||||
messageItem.type === MessageType.Error ? 'busy' : 'away'
|
|
||||||
} />
|
|
||||||
}
|
|
||||||
<div className="flex invisible" id={'utils-' + uuid}>
|
|
||||||
<ReadButton content={messageItem.content} />
|
|
||||||
<CopyButton content={messageItem.content} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end gap-2">
|
<div className="flex items-end gap-2">
|
||||||
<DialogButton tooltip={t('Clear')}
|
<DialogButton tooltip={t('Clear')}
|
||||||
|
Loading…
Reference in New Issue
Block a user