diff --git a/src/main/event.ts b/src/main/event.ts new file mode 100644 index 00000000..47785a20 --- /dev/null +++ b/src/main/event.ts @@ -0,0 +1,24 @@ +import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron' +import { writeFile } from 'fs' +import logger from 'electron-log' + +export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise { + try { + const options: SaveDialogOptions = { + title: '保存文件', + defaultPath: fileName + } + + const result: SaveDialogReturnValue = await dialog.showSaveDialog(options) + + if (!result.canceled && result.filePath) { + writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => { + if (err) { + logger.error('[IPC - Error]', 'An error occurred saving the file:', err) + } + }) + } + } catch (err) { + logger.error('[IPC - Error]', 'An error occurred saving the file:', err) + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 9e268035..e9a3ed82 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -6,6 +6,7 @@ import windowStateKeeper from 'electron-window-state' import { join } from 'path' import icon from '../../resources/icon.png?asset' import AppUpdater from './updater' +import { saveFile } from './event' function createWindow() { // Load the previous state with fallback to defaults @@ -115,6 +116,8 @@ app.whenReady().then(() => { session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {}) }) + ipcMain.handle('save-file', saveFile) + // 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法) ipcMain.handle('check-for-update', async () => { autoUpdater.logger?.info('触发检查更新') diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index fbb92686..a08c333c 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -11,6 +11,7 @@ declare global { checkForUpdate: () => void openWebsite: (url: string) => void setProxy: (proxy: string | undefined) => void + saveFile: (path: string, content: string) => void } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index 0bfaa489..8f1cb7bf 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -6,7 +6,8 @@ const api = { getAppInfo: () => ipcRenderer.invoke('get-app-info'), checkForUpdate: () => ipcRenderer.invoke('check-for-update'), openWebsite: (url: string) => ipcRenderer.invoke('open-website', url), - setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy) + setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy), + saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content) } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/hooks/useStore.ts b/src/renderer/src/hooks/useStore.ts index d1959437..51e0981b 100644 --- a/src/renderer/src/hooks/useStore.ts +++ b/src/renderer/src/hooks/useStore.ts @@ -22,3 +22,7 @@ export function useShowAssistants() { toggleShowAssistants: () => dispatch(toggleShowAssistants()) } } + +export function useRuntime() { + return useAppSelector((state) => state.runtime) +} diff --git a/src/renderer/src/i18n/index.ts b/src/renderer/src/i18n/index.ts index 2c3e119e..4ccd1b35 100644 --- a/src/renderer/src/i18n/index.ts +++ b/src/renderer/src/i18n/index.ts @@ -24,7 +24,8 @@ const resources = { copy: 'Copy', regenerate: 'Regenerate', provider: 'Provider', - you: 'You' + you: 'You', + save: 'Save' }, button: { add: 'Add', @@ -46,6 +47,9 @@ const resources = { 'chat.completion.paused': 'Chat completion paused', 'switch.disabled': 'Switching is disabled while the assistant is generating' }, + chat: { + save: 'Save' + }, assistant: { 'default.name': '😀 Default Assistant', 'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away", @@ -197,6 +201,9 @@ const resources = { 'chat.completion.paused': '会话已停止', 'switch.disabled': '模型回复完成后才能切换' }, + chat: { + save: '保存' + }, assistant: { 'default.name': '😃 默认助手 - Assistant', 'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。', diff --git a/src/renderer/src/pages/home/components/Message.tsx b/src/renderer/src/pages/home/components/Message.tsx index 854d3b1f..586c2881 100644 --- a/src/renderer/src/pages/home/components/Message.tsx +++ b/src/renderer/src/pages/home/components/Message.tsx @@ -1,4 +1,4 @@ -import { CopyOutlined, DeleteOutlined, EditOutlined, SyncOutlined } from '@ant-design/icons' +import { CopyOutlined, DeleteOutlined, EditOutlined, MenuOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons' import Logo from '@renderer/assets/images/logo.png' import { getModelLogo } from '@renderer/config/provider' import { useAssistant } from '@renderer/hooks/useAssistant' @@ -7,14 +7,15 @@ import { useSettings } from '@renderer/hooks/useSettings' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { Message } from '@renderer/types' import { firstLetter } from '@renderer/utils' -import { Avatar, Tooltip } from 'antd' +import { Avatar, Dropdown, Tooltip } from 'antd' import dayjs from 'dayjs' import { isEmpty, upperFirst } from 'lodash' -import { FC } from 'react' +import { FC, useCallback } from 'react' import { useTranslation } from 'react-i18next' import Markdown from 'react-markdown' import styled from 'styled-components' import CodeBlock from './CodeBlock' +import { useRuntime } from '@renderer/hooks/useStore' interface Props { message: Message @@ -29,6 +30,7 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = const { t } = useTranslation() const { assistant } = useAssistant(message.assistantId) const { userName, showMessageDivider, messageFont } = useSettings() + const { generating } = useRuntime() const isLastMessage = index === 0 const isUserMessage = message.role === 'user' @@ -66,7 +68,7 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = return message.content } - const getUserName = () => { + const getUserName = useCallback(() => { if (message.id === 'assistant') { return assistant.name } @@ -76,7 +78,24 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = } return userName || t('common.you') - } + }, [assistant.name, message.id, message.modelId, message.role, t, userName]) + + const getDropdownMenus = useCallback( + (message: Message) => { + return [ + { + label: t('chat.save'), + key: 'save', + icon: , + onClick: () => { + const fileName = message.createdAt + '.md' + window.api.saveFile(fileName, message.content) + } + } + ] + }, + [t] + ) const fontFamily = messageFont === 'serif' ? "Georgia, Cambria, 'Times New Roman', Times, serif" : undefined const messageBorder = showMessageDivider ? undefined : 'none' @@ -109,7 +128,7 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = {getMessageContent(message)} )} - {message.usage && ( + {message.usage && !generating && ( Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens} @@ -140,6 +159,13 @@ const MessageItem: FC = ({ message, index, showMenu, onDeleteMessage }) = )} + {!isUserMessage && ( + + + + + + )} )} diff --git a/src/renderer/src/pages/home/components/RightSidebar.tsx b/src/renderer/src/pages/home/components/RightSidebar.tsx index 08e15b2a..e02f930c 100644 --- a/src/renderer/src/pages/home/components/RightSidebar.tsx +++ b/src/renderer/src/pages/home/components/RightSidebar.tsx @@ -93,7 +93,7 @@ const Tab = styled.div` color: #8a8a8a; border-bottom: 1px solid transparent; &.active { - color: #a8a8a8; + color: #bbb; font-weight: 600; } `