feat: Improved file management and added new features.

- Updated file manager to use FileManager class instead of File class.
- Improved file management functionality with features for finding duplicate files, file uploading, and storage management.
- Added styles to wrap and truncate text in a no-drag area.
- Added explicit file extensions to imageExts constant.
- Added the 'paste long text as file' input setting.
- Added image file display and UI improvements for file names and overflow.
- Improved file paste and long text handling functionality.
- awaited onSendMessage function call and added message to chat completion.
- Implemented new option to paste long text as file in the Settings page.
- Updated content display logic to include file origin name along with the file content for text files.
- Improved functionality for handling image and text file contents in the Gemini chat provider.
- Updated file content formatting logic for text files with origin name and content prefix.
- Added a new setting "pasteLongTextAsFile" and its corresponding action to the application settings.
This commit is contained in:
kangfenmao 2024-09-19 10:51:30 +08:00
parent 29605fbcdb
commit af8144d45e
13 changed files with 111 additions and 66 deletions

View File

@ -3,12 +3,12 @@ import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'elect
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
import File from './services/File'
import FileManager from './services/FileManager'
import { openFile, saveFile } from './utils/file'
import { compress, decompress } from './utils/zip'
import { createMinappWindow } from './window'
const fileManager = new File()
const fileManager = new FileManager()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow)

View File

@ -6,7 +6,7 @@ import * as fs from 'fs'
import * as path from 'path'
import { v4 as uuidv4 } from 'uuid'
class File {
class FileManager {
private storageDir: string
constructor() {
@ -169,7 +169,7 @@ class File {
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
const tempFilePath = path.join(tempDir, `${uuidv4()}_${fileName}`)
const tempFilePath = path.join(tempDir, `temp_file_${uuidv4()}_${fileName}`)
return tempFilePath
}
@ -195,4 +195,4 @@ class File {
}
}
export default File
export default FileManager

View File

@ -182,6 +182,12 @@ body,
-webkit-app-region: no-drag;
}
.text-nowrap {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.minapp-drawer {
.ant-drawer-content-wrapper {
box-shadow: none;

View File

@ -8,7 +8,7 @@ export const isMac = platform === 'darwin'
export const isWindows = platform === 'win32' || platform === 'win64'
export const isLinux = platform === 'linux'
export const imageExts = ['jpg', 'png', 'jpeg']
export const imageExts = ['.jpg', '.png', '.jpeg']
export const textExts = [
'.txt', // 普通文本文件
'.md', // Markdown 文件

View File

@ -166,6 +166,7 @@ const resources = {
'messages.input.title': 'Input Settings',
'messages.input.show_estimated_tokens': 'Show estimated input tokens',
'messages.input.send_shortcuts': 'Send shortcuts',
'messages.input.paste_long_text_as_file': 'Paste long text as file',
'general.title': 'General Settings',
'general.user_name': 'User Name',
'general.user_name.placeholder': 'Enter your name',
@ -435,6 +436,7 @@ const resources = {
'messages.input.title': '输入设置',
'messages.input.show_estimated_tokens': '状态显示',
'messages.input.send_shortcuts': '发送快捷键',
'messages.input.paste_long_text_as_file': '长文本粘贴为文件',
'general.title': '常规设置',
'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名',

View File

@ -12,14 +12,14 @@ import styled from 'styled-components'
const FilesPage: FC = () => {
const { t } = useTranslation()
const files = useLiveQuery<FileType[]>(() => db.files.toArray())
const files = useLiveQuery<FileType[]>(() => db.files.orderBy('created_at').reverse().toArray())
const dataSource = files?.map((file) => {
const isImage = file.type === FileTypes.IMAGE
const ImageView = <Image src={'file://' + file.path} preview={false} style={{ maxHeight: '40px' }} />
return {
key: file.id,
file: isImage ? ImageView : file.origin_name,
file: isImage ? ImageView : <FileNameText className="text-nowrap">{file.origin_name}</FileNameText>,
name: <a href={'file://' + getFileDirectory(file.path)}>{file.origin_name}</a>,
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
created_at: dayjs(file.created_at).format('MM-DD HH:mm')
@ -84,4 +84,10 @@ const ContentContainer = styled.div`
padding: 20px;
`
const FileNameText = styled.div`
font-size: 14px;
color: var(--color-text);
max-width: 300px;
`
export default FilesPage

View File

@ -8,7 +8,8 @@ import {
PauseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons'
import { textExts } from '@renderer/config/constant'
import { imageExts, textExts } from '@renderer/config/constant'
import { isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
@ -45,7 +46,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState(_text)
const [inputFocus, setInputFocus] = useState(false)
const { addTopic, model } = useAssistant(assistant.id)
const { sendMessageShortcut, fontSize } = useSettings()
const { sendMessageShortcut, fontSize, pasteLongTextAsFile } = useSettings()
const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
const [contextCount, setContextCount] = useState(0)
@ -58,6 +59,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const { searching } = useRuntime()
const dispatch = useAppDispatch()
const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...(isVision ? imageExts : [])], [isVision])
_text = text
const sendMessage = useCallback(async () => {
@ -172,43 +176,52 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const onInput = () => !expended && resizeTextArea()
const onPaste = useCallback(async (event: ClipboardEvent) => {
for (const file of event.clipboardData?.files || []) {
event.preventDefault()
const ext = getFileExtension(file.path)
if (textExts.includes(ext)) {
const selectedFile = await window.api.file.get(file.path)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
}
}
const onPaste = useCallback(
async (event: ClipboardEvent) => {
for (const file of event.clipboardData?.files || []) {
event.preventDefault()
if (event.clipboardData?.items) {
const item = event.clipboardData.items[0]
const file = item.getAsFile()
if (file && file.type.startsWith('image/')) {
const tempFilePath = await window.api.file.create(file.name)
const arrayBuffer = await file.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
await window.api.file.write(tempFilePath, uint8Array)
const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
if (file.path === '') {
if (file.type.startsWith('image/')) {
const tempFilePath = await window.api.file.create(file.name)
const arrayBuffer = await file.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
await window.api.file.write(tempFilePath, uint8Array)
const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
break
}
}
if (file.path) {
if (supportExts.includes(getFileExtension(file.path))) {
const selectedFile = await window.api.file.get(file.path)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
}
}
}
// if (item.kind === 'string' && item.type === 'text/plain') {
// // 处理文本内容
// await new Promise<void>((resolve) => {
// item.getAsString(async (text) => {
// const tempFilePath = await window.api.file.create('pasted_text.txt')
// await window.api.file.write(tempFilePath, text)
// const selectedFile = await window.api.file.get(tempFilePath)
// if (selectedFile) {
// newFiles.push(selectedFile)
// }
// resolve()
// })
// })
// }
}
}, [])
if (pasteLongTextAsFile) {
const item = event.clipboardData?.items[0]
if (item && item.kind === 'string' && item.type === 'text/plain') {
event.preventDefault()
item.getAsString(async (text) => {
if (text.length > 1500) {
console.debug(item.getAsFile())
const tempFilePath = await window.api.file.create('pasted_text.txt')
await window.api.file.write(tempFilePath, text)
const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
} else {
setText((prevText) => prevText + text)
setTimeout(() => resizeTextArea(), 0)
}
})
}
}
},
[supportExts, pasteLongTextAsFile]
)
// Command or Ctrl + N create new topic
useEffect(() => {

View File

@ -66,7 +66,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
onSendMessage(msg)
await onSendMessage(msg)
fetchChatCompletion({
assistant,
messages: [...messages, msg],

View File

@ -8,6 +8,7 @@ import { useAppDispatch } from '@renderer/store'
import {
setFontSize,
setMessageFont,
setPasteLongTextAsFile,
setShowInputEstimatedTokens,
setShowMessageDivider
} from '@renderer/store/settings'
@ -33,8 +34,14 @@ const SettingsTab: FC<Props> = (props) => {
const dispatch = useAppDispatch()
const { showMessageDivider, messageFont, showInputEstimatedTokens, sendMessageShortcut, setSendMessageShortcut } =
useSettings()
const {
showMessageDivider,
messageFont,
showInputEstimatedTokens,
sendMessageShortcut,
setSendMessageShortcut,
pasteLongTextAsFile
} = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
updateAssistantSettings({
@ -210,6 +217,15 @@ const SettingsTab: FC<Props> = (props) => {
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
<Switch
size="small"
checked={pasteLongTextAsFile}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
</SettingRow>

View File

@ -34,9 +34,10 @@ export default class AnthropicProvider extends BaseProvider {
})
}
if (file.type === FileTypes.TEXT) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
parts.push({
type: 'text',
text: (await window.api.file.read(file.id + file.ext)).trimEnd()
text: file.origin_name + '\n' + fileContent
})
}
}

View File

@ -4,7 +4,7 @@ import { EVENT_NAMES } from '@renderer/services/event'
import { filterContextMessages, filterMessages } from '@renderer/services/messages'
import { Assistant, FileTypes, Message, Provider, Suggestion } from '@renderer/types'
import axios from 'axios'
import { flatten, isEmpty, takeRight } from 'lodash'
import { isEmpty, takeRight } from 'lodash'
import OpenAI from 'openai'
import BaseProvider from './BaseProvider'
@ -20,12 +20,7 @@ export default class GeminiProvider extends BaseProvider {
private async getMessageContents(message: Message): Promise<Content> {
const role = message.role === 'user' ? 'user' : 'model'
const parts: Part[] = [
{
type: 'text',
text: message.content
} as TextPart
]
const parts: Part[] = [{ text: message.content }]
for (const file of message.files || []) {
if (file.type === FileTypes.IMAGE) {
@ -38,15 +33,16 @@ export default class GeminiProvider extends BaseProvider {
} as InlineDataPart)
}
if (file.type === FileTypes.TEXT) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
parts.push({
text: await window.api.file.read(file.id + file.ext)
text: file.origin_name + '\n' + fileContent
} as TextPart)
}
}
return {
role,
parts: parts
parts
}
}
@ -60,14 +56,12 @@ export default class GeminiProvider extends BaseProvider {
const userLastMessage = userMessages.pop()
let historyContents: Content[][] = []
const history: Content[] = []
for (const message of userMessages) {
historyContents = historyContents.concat(await this.getMessageContents(message))
history.push(await this.getMessageContents(message))
}
const history = flatten(historyContents)
const geminiModel = this.sdk.getGenerativeModel({
model: model.id,
systemInstruction: assistant.prompt,
@ -79,7 +73,7 @@ export default class GeminiProvider extends BaseProvider {
const chat = geminiModel.startChat({ history })
const messageContents = await this.getMessageContents(userLastMessage!)
const userMessagesStream = await chat.sendMessageStream(messageContents[0].parts)
const userMessagesStream = await chat.sendMessageStream(messageContents.parts)
for await (const chunk of userMessagesStream.stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break

View File

@ -50,9 +50,10 @@ export default class OpenAIProvider extends BaseProvider {
})
}
if (file.type === FileTypes.TEXT) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
parts.push({
type: 'text',
text: await window.api.file.read(file.id + file.ext)
text: file.origin_name + '\n' + fileContent
})
}
}

View File

@ -17,6 +17,7 @@ export interface SettingsState {
windowStyle: 'transparent' | 'opaque'
fontSize: number
topicPosition: 'left' | 'right'
pasteLongTextAsFile: boolean
}
const initialState: SettingsState = {
@ -32,7 +33,8 @@ const initialState: SettingsState = {
theme: ThemeMode.light,
windowStyle: 'opaque',
fontSize: 14,
topicPosition: 'right'
topicPosition: 'right',
pasteLongTextAsFile: true
}
const settingsSlice = createSlice({
@ -84,6 +86,9 @@ const settingsSlice = createSlice({
},
setTopicPosition: (state, action: PayloadAction<'left' | 'right'>) => {
state.topicPosition = action.payload
},
setPasteLongTextAsFile: (state, action: PayloadAction<boolean>) => {
state.pasteLongTextAsFile = action.payload
}
}
})
@ -103,7 +108,8 @@ export const {
setTheme,
setFontSize,
setWindowStyle,
setTopicPosition
setTopicPosition,
setPasteLongTextAsFile
} = settingsSlice.actions
export default settingsSlice.reducer