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:
parent
29605fbcdb
commit
af8144d45e
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -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;
|
||||
|
||||
@ -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 文件
|
||||
|
||||
@ -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': '请输入用户名',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user