save conversation button
This commit is contained in:
parent
7f85a08508
commit
2beddab114
@ -10,6 +10,8 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a *App) SaveJson(fileName string, jsonData any) error {
|
func (a *App) SaveJson(fileName string, jsonData any) error {
|
||||||
@ -119,6 +121,26 @@ func (a *App) CopyFile(src string, dst string) error {
|
|||||||
return nil
|
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 {
|
func (a *App) OpenFileFolder(path string, relative bool) error {
|
||||||
var absPath string
|
var absPath string
|
||||||
var err error
|
var err error
|
||||||
|
@ -146,5 +146,8 @@
|
|||||||
"Are you sure you want to reset this page? It cannot be undone.": "你确定要重置本页吗?这无法撤销",
|
"Are you sure you want to reset this page? It cannot be undone.": "你确定要重置本页吗?这无法撤销",
|
||||||
"Model file download is not complete": "模型文件下载未完成",
|
"Model file download is not complete": "模型文件下载未完成",
|
||||||
"Error": "错误",
|
"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": "打开"
|
||||||
}
|
}
|
@ -10,12 +10,14 @@ import { ConversationPair, getConversationPairs, Record } from '../utils/get-con
|
|||||||
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';
|
||||||
import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular } from '@fluentui/react-icons';
|
import { ArrowCircleUp28Regular, Delete28Regular, RecordStop28Regular, Save28Regular } from '@fluentui/react-icons';
|
||||||
import { CopyButton } from '../components/CopyButton';
|
import { CopyButton } from '../components/CopyButton';
|
||||||
import { ReadButton } from '../components/ReadButton';
|
import { ReadButton } from '../components/ReadButton';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { WorkHeader } from '../components/WorkHeader';
|
import { WorkHeader } from '../components/WorkHeader';
|
||||||
import { DialogButton } from '../components/DialogButton';
|
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 userName = 'M E';
|
||||||
export const botName = 'A I';
|
export const botName = 'A I';
|
||||||
@ -40,7 +42,7 @@ export type MessageItem = {
|
|||||||
done: boolean
|
done: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Conversations = {
|
export type Conversation = {
|
||||||
[uuid: string]: MessageItem
|
[uuid: string]: MessageItem
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,9 +56,9 @@ const ChatPanel: FC = observer(() => {
|
|||||||
|
|
||||||
let lastMessageId: string;
|
let lastMessageId: string;
|
||||||
let generating: boolean = false;
|
let generating: boolean = false;
|
||||||
if (commonStore.conversationsOrder.length > 0) {
|
if (commonStore.conversationOrder.length > 0) {
|
||||||
lastMessageId = commonStore.conversationsOrder[commonStore.conversationsOrder.length - 1];
|
lastMessageId = commonStore.conversationOrder[commonStore.conversationOrder.length - 1];
|
||||||
const lastMessage = commonStore.conversations[lastMessageId];
|
const lastMessage = commonStore.conversation[lastMessageId];
|
||||||
if (lastMessage.sender === botName)
|
if (lastMessage.sender === botName)
|
||||||
generating = !lastMessage.done;
|
generating = !lastMessage.done;
|
||||||
}
|
}
|
||||||
@ -68,9 +70,9 @@ const ChatPanel: FC = observer(() => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (commonStore.conversationsOrder.length === 0) {
|
if (commonStore.conversationOrder.length === 0) {
|
||||||
commonStore.setConversationsOrder(['welcome']);
|
commonStore.setConversationOrder(['welcome']);
|
||||||
commonStore.setConversations({
|
commonStore.setConversation({
|
||||||
'welcome': {
|
'welcome': {
|
||||||
sender: botName,
|
sender: botName,
|
||||||
type: MessageType.Normal,
|
type: MessageType.Normal,
|
||||||
@ -106,7 +108,7 @@ const ChatPanel: FC = observer(() => {
|
|||||||
|
|
||||||
const onSubmit = (message: string) => {
|
const onSubmit = (message: string) => {
|
||||||
const newId = uuid();
|
const newId = uuid();
|
||||||
commonStore.conversations[newId] = {
|
commonStore.conversation[newId] = {
|
||||||
sender: userName,
|
sender: userName,
|
||||||
type: MessageType.Normal,
|
type: MessageType.Normal,
|
||||||
color: 'brand',
|
color: 'brand',
|
||||||
@ -115,19 +117,19 @@ const ChatPanel: FC = observer(() => {
|
|||||||
side: 'right',
|
side: 'right',
|
||||||
done: true
|
done: true
|
||||||
};
|
};
|
||||||
commonStore.setConversations(commonStore.conversations);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.conversationsOrder.push(newId);
|
commonStore.conversationOrder.push(newId);
|
||||||
commonStore.setConversationsOrder(commonStore.conversationsOrder);
|
commonStore.setConversationOrder(commonStore.conversationOrder);
|
||||||
|
|
||||||
const records: Record[] = [];
|
const records: Record[] = [];
|
||||||
commonStore.conversationsOrder.forEach((uuid, index) => {
|
commonStore.conversationOrder.forEach((uuid, index) => {
|
||||||
const conversation = commonStore.conversations[uuid];
|
const messageItem = commonStore.conversation[uuid];
|
||||||
if (conversation.done && conversation.type === MessageType.Normal && conversation.sender === botName) {
|
if (messageItem.done && messageItem.type === MessageType.Normal && messageItem.sender === botName) {
|
||||||
if (index > 0) {
|
if (index > 0) {
|
||||||
const questionId = commonStore.conversationsOrder[index - 1];
|
const questionId = commonStore.conversationOrder[index - 1];
|
||||||
const question = commonStore.conversations[questionId];
|
const question = commonStore.conversation[questionId];
|
||||||
if (question.done && question.type === MessageType.Normal && question.sender === userName) {
|
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 });
|
(messages as ConversationPair[]).push({ role: 'user', content: message });
|
||||||
|
|
||||||
const answerId = uuid();
|
const answerId = uuid();
|
||||||
commonStore.conversations[answerId] = {
|
commonStore.conversation[answerId] = {
|
||||||
sender: botName,
|
sender: botName,
|
||||||
type: MessageType.Normal,
|
type: MessageType.Normal,
|
||||||
color: 'colorful',
|
color: 'colorful',
|
||||||
@ -146,9 +148,9 @@ const ChatPanel: FC = observer(() => {
|
|||||||
side: 'left',
|
side: 'left',
|
||||||
done: false
|
done: false
|
||||||
};
|
};
|
||||||
commonStore.setConversations(commonStore.conversations);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.conversationsOrder.push(answerId);
|
commonStore.conversationOrder.push(answerId);
|
||||||
commonStore.setConversationsOrder(commonStore.conversationsOrder);
|
commonStore.setConversationOrder(commonStore.conversationOrder);
|
||||||
setTimeout(scrollToBottom);
|
setTimeout(scrollToBottom);
|
||||||
let answer = '';
|
let answer = '';
|
||||||
chatSseController = new AbortController();
|
chatSseController = new AbortController();
|
||||||
@ -169,10 +171,10 @@ const ChatPanel: FC = observer(() => {
|
|||||||
console.log('sse message', e);
|
console.log('sse message', e);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
if (e.data === '[DONE]') {
|
if (e.data === '[DONE]') {
|
||||||
commonStore.conversations[answerId].done = true;
|
commonStore.conversation[answerId].done = true;
|
||||||
commonStore.conversations[answerId].content = commonStore.conversations[answerId].content.trim();
|
commonStore.conversation[answerId].content = commonStore.conversation[answerId].content.trim();
|
||||||
commonStore.setConversations(commonStore.conversations);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let data;
|
let data;
|
||||||
@ -184,19 +186,19 @@ 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.conversations[answerId].content = answer;
|
commonStore.conversation[answerId].content = answer;
|
||||||
commonStore.setConversations(commonStore.conversations);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onclose() {
|
onclose() {
|
||||||
console.log('Connection closed');
|
console.log('Connection closed');
|
||||||
},
|
},
|
||||||
onerror(err) {
|
onerror(err) {
|
||||||
commonStore.conversations[answerId].type = MessageType.Error;
|
commonStore.conversation[answerId].type = MessageType.Error;
|
||||||
commonStore.conversations[answerId].done = true;
|
commonStore.conversation[answerId].done = true;
|
||||||
commonStore.setConversations(commonStore.conversations);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -205,13 +207,13 @@ const ChatPanel: FC = observer(() => {
|
|||||||
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.conversationsOrder.map((uuid, index) => {
|
{commonStore.conversationOrder.map((uuid, index) => {
|
||||||
const conversation = commonStore.conversations[uuid];
|
const messageItem = commonStore.conversation[uuid];
|
||||||
return <div
|
return <div
|
||||||
key={uuid}
|
key={uuid}
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'flex gap-2 mb-2 overflow-hidden',
|
'flex gap-2 mb-2 overflow-hidden',
|
||||||
conversation.side === 'left' ? 'flex-row' : 'flex-row-reverse'
|
messageItem.side === 'left' ? 'flex-row' : 'flex-row-reverse'
|
||||||
)}
|
)}
|
||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
const utils = document.getElementById('utils-' + uuid);
|
const utils = document.getElementById('utils-' + uuid);
|
||||||
@ -223,29 +225,29 @@ const ChatPanel: FC = observer(() => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
color={conversation.color}
|
color={messageItem.color}
|
||||||
name={conversation.sender}
|
name={messageItem.sender}
|
||||||
image={conversation.avatarImg ? { src: conversation.avatarImg } : undefined}
|
image={messageItem.avatarImg ? { src: messageItem.avatarImg } : undefined}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className={classnames(
|
className={classnames(
|
||||||
'p-2 rounded-lg overflow-hidden',
|
'p-2 rounded-lg overflow-hidden',
|
||||||
conversation.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
|
messageItem.side === 'left' ? 'bg-gray-200' : 'bg-blue-500',
|
||||||
conversation.side === 'left' ? 'text-gray-600' : 'text-white'
|
messageItem.side === 'left' ? 'text-gray-600' : 'text-white'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MarkdownRender>{conversation.content}</MarkdownRender>
|
<MarkdownRender>{messageItem.content}</MarkdownRender>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1 items-start">
|
<div className="flex flex-col gap-1 items-start">
|
||||||
<div className="grow" />
|
<div className="grow" />
|
||||||
{(conversation.type === MessageType.Error || !conversation.done) &&
|
{(messageItem.type === MessageType.Error || !messageItem.done) &&
|
||||||
<PresenceBadge size="extra-small" status={
|
<PresenceBadge size="extra-small" status={
|
||||||
conversation.type === MessageType.Error ? 'busy' : 'away'
|
messageItem.type === MessageType.Error ? 'busy' : 'away'
|
||||||
} />
|
} />
|
||||||
}
|
}
|
||||||
<div className="flex invisible" id={'utils-' + uuid}>
|
<div className="flex invisible" id={'utils-' + uuid}>
|
||||||
<ReadButton content={conversation.content} />
|
<ReadButton content={messageItem.content} />
|
||||||
<CopyButton content={conversation.content} />
|
<CopyButton content={messageItem.content} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
@ -259,8 +261,8 @@ const ChatPanel: FC = observer(() => {
|
|||||||
onConfirm={() => {
|
onConfirm={() => {
|
||||||
if (generating)
|
if (generating)
|
||||||
chatSseController?.abort();
|
chatSseController?.abort();
|
||||||
commonStore.setConversations({});
|
commonStore.setConversation({});
|
||||||
commonStore.setConversationsOrder([]);
|
commonStore.setConversationOrder([]);
|
||||||
}} />
|
}} />
|
||||||
<Textarea
|
<Textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@ -278,15 +280,34 @@ const ChatPanel: FC = observer(() => {
|
|||||||
if (generating) {
|
if (generating) {
|
||||||
chatSseController?.abort();
|
chatSseController?.abort();
|
||||||
if (lastMessageId) {
|
if (lastMessageId) {
|
||||||
commonStore.conversations[lastMessageId].type = MessageType.Error;
|
commonStore.conversation[lastMessageId].type = MessageType.Error;
|
||||||
commonStore.conversations[lastMessageId].done = true;
|
commonStore.conversation[lastMessageId].done = true;
|
||||||
commonStore.setConversations(commonStore.conversations);
|
commonStore.setConversation(commonStore.conversation);
|
||||||
commonStore.setConversationsOrder([...commonStore.conversationsOrder]);
|
commonStore.setConversationOrder([...commonStore.conversationOrder]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handleKeyDownOrClick(e);
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import { getUserLanguage, isSystemLightMode, saveConfigs } from '../utils';
|
|||||||
import { WindowSetDarkTheme, WindowSetLightTheme } from '../../wailsjs/runtime';
|
import { WindowSetDarkTheme, WindowSetLightTheme } from '../../wailsjs/runtime';
|
||||||
import manifest from '../../../manifest.json';
|
import manifest from '../../../manifest.json';
|
||||||
import { ModelConfig } from '../pages/Configs';
|
import { ModelConfig } from '../pages/Configs';
|
||||||
import { Conversations } from '../pages/Chat';
|
import { Conversation } from '../pages/Chat';
|
||||||
import { ModelSourceItem } from '../pages/Models';
|
import { ModelSourceItem } from '../pages/Models';
|
||||||
import { DownloadStatus } from '../pages/Downloads';
|
import { DownloadStatus } from '../pages/Downloads';
|
||||||
import { SettingsType } from '../pages/Settings';
|
import { SettingsType } from '../pages/Settings';
|
||||||
@ -42,8 +42,8 @@ class CommonStore {
|
|||||||
introduction: IntroductionContent = manifest.introduction;
|
introduction: IntroductionContent = manifest.introduction;
|
||||||
// chat
|
// chat
|
||||||
currentInput: string = '';
|
currentInput: string = '';
|
||||||
conversations: Conversations = {};
|
conversation: Conversation = {};
|
||||||
conversationsOrder: string[] = [];
|
conversationOrder: string[] = [];
|
||||||
// completion
|
// completion
|
||||||
completionPreset: CompletionPreset | null = null;
|
completionPreset: CompletionPreset | null = null;
|
||||||
completionGenerating: boolean = false;
|
completionGenerating: boolean = false;
|
||||||
@ -164,12 +164,12 @@ class CommonStore {
|
|||||||
this.downloadList = value;
|
this.downloadList = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConversations = (value: Conversations) => {
|
setConversation = (value: Conversation) => {
|
||||||
this.conversations = value;
|
this.conversation = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
setConversationsOrder = (value: string[]) => {
|
setConversationOrder = (value: string[]) => {
|
||||||
this.conversationsOrder = value;
|
this.conversationOrder = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
setCompletionPreset(value: CompletionPreset) {
|
setCompletionPreset(value: CompletionPreset) {
|
||||||
|
2
frontend/wailsjs/go/backend_golang/App.d.ts
generated
vendored
2
frontend/wailsjs/go/backend_golang/App.d.ts
generated
vendored
@ -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 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 PauseDownload(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function ReadFileInfo(arg1:string):Promise<backend_golang.FileInfo>;
|
export function ReadFileInfo(arg1:string):Promise<backend_golang.FileInfo>;
|
||||||
|
4
frontend/wailsjs/go/backend_golang/App.js
generated
4
frontend/wailsjs/go/backend_golang/App.js
generated
@ -50,6 +50,10 @@ export function OpenFileFolder(arg1, arg2) {
|
|||||||
return window['go']['backend_golang']['App']['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) {
|
export function PauseDownload(arg1) {
|
||||||
return window['go']['backend_golang']['App']['PauseDownload'](arg1);
|
return window['go']['backend_golang']['App']['PauseDownload'](arg1);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user