save conversation button
This commit is contained in:
		
							parent
							
								
									7f85a08508
								
							
						
					
					
						commit
						2beddab114
					
				@ -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
 | 
			
		||||
 | 
			
		||||
@ -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": "打开"
 | 
			
		||||
}
 | 
			
		||||
@ -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>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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 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>;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user