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 { join } from 'path'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import AppUpdater from './updater' import AppUpdater from './updater'
import { saveFile } from './event'
function createWindow() { function createWindow() {
// Load the previous state with fallback to defaults // Load the previous state with fallback to defaults
@ -115,6 +116,8 @@ app.whenReady().then(() => {
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {}) session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
}) })
ipcMain.handle('save-file', saveFile)
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法) // 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => { ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新') autoUpdater.logger?.info('触发检查更新')

View File

@ -11,6 +11,7 @@ declare global {
checkForUpdate: () => void checkForUpdate: () => void
openWebsite: (url: string) => void openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => 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'), getAppInfo: () => ipcRenderer.invoke('get-app-info'),
checkForUpdate: () => ipcRenderer.invoke('check-for-update'), checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url), 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 // Use `contextBridge` APIs to expose Electron APIs to

View File

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

View File

@ -24,7 +24,8 @@ const resources = {
copy: 'Copy', copy: 'Copy',
regenerate: 'Regenerate', regenerate: 'Regenerate',
provider: 'Provider', provider: 'Provider',
you: 'You' you: 'You',
save: 'Save'
}, },
button: { button: {
add: 'Add', add: 'Add',
@ -46,6 +47,9 @@ const resources = {
'chat.completion.paused': 'Chat completion paused', 'chat.completion.paused': 'Chat completion paused',
'switch.disabled': 'Switching is disabled while the assistant is generating' 'switch.disabled': 'Switching is disabled while the assistant is generating'
}, },
chat: {
save: 'Save'
},
assistant: { assistant: {
'default.name': '😀 Default Assistant', 'default.name': '😀 Default Assistant',
'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away", 'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
@ -197,6 +201,9 @@ const resources = {
'chat.completion.paused': '会话已停止', 'chat.completion.paused': '会话已停止',
'switch.disabled': '模型回复完成后才能切换' 'switch.disabled': '模型回复完成后才能切换'
}, },
chat: {
save: '保存'
},
assistant: { assistant: {
'default.name': '😃 默认助手 - Assistant', 'default.name': '😃 默认助手 - Assistant',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。', '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 Logo from '@renderer/assets/images/logo.png'
import { getModelLogo } from '@renderer/config/provider' import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant' 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 { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message } from '@renderer/types' import { Message } from '@renderer/types'
import { firstLetter } from '@renderer/utils' import { firstLetter } from '@renderer/utils'
import { Avatar, Tooltip } from 'antd' import { Avatar, Dropdown, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isEmpty, upperFirst } from 'lodash' import { isEmpty, upperFirst } from 'lodash'
import { FC } from 'react' import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
import CodeBlock from './CodeBlock' import CodeBlock from './CodeBlock'
import { useRuntime } from '@renderer/hooks/useStore'
interface Props { interface Props {
message: Message message: Message
@ -29,6 +30,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const { t } = useTranslation() const { t } = useTranslation()
const { assistant } = useAssistant(message.assistantId) const { assistant } = useAssistant(message.assistantId)
const { userName, showMessageDivider, messageFont } = useSettings() const { userName, showMessageDivider, messageFont } = useSettings()
const { generating } = useRuntime()
const isLastMessage = index === 0 const isLastMessage = index === 0
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
@ -66,7 +68,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
return message.content return message.content
} }
const getUserName = () => { const getUserName = useCallback(() => {
if (message.id === 'assistant') { if (message.id === 'assistant') {
return assistant.name return assistant.name
} }
@ -76,7 +78,24 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
} }
return userName || t('common.you') 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 fontFamily = messageFont === 'serif' ? "Georgia, Cambria, 'Times New Roman', Times, serif" : undefined
const messageBorder = showMessageDivider ? undefined : 'none' const messageBorder = showMessageDivider ? undefined : 'none'
@ -109,7 +128,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
{getMessageContent(message)} {getMessageContent(message)}
</Markdown> </Markdown>
)} )}
{message.usage && ( {message.usage && !generating && (
<MessageMetadata> <MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens} Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
</MessageMetadata> </MessageMetadata>
@ -140,6 +159,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
{!isUserMessage && (
<Dropdown menu={{ items: getDropdownMenus(message) }} trigger={['click']} placement="topRight" arrow>
<ActionButton>
<MenuOutlined />
</ActionButton>
</Dropdown>
)}
</MenusBar> </MenusBar>
)} )}
</MessageContent> </MessageContent>

View File

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