save conversation button

This commit is contained in:
josc146 2023-06-16 00:12:13 +08:00
parent 7f85a08508
commit 2beddab114
6 changed files with 112 additions and 60 deletions

View File

@ -10,6 +10,8 @@ import (
"runtime"
"strings"
"time"
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)
func (a *App) SaveJson(fileName string, jsonData any) error {
@ -119,6 +121,26 @@ func (a *App) CopyFile(src string, dst string) error {
return nil
}
func (a *App) OpenSaveFileDialog(filterPattern string, defaultFileName string, savedContent string) (string, error) {
path, err := wruntime.SaveFileDialog(a.ctx, wruntime.SaveDialogOptions{
DefaultFilename: defaultFileName,
Filters: []wruntime.FileFilter{{
Pattern: filterPattern,
}},
CanCreateDirectories: true,
})
if err != nil {
return "", err
}
if path == "" {
return "", nil
}
if err := os.WriteFile(path, []byte(savedContent), 0644); err != nil {
return "", err
}
return path, nil
}
func (a *App) OpenFileFolder(path string, relative bool) error {
var absPath string
var err error

View File

@ -146,5 +146,8 @@
"Are you sure you want to reset this page? It cannot be undone.": "你确定要重置本页吗?这无法撤销",
"Model file download is not complete": "模型文件下载未完成",
"Error": "错误",
"Are you sure you want to clear the conversation? It cannot be undone.": "你确定要清空对话吗?这无法撤销"
"Are you sure you want to clear the conversation? It cannot be undone.": "你确定要清空对话吗?这无法撤销",
"Save": "保存",
"Conversation Saved": "对话已保存",
"Open": "打开"
}

View File

@ -10,12 +10,14 @@ import { ConversationPair, getConversationPairs, Record } from '../utils/get-con
import logo from '../assets/images/logo.jpg';
import MarkdownRender from '../components/MarkdownRender';
import { ToolTipButton } from '../components/ToolTipButton';
import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular } from '@fluentui/react-icons';
import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular, Save28Regular } from '@fluentui/react-icons';
import { CopyButton } from '../components/CopyButton';
import { ReadButton } from '../components/ReadButton';
import { toast } from 'react-toastify';
import { WorkHeader } from '../components/WorkHeader';
import { DialogButton } from '../components/DialogButton';
import { OpenFileFolder, OpenSaveFileDialog } from '../../wailsjs/go/backend_golang/App';
import { toastWithButton } from '../utils';
export const userName = 'M E';
export const botName = 'A I';
@ -40,7 +42,7 @@ export type MessageItem = {
done: boolean
}
export type Conversations = {
export type Conversation = {
[uuid: string]: MessageItem
}
@ -54,9 +56,9 @@ const ChatPanel: FC = observer(() => {
let lastMessageId: string;
let generating: boolean = false;
if (commonStore.conversationsOrder.length > 0) {
lastMessageId = commonStore.conversationsOrder[commonStore.conversationsOrder.length - 1];
const lastMessage = commonStore.conversations[lastMessageId];
if (commonStore.conversationOrder.length > 0) {
lastMessageId = commonStore.conversationOrder[commonStore.conversationOrder.length - 1];
const lastMessage = commonStore.conversation[lastMessageId];
if (lastMessage.sender === botName)
generating = !lastMessage.done;
}
@ -68,9 +70,9 @@ const ChatPanel: FC = observer(() => {
}, []);
useEffect(() => {
if (commonStore.conversationsOrder.length === 0) {
commonStore.setConversationsOrder(['welcome']);
commonStore.setConversations({
if (commonStore.conversationOrder.length === 0) {
commonStore.setConversationOrder(['welcome']);
commonStore.setConversation({
'welcome': {
sender: botName,
type: MessageType.Normal,
@ -106,7 +108,7 @@ const ChatPanel: FC = observer(() => {
const onSubmit = (message: string) => {
const newId = uuid();
commonStore.conversations[newId] = {
commonStore.conversation[newId] = {
sender: userName,
type: MessageType.Normal,
color: 'brand',
@ -115,19 +117,19 @@ const ChatPanel: FC = observer(() => {
side: 'right',
done: true
};
commonStore.setConversations(commonStore.conversations);
commonStore.conversationsOrder.push(newId);
commonStore.setConversationsOrder(commonStore.conversationsOrder);
commonStore.setConversation(commonStore.conversation);
commonStore.conversationOrder.push(newId);
commonStore.setConversationOrder(commonStore.conversationOrder);
const records: Record[] = [];
commonStore.conversationsOrder.forEach((uuid, index) => {
const conversation = commonStore.conversations[uuid];
if (conversation.done && conversation.type === MessageType.Normal && conversation.sender === botName) {
commonStore.conversationOrder.forEach((uuid, index) => {
const messageItem = commonStore.conversation[uuid];
if (messageItem.done && messageItem.type === MessageType.Normal && messageItem.sender === botName) {
if (index > 0) {
const questionId = commonStore.conversationsOrder[index - 1];
const question = commonStore.conversations[questionId];
const questionId = commonStore.conversationOrder[index - 1];
const question = commonStore.conversation[questionId];
if (question.done && question.type === MessageType.Normal && question.sender === userName) {
records.push({ question: question.content, answer: conversation.content });
records.push({ question: question.content, answer: messageItem.content });
}
}
}
@ -136,7 +138,7 @@ const ChatPanel: FC = observer(() => {
(messages as ConversationPair[]).push({ role: 'user', content: message });
const answerId = uuid();
commonStore.conversations[answerId] = {
commonStore.conversation[answerId] = {
sender: botName,
type: MessageType.Normal,
color: 'colorful',
@ -146,9 +148,9 @@ const ChatPanel: FC = observer(() => {
side: 'left',
done: false
};
commonStore.setConversations(commonStore.conversations);
commonStore.conversationsOrder.push(answerId);
commonStore.setConversationsOrder(commonStore.conversationsOrder);
commonStore.setConversation(commonStore.conversation);
commonStore.conversationOrder.push(answerId);
commonStore.setConversationOrder(commonStore.conversationOrder);
setTimeout(scrollToBottom);
let answer = '';
chatSseController = new AbortController();
@ -169,10 +171,10 @@ const ChatPanel: FC = observer(() => {
console.log('sse message', e);
scrollToBottom();
if (e.data === '[DONE]') {
commonStore.conversations[answerId].done = true;
commonStore.conversations[answerId].content = commonStore.conversations[answerId].content.trim();
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
commonStore.conversation[answerId].done = true;
commonStore.conversation[answerId].content = commonStore.conversation[answerId].content.trim();
commonStore.setConversation(commonStore.conversation);
commonStore.setConversationOrder([...commonStore.conversationOrder]);
return;
}
let data;
@ -184,19 +186,19 @@ const ChatPanel: FC = observer(() => {
}
if (data.choices && Array.isArray(data.choices) && data.choices.length > 0) {
answer += data.choices[0]?.delta?.content || '';
commonStore.conversations[answerId].content = answer;
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
commonStore.conversation[answerId].content = answer;
commonStore.setConversation(commonStore.conversation);
commonStore.setConversationOrder([...commonStore.conversationOrder]);
}
},
onclose() {
console.log('Connection closed');
},
onerror(err) {
commonStore.conversations[answerId].type = MessageType.Error;
commonStore.conversations[answerId].done = true;
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
commonStore.conversation[answerId].type = MessageType.Error;
commonStore.conversation[answerId].done = true;
commonStore.setConversation(commonStore.conversation);
commonStore.setConversationOrder([...commonStore.conversationOrder]);
throw err;
}
});
@ -205,13 +207,13 @@ const ChatPanel: FC = observer(() => {
return (
<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">
{commonStore.conversationsOrder.map((uuid, index) => {
const conversation = commonStore.conversations[uuid];
{commonStore.conversationOrder.map((uuid, index) => {
const messageItem = commonStore.conversation[uuid];
return <div
key={uuid}
className={classnames(
'flex gap-2 mb-2 overflow-hidden',
conversation.side === 'left' ? 'flex-row' : 'flex-row-reverse'
messageItem.side === 'left' ? 'flex-row' : 'flex-row-reverse'
)}
onMouseEnter={() => {
const utils = document.getElementById('utils-' + uuid);
@ -223,29 +225,29 @@ const ChatPanel: FC = observer(() => {
}}
>
<Avatar
color={conversation.color}
name={conversation.sender}
image={conversation.avatarImg ? { src: conversation.avatarImg } : undefined}
color={messageItem.color}
name={messageItem.sender}
image={messageItem.avatarImg ? { src: messageItem.avatarImg } : undefined}
/>
<div
className={classnames(
'p-2 rounded-lg overflow-hidden',
conversation.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
conversation.side === 'left' ? 'text-gray-600' : 'text-white'
messageItem.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
messageItem.side === 'left' ? 'text-gray-600' : 'text-white'
)}
>
<MarkdownRender>{conversation.content}</MarkdownRender>
<MarkdownRender>{messageItem.content}</MarkdownRender>
</div>
<div className="flex flex-col gap-1 items-start">
<div className="grow" />
{(conversation.type === MessageType.Error || !conversation.done) &&
{(messageItem.type === MessageType.Error || !messageItem.done) &&
<PresenceBadge size="extra-small" status={
conversation.type === MessageType.Error ? 'busy' : 'away'
messageItem.type === MessageType.Error ? 'busy' : 'away'
} />
}
<div className="flex invisible" id={'utils-' + uuid}>
<ReadButton content={conversation.content} />
<CopyButton content={conversation.content} />
<ReadButton content={messageItem.content} />
<CopyButton content={messageItem.content} />
</div>
</div>
</div>;
@ -259,8 +261,8 @@ const ChatPanel: FC = observer(() => {
onConfirm={() => {
if (generating)
chatSseController?.abort();
commonStore.setConversations({});
commonStore.setConversationsOrder([]);
commonStore.setConversation({});
commonStore.setConversationOrder([]);
}} />
<Textarea
ref={inputRef}
@ -278,15 +280,34 @@ const ChatPanel: FC = observer(() => {
if (generating) {
chatSseController?.abort();
if (lastMessageId) {
commonStore.conversations[lastMessageId].type = MessageType.Error;
commonStore.conversations[lastMessageId].done = true;
commonStore.setConversations(commonStore.conversations);
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
commonStore.conversation[lastMessageId].type = MessageType.Error;
commonStore.conversation[lastMessageId].done = true;
commonStore.setConversation(commonStore.conversation);
commonStore.setConversationOrder([...commonStore.conversationOrder]);
}
} else {
handleKeyDownOrClick(e);
}
}} />
<ToolTipButton desc={t('Save')}
icon={<Save28Regular />}
size="large" shape="circular" appearance="subtle"
onClick={() => {
let savedContent: string = '';
commonStore.conversationOrder.forEach((uuid) => {
const messageItem = commonStore.conversation[uuid];
savedContent += `**${messageItem.sender}**\n - ${new Date(messageItem.time).toLocaleString()}\n\n${messageItem.content}\n\n`;
});
OpenSaveFileDialog('*.md', 'conversation.md', savedContent).then((path) => {
if (path)
toastWithButton(t('Conversation Saved'), t('Open'), () => {
OpenFileFolder(path, false);
});
}).catch(e => {
toast(t('Error') + ' - ' + e.message || e, { type: 'error', autoClose: 2500 });
});
}} />
</div>
</div>
);

View File

@ -3,7 +3,7 @@ import { getUserLanguage, isSystemLightMode, saveConfigs } from '../utils';
import { WindowSetDarkTheme, WindowSetLightTheme } from '../../wailsjs/runtime';
import manifest from '../../../manifest.json';
import { ModelConfig } from '../pages/Configs';
import { Conversations } from '../pages/Chat';
import { Conversation } from '../pages/Chat';
import { ModelSourceItem } from '../pages/Models';
import { DownloadStatus } from '../pages/Downloads';
import { SettingsType } from '../pages/Settings';
@ -42,8 +42,8 @@ class CommonStore {
introduction: IntroductionContent = manifest.introduction;
// chat
currentInput: string = '';
conversations: Conversations = {};
conversationsOrder: string[] = [];
conversation: Conversation = {};
conversationOrder: string[] = [];
// completion
completionPreset: CompletionPreset | null = null;
completionGenerating: boolean = false;
@ -164,12 +164,12 @@ class CommonStore {
this.downloadList = value;
};
setConversations = (value: Conversations) => {
this.conversations = value;
setConversation = (value: Conversation) => {
this.conversation = value;
};
setConversationsOrder = (value: string[]) => {
this.conversationsOrder = value;
setConversationOrder = (value: string[]) => {
this.conversationOrder = value;
};
setCompletionPreset(value: CompletionPreset) {

View File

@ -26,6 +26,8 @@ export function ListDirFiles(arg1:string):Promise<Array<backend_golang.FileInfo>
export function OpenFileFolder(arg1:string,arg2:boolean):Promise<void>;
export function OpenSaveFileDialog(arg1:string,arg2:string,arg3:string):Promise<string>;
export function PauseDownload(arg1:string):Promise<void>;
export function ReadFileInfo(arg1:string):Promise<backend_golang.FileInfo>;

View File

@ -50,6 +50,10 @@ export function OpenFileFolder(arg1, arg2) {
return window['go']['backend_golang']['App']['OpenFileFolder'](arg1, arg2);
}
export function OpenSaveFileDialog(arg1, arg2, arg3) {
return window['go']['backend_golang']['App']['OpenSaveFileDialog'](arg1, arg2, arg3);
}
export function PauseDownload(arg1) {
return window['go']['backend_golang']['App']['PauseDownload'](arg1);
}