feat: save file to disk

This commit is contained in:
kangfenmao 2024-07-26 09:50:29 +08:00
parent d7b8721848
commit 40e76f3e53
8 changed files with 75 additions and 9 deletions

24
src/main/event.ts Normal file
View File

@ -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<void> {
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)
}
}

View File

@ -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('触发检查更新')

View File

@ -11,6 +11,7 @@ declare global {
checkForUpdate: () => void
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
saveFile: (path: string, content: string) => void
}
}
}

View File

@ -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

View File

@ -22,3 +22,7 @@ export function useShowAssistants() {
toggleShowAssistants: () => dispatch(toggleShowAssistants())
}
}
export function useRuntime() {
return useAppSelector((state) => state.runtime)
}

View File

@ -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': '你好,我是默认助手。你可以立刻开始跟我聊天。',

View File

@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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: <SaveOutlined />,
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<Props> = ({ message, index, showMenu, onDeleteMessage }) =
{getMessageContent(message)}
</Markdown>
)}
{message.usage && (
{message.usage && !generating && (
<MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
</MessageMetadata>
@ -140,6 +159,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</ActionButton>
</Tooltip>
)}
{!isUserMessage && (
<Dropdown menu={{ items: getDropdownMenus(message) }} trigger={['click']} placement="topRight" arrow>
<ActionButton>
<MenuOutlined />
</ActionButton>
</Dropdown>
)}
</MenusBar>
)}
</MessageContent>

View File

@ -93,7 +93,7 @@ const Tab = styled.div`
color: #8a8a8a;
border-bottom: 1px solid transparent;
&.active {
color: #a8a8a8;
color: #bbb;
font-weight: 600;
}
`