feat: save file to disk
This commit is contained in:
parent
d7b8721848
commit
40e76f3e53
24
src/main/event.ts
Normal file
24
src/main/event.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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('触发检查更新')
|
||||
|
||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@ -11,6 +11,7 @@ declare global {
|
||||
checkForUpdate: () => void
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
saveFile: (path: string, content: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -22,3 +22,7 @@ export function useShowAssistants() {
|
||||
toggleShowAssistants: () => dispatch(toggleShowAssistants())
|
||||
}
|
||||
}
|
||||
|
||||
export function useRuntime() {
|
||||
return useAppSelector((state) => state.runtime)
|
||||
}
|
||||
|
||||
@ -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': '你好,我是默认助手。你可以立刻开始跟我聊天。',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -93,7 +93,7 @@ const Tab = styled.div`
|
||||
color: #8a8a8a;
|
||||
border-bottom: 1px solid transparent;
|
||||
&.active {
|
||||
color: #a8a8a8;
|
||||
color: #bbb;
|
||||
font-weight: 600;
|
||||
}
|
||||
`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user