Compare commits

...

33 Commits

Author SHA1 Message Date
kangfenmao
c579eff86e chore(version): 0.9.5 2025-01-08 16:52:03 +08:00
kangfenmao
f9f5befc59 fix: window navbar layout 2025-01-08 14:35:48 +08:00
kangfenmao
7271a86677 style: update container component styling and navbar responsiveness 2025-01-08 13:25:34 +08:00
kangfenmao
42ede42f62 feat: narrow layout 2025-01-08 12:44:01 +08:00
kangfenmao
ea7a42f736 style: adjusted padding and container gap styles 2025-01-08 11:06:51 +08:00
kangfenmao
d2836826e7 fix: removed unnecessary conditional logic for attachment button #667 2025-01-08 10:56:22 +08:00
kangfenmao
7d61af7170 Revert "fix:修复单行CodeBlock中显示sub"
This reverts commit 09e6756efe.
2025-01-08 10:46:35 +08:00
kangfenmao
3f4fa9b0ec refactor: refactor upload component layout and styling for responsiveness #674
fix: 当插入文件过多的时候,无法看到输入框了。 close #674
2025-01-08 10:21:17 +08:00
kangfenmao
1bdf6c7955 fix: update model filtering logic to exclude empty ids #493
close #493
2025-01-08 10:00:23 +08:00
kangfenmao
5d005cf5a7 chore: standardize artifact names across platforms 2025-01-08 09:42:38 +08:00
kangfenmao
1fbd727a7b fix: @google/generative-ai local compilation issue #682
close #682
2025-01-07 23:18:18 +08:00
亢奋猫
c9813bb1e2 feature: customizable sidebar module #644 (#680)
* feat:对话的时候支持侧边栏拖拽调整宽度

* feat:对话的时候支持侧边栏拖拽调整宽度

* feat: 隐藏app sidebar 用户体验度提升,不支持隐藏对话

* fix:对话勾选知识库 国际化错误

* refactor: split the SidebarIconsManager module out of DisplaySettings

* style: update SidebarIconsManager style

* ci: fix typecheck

* Revert "feat:对话的时候支持侧边栏拖拽调整宽度"

This reverts commit 58072128f0.

* refactor: merge migrate versions

* refactor: simplify sidebarIcons data structure

* chore: move react-beautiful-dnd to dev dependencies

* chore: use @hello-pangea/dnd replace react-beautiful-dnd

* docs: update translation and formatting of input messages

---------

Co-authored-by: hxp0618 <1169924772@qq.com>
Co-authored-by: huang <hxp0618@gmail.com>
2025-01-07 19:11:12 +08:00
kangfenmao
edac2004a0 feat: add gemini files support 2025-01-07 16:49:11 +08:00
kangfenmao
a051f9fa44 feat: add optional free model tag display 2025-01-07 11:23:32 +08:00
kangfenmao
a70e69caf9 feat: enable web search for zhipu ai provider #657 2025-01-07 10:53:34 +08:00
kangfenmao
4896db93fd fix: improved error message formatting in api service 2025-01-07 10:19:21 +08:00
kangfenmao
2e7ecbc753 feat: add ModelTags component 2025-01-07 09:54:22 +08:00
kangfenmao
f68bd4d8d8 feat: add support for 'aihubmix' models and aihubmix llm provider 2025-01-07 09:46:05 +08:00
kangfenmao
d0948e6f8a feature: customizable sidebar module #644
close #644
2025-01-06 16:59:10 +08:00
kangfenmao
ac9017c031 feat: add search message shortcut #366 2025-01-06 16:29:39 +08:00
kangfenmao
de1d79abb8 fix: the minimum width limit of the window is too large #544
close #544
2025-01-06 16:25:00 +08:00
kangfenmao
ad577818dd fix: generating topic name after exporting prompt file name is invalid #641
close #641
2025-01-06 15:50:57 +08:00
kangfenmao
bb50447a98 fix: Ollama is unable to create a knowledge base using a local embedding model #630 2025-01-06 15:43:20 +08:00
kangfenmao
158f9bf1ad fix: turn off spell check #648
The next version will be released. close #648
2025-01-06 15:10:03 +08:00
kangfenmao
6a9bc103d7 feat: added optional chaining for code variable 2025-01-06 14:54:04 +08:00
xx-moos
529ec3612e fix: 修复 message 显示时间过长的问题 2025-01-06 14:43:31 +08:00
kangfenmao
d241c38c61 style: border radius use var 2025-01-04 22:50:44 +08:00
kangfenmao
ee5ed8c565 style: logo v3
# Conflicts:
#	src/renderer/src/assets/images/logo.png
2025-01-04 21:52:05 +08:00
huang
dc73661678 feat: 支持 mermaid 点击按钮放大缩小以及鼠标滑轮放大缩小 2025-01-04 19:17:39 +08:00
huang
ce973ce3a0 feat: 支持 mermaid 点击按钮放大缩小以及鼠标滑轮放大缩小 2025-01-04 19:17:39 +08:00
huang
a0413158c8 fix: 修复在macOS m1 中点击全屏幕后,点击关闭后黑屏的问题 2025-01-04 19:17:39 +08:00
kangfenmao
6cb3b16451 fix: Qwen2.5和Qwen的划分不合理 #633 2025-01-03 18:05:01 +08:00
huang
08b0990cf9 fix: 中文国际化错误 2025-01-03 17:35:17 +08:00
91 changed files with 3578 additions and 2413 deletions

View File

@@ -1,19 +0,0 @@
name: Auto Assign
on:
issues:
types: [opened]
pull_request:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: 'Auto-assign issue'
uses: pozil/auto-assign-issue@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
assignees: kangfenmao
numOfAssignee: 1

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 353 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -32,6 +32,10 @@ asarUnpack:
- '**/*.{node,dll,metal,exp,lib}' - '**/*.{node,dll,metal,exp,lib}'
win: win:
executableName: Cherry Studio executableName: Cherry Studio
artifactName: ${productName}-${version}-portable.${ext}
target:
- target: nsis
- target: portable
nsis: nsis:
artifactName: ${productName}-${version}-setup.${ext} artifactName: ${productName}-${version}-setup.${ext}
shortcutName: ${productName} shortcutName: ${productName}
@@ -43,6 +47,7 @@ nsis:
mac: mac:
entitlementsInherit: build/entitlements.mac.plist entitlementsInherit: build/entitlements.mac.plist
notarize: false notarize: false
artifactName: ${productName}-${version}-${arch}.${ext}
extendInfo: extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera. - NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone. - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -57,9 +62,8 @@ mac:
arch: arch:
- arm64 - arm64
- x64 - x64
dmg:
artifactName: ${productName}-${version}-${arch}.${ext}
linux: linux:
artifactName: ${productName}-${version}-${arch}.${ext}
target: target:
- target: AppImage - target: AppImage
arch: arch:
@@ -67,8 +71,6 @@ linux:
- x64 - x64
maintainer: electronjs.org maintainer: electronjs.org
category: Utility category: Utility
appImage:
artifactName: ${productName}-${version}-${arch}.${ext}
publish: publish:
provider: generic provider: generic
url: https://cherrystudio.ocool.online url: https://cherrystudio.ocool.online
@@ -78,8 +80,14 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
文件支持删除 全新的应用图标
增加 Hika 小程序 首页窗口宽度支持调整
增加 WebDAV 同步状态显示 内容显示支持宽窄模式
自定义参数增加 JSON 类型 增加搜索快捷键
腾讯混元的联网开关 可以自定义侧边栏图标 @hxp0618
Mermaid 预览增加放大缩小功能 @hxp0618
支持 AiHubMix 和智普联网模型
支持使用 Gemini PDF 附件使用官方 API 进行处理
文件模块增加 Gemini 文件列表
修复 Ollma 嵌入模型无法创建知识库问题
其他错误修复

View File

@@ -50,7 +50,7 @@ export default defineConfig({
} }
}, },
optimizeDeps: { optimizeDeps: {
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js'] exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js', 'chunk-AFE5XGNG.js']
} }
} }
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.9.4", "version": "0.9.5",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
@@ -50,6 +50,7 @@
"@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@electron/notarize": "^2.5.0", "@electron/notarize": "^2.5.0",
"@google/generative-ai": "^0.21.0",
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch", "@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch",
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch", "@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch",
"@llm-tools/embedjs-loader-csv": "^0.1.25", "@llm-tools/embedjs-loader-csv": "^0.1.25",
@@ -64,7 +65,6 @@
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"apache-arrow": "^18.1.0", "apache-arrow": "^18.1.0",
"docx": "^9.0.2", "docx": "^9.0.2",
"dompurify": "^3.2.3",
"electron-log": "^5.1.5", "electron-log": "^5.1.5",
"electron-store": "^8.2.0", "electron-store": "^8.2.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
@@ -81,7 +81,6 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0", "@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1", "@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",
"@google/generative-ai": "^0.21.0",
"@hello-pangea/dnd": "^16.6.0", "@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0", "@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5", "@reduxjs/toolkit": "^2.2.5",

View File

@@ -11,6 +11,7 @@ import BackupManager from './services/BackupManager'
import { configManager } from './services/ConfigManager' import { configManager } from './services/ConfigManager'
import { ExportService } from './services/ExportService' import { ExportService } from './services/ExportService'
import FileStorage from './services/FileStorage' import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService' import KnowledgeService from './services/KnowledgeService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import { windowService } from './services/WindowService' import { windowService } from './services/WindowService'
@@ -154,4 +155,24 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('knowledge-base:add', KnowledgeService.add) ipcMain.handle('knowledge-base:add', KnowledgeService.add)
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove) ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
ipcMain.handle('knowledge-base:search', KnowledgeService.search) ipcMain.handle('knowledge-base:search', KnowledgeService.search)
// window
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
mainWindow?.setMinimumSize(width, height)
})
ipcMain.handle('window:reset-minimum-size', () => {
mainWindow?.setMinimumSize(1080, 600)
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
if (width < 1080) {
mainWindow?.setSize(1080, height)
}
})
// gemini
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
} }

View File

@@ -0,0 +1,74 @@
interface CacheItem<T> {
data: T
timestamp: number
duration: number
}
export class CacheService {
private static cache: Map<string, CacheItem<any>> = new Map()
/**
* Set cache
* @param key Cache key
* @param data Cache data
* @param duration Cache duration (in milliseconds)
*/
static set<T>(key: string, data: T, duration: number): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
duration
})
}
/**
* Get cache
* @param key Cache key
* @returns Returns data if cache exists and not expired, otherwise returns null
*/
static get<T>(key: string): T | null {
const item = this.cache.get(key)
if (!item) return null
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return null
}
return item.data
}
/**
* Remove specific cache
* @param key Cache key
*/
static remove(key: string): void {
this.cache.delete(key)
}
/**
* Clear all cache
*/
static clear(): void {
this.cache.clear()
}
/**
* Check if cache exists and is valid
* @param key Cache key
* @returns boolean
*/
static has(key: string): boolean {
const item = this.cache.get(key)
if (!item) return false
const now = Date.now()
if (now - item.timestamp > item.duration) {
this.remove(key)
return false
}
return true
}
}

View File

@@ -0,0 +1,63 @@
import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server'
import { FileType } from '@types'
import fs from 'fs'
import { CacheService } from './CacheService'
export class GeminiService {
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
private static readonly CACHE_DURATION = 3000
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
const fileManager = new GoogleAIFileManager(apiKey)
const uploadResult = await fileManager.uploadFile(file.path, {
mimeType: 'application/pdf',
displayName: file.origin_name
})
return uploadResult
}
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
return {
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
mimeType: 'application/pdf'
}
}
static async retrieveFile(
_: Electron.IpcMainInvokeEvent,
file: FileType,
apiKey: string
): Promise<FileMetadataResponse | undefined> {
const fileManager = new GoogleAIFileManager(apiKey)
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
if (cachedResponse) {
return GeminiService.processResponse(cachedResponse, file)
}
const response = await fileManager.listFiles()
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
return GeminiService.processResponse(response, file)
}
private static processResponse(response: any, file: FileType) {
if (response.files) {
return response.files
.filter((file) => file.state === FileState.ACTIVE)
.find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size)
}
return undefined
}
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
const fileManager = new GoogleAIFileManager(apiKey)
return await fileManager.listFiles()
}
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
const fileManager = new GoogleAIFileManager(apiKey)
await fileManager.deleteFile(fileId)
}
}

View File

@@ -89,12 +89,14 @@ class KnowledgeService {
if (item.type === 'url') { if (item.type === 'url') {
const content = item.content as string const content = item.content as string
if (content.startsWith('http')) { if (content.startsWith('http')) {
// @ts-ignore loader type
return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload) return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload)
} }
} }
if (item.type === 'sitemap') { if (item.type === 'sitemap') {
const content = item.content as string const content = item.content as string
// @ts-ignore loader type
return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload) return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload)
} }

View File

@@ -13,6 +13,8 @@ import { configManager } from './ConfigManager'
export class WindowService { export class WindowService {
private static instance: WindowService | null = null private static instance: WindowService | null = null
private mainWindow: BrowserWindow | null = null private mainWindow: BrowserWindow | null = null
private isQuitting: boolean = false
private wasFullScreen: boolean = false
public static getInstance(): WindowService { public static getInstance(): WindowService {
if (!WindowService.instance) { if (!WindowService.instance) {
@@ -42,7 +44,7 @@ export class WindowService {
height: mainWindowState.height, height: mainWindowState.height,
minWidth: 1080, minWidth: 1080,
minHeight: 600, minHeight: 600,
show: true, show: false, // 初始不显示
autoHideMenuBar: true, autoHideMenuBar: true,
transparent: isMac, transparent: isMac,
vibrancy: 'under-window', vibrancy: 'under-window',
@@ -118,9 +120,20 @@ export class WindowService {
} }
private setupWindowEvents(mainWindow: BrowserWindow) { private setupWindowEvents(mainWindow: BrowserWindow) {
mainWindow.on('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
mainWindow.show() mainWindow.show()
}) })
// 处理全屏相关事件
mainWindow.on('enter-full-screen', () => {
this.wasFullScreen = true
mainWindow.webContents.send('fullscreen-status-changed', true)
})
mainWindow.on('leave-full-screen', () => {
this.wasFullScreen = false
mainWindow.webContents.send('fullscreen-status-changed', false)
})
} }
private setupWebContentsHandlers(mainWindow: BrowserWindow) { private setupWebContentsHandlers(mainWindow: BrowserWindow) {
@@ -182,6 +195,11 @@ export class WindowService {
} }
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) { private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
// 监听应用退出事件
app.on('before-quit', () => {
this.isQuitting = true
})
mainWindow.on('close', (event) => { mainWindow.on('close', (event) => {
const notInTray = !configManager.isTray() const notInTray = !configManager.isTray()
@@ -191,9 +209,15 @@ export class WindowService {
} }
// Mac // Mac
if (!app.isQuitting) { if (!this.isQuitting) {
event.preventDefault() if (this.wasFullScreen) {
mainWindow.hide() // 如果是全屏状态,直接退出
this.isQuitting = true
app.quit()
} else {
event.preventDefault()
mainWindow.hide()
}
} }
}) })
} }

View File

@@ -1,4 +1,5 @@
import { ElectronAPI } from '@electron-toolkit/preload' import { ElectronAPI } from '@electron-toolkit/preload'
import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server'
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces' import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import { FileType } from '@renderer/types' import { FileType } from '@renderer/types'
import { WebDavConfig } from '@renderer/types' import { WebDavConfig } from '@renderer/types'
@@ -76,6 +77,17 @@ declare global {
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void> remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]> search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
} }
window: {
setMinimumSize: (width: number, height: number) => Promise<void>
resetMinimumSize: () => Promise<void>
}
gemini: {
uploadFile: (file: FileType, apiKey: string) => Promise<UploadFileResponse>
retrieveFile: (file: FileType, apiKey: string) => Promise<FileMetadataResponse | undefined>
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
listFiles: (apiKey: string) => Promise<ListFilesResponse>
deleteFile: (apiKey: string, fileId: string) => Promise<void>
}
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { electronAPI } from '@electron-toolkit/preload' import { electronAPI } from '@electron-toolkit/preload'
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types' import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron' import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
// Custom APIs for renderer // Custom APIs for renderer
@@ -70,6 +70,17 @@ const api = {
ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }), ipcRenderer.invoke('knowledge-base:remove', { uniqueId, base }),
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
ipcRenderer.invoke('knowledge-base:search', { search, base }) ipcRenderer.invoke('knowledge-base:search', { search, base })
},
window: {
setMinimumSize: (width: number, height: number) => ipcRenderer.invoke('window:set-minimum-size', width, height),
resetMinimumSize: () => ipcRenderer.invoke('window:reset-minimum-size')
},
gemini: {
uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:upload-file', file, apiKey),
base64File: (file: FileType) => ipcRenderer.invoke('gemini:base64-file', file),
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey),
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId)
} }
} }

View File

@@ -1,88 +1,91 @@
@font-face { @font-face {
font-family: 'iconfont'; /* Project id 4753420 */ font-family: "iconfont"; /* Project id 4753420 */
src: url('iconfont.woff2?t=1733224456443') format('woff2'); src: url('iconfont.woff2?t=1736309723926') format('woff2'),
url('iconfont.woff?t=1736309723926') format('woff'),
url('iconfont.ttf?t=1736309723926') format('truetype');
} }
.iconfont { .iconfont {
font-family: 'iconfont' !important; font-family: "iconfont" !important;
font-size: 16px; font-size: 16px;
font-style: normal; font-style: normal;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-at1:before { .icon-at:before {
content: '\e7df'; content: "\e623";
} }
.icon-at:before { .icon-icon-adaptive-width:before {
content: '\e630'; content: "\e87a";
} }
.icon-a-darkmode:before { .icon-a-darkmode:before {
content: '\e6cd'; content: "\e6cd";
} }
.icon-ai-model:before { .icon-ai-model:before {
content: '\e827'; content: "\e827";
} }
.icon-ai-model1:before { .icon-ai-model1:before {
content: '\ec09'; content: "\ec09";
} }
.icon-gridlines:before { .icon-gridlines:before {
content: '\e942'; content: "\e942";
} }
.icon-inbox:before { .icon-inbox:before {
content: '\e869'; content: "\e869";
} }
.icon-business-smart-assistant:before { .icon-business-smart-assistant:before {
content: '\e601'; content: "\e601";
} }
.icon-copy:before { .icon-copy:before {
content: '\e6ae'; content: "\e6ae";
} }
.icon-ic_send:before { .icon-ic_send:before {
content: '\e795'; content: "\e795";
} }
.icon-dark1:before { .icon-dark1:before {
content: '\e72f'; content: "\e72f";
} }
.icon-theme-light:before { .icon-theme-light:before {
content: '\e6b7'; content: "\e6b7";
} }
.icon-translate_line:before { .icon-translate_line:before {
content: '\e7de'; content: "\e7de";
} }
.icon-history:before { .icon-history:before {
content: '\e758'; content: "\e758";
} }
.icon-hide-sidebar:before { .icon-hide-sidebar:before {
content: '\e8eb'; content: "\e8eb";
} }
.icon-show-sidebar:before { .icon-show-sidebar:before {
content: '\e944'; content: "\e944";
} }
.icon-appstore:before { .icon-appstore:before {
content: '\e792'; content: "\e792";
} }
.icon-chat:before { .icon-chat:before {
content: '\e615'; content: "\e615";
} }
.icon-setting:before { .icon-setting:before {
content: '\e78e'; content: "\e78e";
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -60,6 +60,8 @@
--chat-background-user: #28b561; --chat-background-user: #28b561;
--chat-background-assistant: #2c2c2c; --chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black); --chat-text-user: var(--color-black);
--list-item-border-radius: 16px;
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@@ -169,12 +171,9 @@ body,
#content-container { #content-container {
background-color: var(--color-background); background-color: var(--color-background);
border-top: 0.5px solid var(--color-border); border-top: 0.5px solid var(--color-border);
}
#content-container {
border-top-left-radius: 12px; border-top-left-radius: 12px;
border-left: 0.5px solid var(--color-border); border-left: 0.5px solid var(--color-border);
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08); box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
} }
.loader { .loader {
@@ -216,10 +215,7 @@ body,
background-color: var(--chat-background); background-color: var(--chat-background);
} }
#inputbar { #inputbar {
border-radius: 0; margin: -5px 15px 15px 15px;
margin: 0;
border: none;
border-top: 1px solid var(--color-border-mute);
background: var(--color-background); background: var(--color-background);
} }
.system-prompt { .system-prompt {

View File

@@ -10,9 +10,8 @@ interface ListItemProps {
} }
const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => { const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => {
const borderRadius = subtitle ? '10px' : '16px'
return ( return (
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={{ borderRadius }}> <ListItemContainer className={active ? 'active' : ''} onClick={onClick}>
<ListItemContent> <ListItemContent>
{icon && <IconWrapper>{icon}</IconWrapper>} {icon && <IconWrapper>{icon}</IconWrapper>}
<TextContainer> <TextContainer>
@@ -26,7 +25,7 @@ const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) =>
const ListItemContainer = styled.div` const ListItemContainer = styled.div`
padding: 7px 12px; padding: 7px 12px;
border-radius: 16px; border-radius: var(--list-item-border-radius);
font-size: 13px; font-size: 13px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -0,0 +1,36 @@
import { isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
import { Model } from '@renderer/types'
import { isFreeModel } from '@renderer/utils'
import { Tag } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import VisionIcon from './Icons/VisionIcon'
import WebSearchIcon from './Icons/WebSearchIcon'
interface ModelTagsProps {
model: Model
showFree?: boolean
}
const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true }) => {
const { t } = useTranslation()
return (
<>
{isVisionModel(model) && <VisionIcon />}
{isWebSearchModel(model) && <WebSearchIcon />}
{showFree && isFreeModel(model) && (
<Tag style={{ marginLeft: 10 }} color="green">
{t('models.free')}
</Tag>
)}
{isEmbeddingModel(model) && (
<Tag style={{ marginLeft: 10 }} color="orange">
{t('models.embedding')}
</Tag>
)}
</>
)
}
export default ModelTags

View File

@@ -48,7 +48,7 @@ const AppStorePopover: FC<Props> = ({ children }) => {
content={content} content={content}
trigger="click" trigger="click"
placement="bottomRight" placement="bottomRight"
overlayInnerStyle={{ padding: 25 }}> styles={{ body: { padding: 25 } }}>
{children} {children}
</Popover> </Popover>
) )
@@ -59,7 +59,7 @@ const PopoverContent = styled(Scrollbar)``
const AppsContainer = styled.div` const AppsContainer = styled.div`
display: grid; display: grid;
grid-template-columns: repeat(6, minmax(90px, 1fr)); grid-template-columns: repeat(6, minmax(90px, 1fr));
gap: 25px; gap: 18px;
` `
export default AppStorePopover export default AppStorePopover

View File

@@ -1,7 +1,7 @@
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons' import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon' import VisionIcon from '@renderer/components/Icons/VisionIcon'
import { TopView } from '@renderer/components/TopView' import { TopView } from '@renderer/components/TopView'
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import { getModelLogo, isEmbeddingModel, isVisionModel } from '@renderer/config/models'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
import { getModelUniqId } from '@renderer/services/ModelService' import { getModelUniqId } from '@renderer/services/ModelService'
@@ -12,8 +12,8 @@ import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import WebSearchIcon from '../Icons/WebSearchIcon'
import { HStack } from '../Layout' import { HStack } from '../Layout'
import ModelTags from '../ModelTags'
import Scrollbar from '../Scrollbar' import Scrollbar from '../Scrollbar'
type MenuItem = Required<MenuProps>['items'][number] type MenuItem = Required<MenuProps>['items'][number]
@@ -75,7 +75,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
label: ( label: (
<ModelItem> <ModelItem>
<span> <span>
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />} {m?.name} <ModelTags model={m} />
</span> </span>
<PinIcon <PinIcon
onClick={(e) => { onClick={(e) => {

View File

@@ -70,6 +70,7 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
ref={textareaRef} ref={textareaRef}
rows={2} rows={2}
autoFocus autoFocus
spellCheck={false}
{...textareaProps} {...textareaProps}
value={textValue} value={textValue}
onInput={resizeTextArea} onInput={resizeTextArea}

View File

@@ -21,7 +21,7 @@ const Sidebar: FC = () => {
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings() const { windowStyle, sidebarIcons } = useSettings()
const { theme, toggleTheme } = useTheme() const { theme, toggleTheme } = useTheme()
const isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path ? 'active' : '')
@@ -37,6 +37,41 @@ const Sidebar: FC = () => {
navigate(path) navigate(path)
} }
const renderMainMenus = () => {
return sidebarIcons.visible.map((icon) => {
const iconMap = {
assistants: <i className="iconfont icon-chat" />,
agents: <i className="iconfont icon-business-smart-assistant" />,
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
translate: <TranslationOutlined />,
minapp: <i className="iconfont icon-appstore" />,
knowledge: <FileSearchOutlined />,
files: <FolderOutlined />
}
const pathMap = {
assistants: '/',
agents: '/agents',
paintings: '/paintings',
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
}
const path = pathMap[icon]
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
return (
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(path)}>
<Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink>
</Tooltip>
)
})
}
return ( return (
<Container <Container
id="app-sidebar" id="app-sidebar"
@@ -46,61 +81,7 @@ const Sidebar: FC = () => {
}}> }}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>{renderMainMenus()}</Menus>
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/')}>
<Icon className={isRoute('/')}>
<i className="iconfont icon-chat" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('agents.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/agents')}>
<Icon className={isRoutes('/agents')}>
<i className="iconfont icon-business-smart-assistant" />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/paintings')}>
<Icon className={isRoute('/paintings')}>
<PictureOutlined style={{ fontSize: 16 }} />
</Icon>
</StyledLink>
</Tooltip>
<Tooltip title={t('translate.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/translate')}>
<Icon className={isRoute('/translate')}>
<TranslationOutlined />
</Icon>
</StyledLink>
</Tooltip>
{showMinappIcon && (
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/apps')}>
<Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore" />
</Icon>
</StyledLink>
</Tooltip>
)}
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
<StyledLink onClick={() => to('/knowledge')}>
<Icon className={isRoute('/knowledge')}>
<FileSearchOutlined />
</Icon>
</StyledLink>
</Tooltip>
{showFilesIcon && (
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to('/files')}>
<Icon className={isRoute('/files')}>
<FolderOutlined />
</Icon>
</StyledLink>
</Tooltip>
)}
</Menus>
</MainMenus> </MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">

View File

@@ -123,8 +123,11 @@ import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png' import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
import { getProviderByModel } from '@renderer/services/AssistantService' import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types' import { Model } from '@renderer/types'
import { isEmpty } from 'lodash'
import OpenAI from 'openai' import OpenAI from 'openai'
import { getWebSearchTools } from './tools'
const visionAllowedModels = [ const visionAllowedModels = [
'llava', 'llava',
'moondream', 'moondream',
@@ -262,6 +265,44 @@ export function getModelLogo(modelId: string) {
} }
export const SYSTEM_MODELS: Record<string, Model[]> = { export const SYSTEM_MODELS: Record<string, Model[]> = {
aihubmix: [
{
id: 'gpt-4o',
provider: 'aihubmix',
name: 'GPT-4o',
group: 'GPT-4o'
},
{
id: 'claude-3-5-sonnet-latest',
provider: 'aihubmix',
name: 'Claude 3.5 Sonnet',
group: 'Claude 3.5'
},
{
id: 'gemini-2.0-flash-exp-search',
provider: 'aihubmix',
name: 'Gemini 2.0 Flash Exp Search',
group: 'Gemini 2.0'
},
{
id: 'deepseek-chat',
provider: 'aihubmix',
name: 'DeepSeek Chat',
group: 'DeepSeek Chat'
},
{
id: 'aihubmix-Llama-3-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama-3.3-70b',
group: 'Llama 3.3'
},
{
id: 'Qwen/QVQ-72B-Preview',
provider: 'aihubmix',
name: 'Qwen/QVQ-72B',
group: 'Qwen'
}
],
ollama: [], ollama: [],
silicon: [ silicon: [
{ {
@@ -274,7 +315,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
id: 'Qwen/Qwen2.5-7B-Instruct', id: 'Qwen/Qwen2.5-7B-Instruct',
provider: 'silicon', provider: 'silicon',
name: 'Qwen2.5-7B-Instruct', name: 'Qwen2.5-7B-Instruct',
group: 'Qwen2.5' group: 'Qwen'
}, },
{ {
id: 'meta-llama/Llama-3.3-70B-Instruct', id: 'meta-llama/Llama-3.3-70B-Instruct',
@@ -521,9 +562,21 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
], ],
zhipu: [ zhipu: [
{ {
id: 'glm-4', id: 'glm-zero-preview',
provider: 'zhipu', provider: 'zhipu',
name: 'GLM-4', name: 'GLM-Zero-Preview',
group: 'GLM-Zero'
},
{
id: 'glm-4-0520',
provider: 'zhipu',
name: 'GLM-4-0520',
group: 'GLM-4'
},
{
id: 'glm-4-long',
provider: 'zhipu',
name: 'GLM-4-Long',
group: 'GLM-4' group: 'GLM-4'
}, },
{ {
@@ -567,6 +620,12 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
provider: 'zhipu', provider: 'zhipu',
name: 'GLM-4-AllTools', name: 'GLM-4-AllTools',
group: 'GLM-4-AllTools' group: 'GLM-4-AllTools'
},
{
id: 'embedding-3',
provider: 'zhipu',
name: 'Embedding-3',
group: 'Embedding'
} }
], ],
moonshot: [ moonshot: [
@@ -750,20 +809,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
group: 'Jina Embeddings V3' group: 'Jina Embeddings V3'
} }
], ],
aihubmix: [
{
id: 'gpt-4o-mini',
provider: 'aihubmix',
name: 'GPT-4o Mini',
group: 'GPT-4o'
},
{
id: 'aihubmix-Llama-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama 3 70B Instruct',
group: 'Llama3'
}
],
fireworks: [ fireworks: [
{ {
id: 'accounts/fireworks/models/mythomax-l2-13b', id: 'accounts/fireworks/models/mythomax-l2-13b',
@@ -1017,5 +1062,31 @@ export function isWebSearchModel(model: Model): boolean {
return model?.id !== 'hunyuan-lite' return model?.id !== 'hunyuan-lite'
} }
if (provider.id === 'aihubmix') {
return model?.id === 'gemini-2.0-flash-exp-search'
}
if (provider.id === 'zhipu') {
return model?.id?.startsWith('glm-4-')
}
return false return false
} }
export function getWebSearchParams(model: Model): Record<string, any> {
if (isWebSearchModel(model)) {
if (model.provider === 'hunyuan') {
return { enable_enhancement: true }
}
if (model.provider === 'zhipu') {
const webSearchTools = getWebSearchTools(model)
return isEmpty(webSearchTools)
? {}
: {
tools: webSearchTools
}
}
}
return {}
}

View File

@@ -0,0 +1,29 @@
import { Model } from '@renderer/types'
import { ChatCompletionTool } from 'openai/resources'
import { isWebSearchModel } from './models'
export function getWebSearchTools(model: Model): ChatCompletionTool[] {
if (model && model.provider === 'zhipu') {
if (isWebSearchModel(model)) {
if (model.id === 'glm-4-alltools') {
return [
{
type: 'web_browser'
} as unknown as ChatCompletionTool
]
}
return [
{
type: 'web_search',
web_search: {
enable: true,
search_result: true
}
} as unknown as ChatCompletionTool
]
}
}
return []
}

View File

@@ -54,13 +54,12 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
const codeToHtml = async (code: string, language: string) => { const codeToHtml = async (code: string, language: string) => {
if (!highlighter) return '' if (!highlighter) return ''
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!) const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[char]!)
try { try {
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) { if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
if (language in bundledLanguages || language === 'text') { if (language in bundledLanguages || language === 'text') {
await highlighter.loadLanguage(language as BundledLanguage) await highlighter.loadLanguage(language as BundledLanguage)
console.log(`Loaded language: ${language}`)
} else { } else {
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>` return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
} }

View File

@@ -2,7 +2,6 @@ import { isMac } from '@renderer/config/constant'
import { isLocalAi } from '@renderer/config/env' import { isLocalAi } from '@renderer/config/env'
import db from '@renderer/databases' import db from '@renderer/databases'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime' import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
import { delay, runAsyncFunction } from '@renderer/utils' import { delay, runAsyncFunction } from '@renderer/utils'
@@ -16,16 +15,7 @@ import useUpdateHandler from './useUpdateHandler'
export function useAppInit() { export function useAppInit() {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode, customCss } = useSettings()
proxyUrl,
language,
windowStyle,
manualUpdateCheck,
proxyMode,
webdavAutoSync,
webdavSyncInterval,
customCss
} = useSettings()
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel() const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar')) const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
@@ -84,10 +74,6 @@ export function useAppInit() {
}) })
}, [dispatch]) }, [dispatch])
useEffect(() => {
webdavAutoSync ? startAutoSync() : stopAutoSync()
}, [webdavAutoSync, webdavSyncInterval])
useEffect(() => { useEffect(() => {
import('@renderer/queue/KnowledgeQueue') import('@renderer/queue/KnowledgeQueue')
}, []) }, [])

View File

@@ -37,4 +37,32 @@ export const useMermaid = () => {
setTimeout(renderMermaid, 100) setTimeout(renderMermaid, 100)
}, [generating]) }, [generating])
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault()
const mermaidElement = (e.target as HTMLElement).closest('.mermaid')
if (!mermaidElement) return
const svg = mermaidElement.querySelector('svg')
if (!svg) return
const currentScale = parseFloat(svg.style.transform?.match(/scale\((.*?)\)/)?.[1] || '1')
const delta = e.deltaY < 0 ? 0.1 : -0.1
const newScale = Math.max(0.1, Math.min(3, currentScale + delta))
const container = svg.parentElement
if (container) {
container.style.overflow = 'auto'
container.style.position = 'relative'
svg.style.transformOrigin = 'top left'
svg.style.transform = `scale(${newScale})`
}
}
}
document.addEventListener('wheel', handleWheel, { passive: false })
return () => document.removeEventListener('wheel', handleWheel)
}, [])
} }

View File

@@ -183,6 +183,7 @@
"name": "Name", "name": "Name",
"open": "Open", "open": "Open",
"size": "Size", "size": "Size",
"type": "Type",
"text": "Text", "text": "Text",
"title": "Files", "title": "Files",
"edit": "Edit", "edit": "Edit",
@@ -217,6 +218,10 @@
"png": "Download PNG", "png": "Download PNG",
"svg": "Download SVG" "svg": "Download SVG"
}, },
"resize": {
"zoom-in": "Zoom In",
"zoom-out": "Zoom Out"
},
"tabs": { "tabs": {
"preview": "Preview", "preview": "Preview",
"source": "Source" "source": "Source"
@@ -249,7 +254,7 @@
"reset.double.confirm.title": "DATA LOST !!!", "reset.double.confirm.title": "DATA LOST !!!",
"restore.success": "Restored successfully", "restore.success": "Restored successfully",
"save.success.title": "Saved successfully", "save.success.title": "Saved successfully",
"switch.disabled": "Switching is disabled while the assistant is generating", "switch.disabled": "Please wait for the current reply to complete",
"topic.added": "New topic added", "topic.added": "New topic added",
"upgrade.success.button": "Restart", "upgrade.success.button": "Restart",
"upgrade.success.content": "Please restart the application to complete the upgrade", "upgrade.success.content": "Please restart the application to complete the upgrade",
@@ -390,9 +395,16 @@
"general.user_name.placeholder": "Enter your name", "general.user_name.placeholder": "Enter your name",
"general.view_webdav_settings": "View WebDAV settings", "general.view_webdav_settings": "View WebDAV settings",
"general.display.title": "Display Settings", "general.display.title": "Display Settings",
"display.sidebar.translate.icon": "Show Translate icon",
"display.sidebar.painting.icon": "Show Painting icon",
"display.sidebar.minapp.icon": "Show MinApp icon", "display.sidebar.minapp.icon": "Show MinApp icon",
"display.sidebar.knowledge.icon": "Show Knowledge icon",
"display.sidebar.files.icon": "Show Files icon", "display.sidebar.files.icon": "Show Files icon",
"display.sidebar.title": "Sidebar Settings", "display.sidebar.title": "Sidebar Settings",
"display.sidebar.visible": "Show my sidebar icons",
"display.sidebar.disabled": "Hide my sidebar icons",
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
"display.sidebar.empty": "Drag the hidden feature from the left side here",
"display.topic.title": "Topic Settings", "display.topic.title": "Topic Settings",
"display.custom.css": "Custom CSS", "display.custom.css": "Custom CSS",
"display.custom.css.placeholder": "/* Put custom CSS here */", "display.custom.css.placeholder": "/* Put custom CSS here */",
@@ -403,7 +415,7 @@
"messages.input.show_estimated_tokens": "Show estimated tokens", "messages.input.show_estimated_tokens": "Show estimated tokens",
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec", "messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
"messages.input.title": "Input Settings", "messages.input.title": "Input Settings",
"messages.markdown_rendering_input_message": "Markdown render input msg", "messages.markdown_rendering_input_message": "Markdown render input message",
"messages.math_engine": "Math render engine", "messages.math_engine": "Math render engine",
"messages.model.title": "Model Settings", "messages.model.title": "Model Settings",
"messages.title": "Message Settings", "messages.title": "Message Settings",
@@ -497,7 +509,8 @@
"clear_shortcut": "Clear Shortcut", "clear_shortcut": "Clear Shortcut",
"toggle_show_assistants": "Toggle Assistants", "toggle_show_assistants": "Toggle Assistants",
"toggle_show_topics": "Toggle Topics", "toggle_show_topics": "Toggle Topics",
"copy_last_message": "Copy Last Message" "copy_last_message": "Copy Last Message",
"search_message": "Search Message"
}, },
"theme.auto": "Auto", "theme.auto": "Auto",
"theme.dark": "Dark", "theme.dark": "Dark",
@@ -538,7 +551,7 @@
"show_window": "Show Window", "show_window": "Show Window",
"quit": "Quit" "quit": "Quit"
}, },
"knowledge_base": { "knowledge": {
"title": "Knowledge Base", "title": "Knowledge Base",
"search": "Search knowledge base", "search": "Search knowledge base",
"empty": "No knowledge base found", "empty": "No knowledge base found",

View File

@@ -183,6 +183,7 @@
"name": "名前", "name": "名前",
"open": "開く", "open": "開く",
"size": "サイズ", "size": "サイズ",
"type": "タイプ",
"text": "テキスト", "text": "テキスト",
"title": "ファイル", "title": "ファイル",
"edit": "編集", "edit": "編集",
@@ -217,6 +218,10 @@
"png": "PNGをダウンロード", "png": "PNGをダウンロード",
"svg": "SVGをダウンロード" "svg": "SVGをダウンロード"
}, },
"resize": {
"zoom-in": "拡大する",
"zoom-out": "ズームアウト"
},
"tabs": { "tabs": {
"preview": "プレビュー", "preview": "プレビュー",
"source": "ソース" "source": "ソース"
@@ -248,7 +253,7 @@
"reset.double.confirm.title": "データが失われます!!!", "reset.double.confirm.title": "データが失われます!!!",
"restore.success": "復元に成功しました", "restore.success": "復元に成功しました",
"save.success.title": "保存に成功しました", "save.success.title": "保存に成功しました",
"switch.disabled": "アシスタントが生成中は切り替え無効す", "switch.disabled": "現在の応答が完了するまで切り替え無効にします",
"topic.added": "新しいトピックが追加されました", "topic.added": "新しいトピックが追加されました",
"upgrade.success.button": "再起動", "upgrade.success.button": "再起動",
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください", "upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
@@ -388,9 +393,16 @@
"general.user_name.placeholder": "ユーザー名を入力", "general.user_name.placeholder": "ユーザー名を入力",
"general.view_webdav_settings": "WebDAV設定を表示", "general.view_webdav_settings": "WebDAV設定を表示",
"general.display.title": "表示設定", "general.display.title": "表示設定",
"display.sidebar.translate.icon": "翻訳のアイコンを表示",
"display.sidebar.painting.icon": "絵画のアイコンを表示",
"display.sidebar.minapp.icon": "ミニアプリのアイコンを表示", "display.sidebar.minapp.icon": "ミニアプリのアイコンを表示",
"display.sidebar.knowledge.icon": "ナレッジのアイコンを表示",
"display.sidebar.files.icon": "ファイルのアイコンを表示", "display.sidebar.files.icon": "ファイルのアイコンを表示",
"display.sidebar.title": "サイドバー設定", "display.sidebar.title": "サイドバー設定",
"display.sidebar.visible": "サイドバーのアイコンを表示する",
"display.sidebar.disabled": "サイドバーのアイコンを非表示にする",
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
"display.topic.title": "トピック設定", "display.topic.title": "トピック設定",
"display.custom.css": "カスタムCSS", "display.custom.css": "カスタムCSS",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */", "display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
@@ -483,7 +495,8 @@
"clear_shortcut": "ショートカットをクリア", "clear_shortcut": "ショートカットをクリア",
"toggle_show_assistants": "アシスタントの表示を切り替え", "toggle_show_assistants": "アシスタントの表示を切り替え",
"toggle_show_topics": "トピックの表示を切り替え", "toggle_show_topics": "トピックの表示を切り替え",
"copy_last_message": "最後のメッセージをコピー" "copy_last_message": "最後のメッセージをコピー",
"search_message": "メッセージを検索"
}, },
"theme.auto": "自動", "theme.auto": "自動",
"theme.dark": "ダークテーマ", "theme.dark": "ダークテーマ",
@@ -524,7 +537,7 @@
"show_window": "ウィンドウを表示", "show_window": "ウィンドウを表示",
"quit": "終了" "quit": "終了"
}, },
"knowledge_base": { "knowledge": {
"title": "ナレッジベース", "title": "ナレッジベース",
"search": "ナレッジベースを検索", "search": "ナレッジベースを検索",
"empty": "ナレッジベースが見つかりません", "empty": "ナレッジベースが見つかりません",

View File

@@ -183,6 +183,7 @@
"name": "Имя", "name": "Имя",
"open": "Открыть", "open": "Открыть",
"size": "Размер", "size": "Размер",
"type": "Тип",
"text": "Текст", "text": "Текст",
"title": "Файлы", "title": "Файлы",
"edit": "Редактировать", "edit": "Редактировать",
@@ -217,6 +218,10 @@
"png": "Скачать PNG", "png": "Скачать PNG",
"svg": "Скачать SVG" "svg": "Скачать SVG"
}, },
"resize": {
"zoom-in": "Yвеличить",
"zoom-out": "Yменьшить масштаб"
},
"tabs": { "tabs": {
"preview": "Предпросмотр", "preview": "Предпросмотр",
"source": "Исходный код" "source": "Исходный код"
@@ -249,7 +254,7 @@
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!", "reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
"restore.success": "Успешно восстановлено", "restore.success": "Успешно восстановлено",
"save.success.title": "Успешно сохранено", "save.success.title": "Успешно сохранено",
"switch.disabled": ереключение отключено, пока ассистент генерирует", "switch.disabled": ожалуйста, дождитесь завершения текущего ответа",
"topic.added": "Новый топик добавлен", "topic.added": "Новый топик добавлен",
"upgrade.success.button": "Перезапустить", "upgrade.success.button": "Перезапустить",
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления", "upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
@@ -390,9 +395,16 @@
"general.user_name.placeholder": "Введите ваше имя", "general.user_name.placeholder": "Введите ваше имя",
"general.view_webdav_settings": "Просмотр настроек WebDAV", "general.view_webdav_settings": "Просмотр настроек WebDAV",
"general.display.title": "Настройки отображения", "general.display.title": "Настройки отображения",
"display.sidebar.translate.icon": "Показывать иконку перевода",
"display.sidebar.painting.icon": "Показывать иконку рисования",
"display.sidebar.minapp.icon": "Показывать иконку мини-приложения", "display.sidebar.minapp.icon": "Показывать иконку мини-приложения",
"display.sidebar.knowledge.icon": "Показывать иконку знаний",
"display.sidebar.files.icon": "Показывать иконку файлов", "display.sidebar.files.icon": "Показывать иконку файлов",
"display.sidebar.title": "Настройки боковой панели", "display.sidebar.title": "Настройки боковой панели",
"display.sidebar.visible": "Показать мои значки на боковой панели",
"display.sidebar.disabled": "Скрыть значок на боковой панели",
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
"display.topic.title": "Настройки топиков", "display.topic.title": "Настройки топиков",
"display.custom.css": "Пользовательский CSS", "display.custom.css": "Пользовательский CSS",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */", "display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
@@ -497,7 +509,8 @@
"clear_shortcut": "Очистить сочетание клавиш", "clear_shortcut": "Очистить сочетание клавиш",
"toggle_show_assistants": "Переключить отображение ассистентов", "toggle_show_assistants": "Переключить отображение ассистентов",
"toggle_show_topics": "Переключить отображение топиков", "toggle_show_topics": "Переключить отображение топиков",
"copy_last_message": "Копировать последнее сообщение" "copy_last_message": "Копировать последнее сообщение",
"search_message": "Поиск сообщения"
}, },
"theme.auto": "Автоматически", "theme.auto": "Автоматически",
"theme.dark": "Темная", "theme.dark": "Темная",
@@ -538,7 +551,7 @@
"show_window": "Показать окно", "show_window": "Показать окно",
"quit": "Выйти" "quit": "Выйти"
}, },
"knowledge_base": { "knowledge": {
"title": "База знаний", "title": "База знаний",
"search": "Поиск в базе знаний", "search": "Поиск в базе знаний",
"empty": "База знаний не найдена", "empty": "База знаний не найдена",

View File

@@ -82,7 +82,7 @@
"input.upload": "上传图片或文档", "input.upload": "上传图片或文档",
"input.web_search": "开启网络搜索", "input.web_search": "开启网络搜索",
"input.knowledge_base": "知识库", "input.knowledge_base": "知识库",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已创建", "message.new.branch.created": "新分支已创建",
"message.regenerate.model": "切换模型", "message.regenerate.model": "切换模型",
"message.new.context": "清除上下文", "message.new.context": "清除上下文",
@@ -184,6 +184,7 @@
"name": "文件名", "name": "文件名",
"open": "打开", "open": "打开",
"size": "大小", "size": "大小",
"type": "类型",
"text": "文本", "text": "文本",
"title": "文件", "title": "文件",
"edit": "编辑", "edit": "编辑",
@@ -218,6 +219,10 @@
"png": "下载 PNG", "png": "下载 PNG",
"svg": "下载 SVG" "svg": "下载 SVG"
}, },
"resize": {
"zoom-in": "放大",
"zoom-out": "缩小"
},
"tabs": { "tabs": {
"preview": "预览", "preview": "预览",
"source": "源码" "source": "源码"
@@ -250,7 +255,7 @@
"reset.double.confirm.title": "数据丢失!!!", "reset.double.confirm.title": "数据丢失!!!",
"restore.success": "恢复成功", "restore.success": "恢复成功",
"save.success.title": "保存成功", "save.success.title": "保存成功",
"switch.disabled": "模型回复完成后才能切换", "switch.disabled": "请等待当前回复完成后操作",
"topic.added": "话题添加成功", "topic.added": "话题添加成功",
"upgrade.success.button": "重启", "upgrade.success.button": "重启",
"upgrade.success.content": "重启用以完成升级", "upgrade.success.content": "重启用以完成升级",
@@ -391,9 +396,16 @@
"general.user_name.placeholder": "请输入用户名", "general.user_name.placeholder": "请输入用户名",
"general.view_webdav_settings": "查看 WebDAV 设置", "general.view_webdav_settings": "查看 WebDAV 设置",
"general.display.title": "显示设置", "general.display.title": "显示设置",
"display.sidebar.translate.icon": "显示翻译图标",
"display.sidebar.painting.icon": "显示绘画图标",
"display.sidebar.minapp.icon": "显示小程序图标", "display.sidebar.minapp.icon": "显示小程序图标",
"display.sidebar.knowledge.icon": "显示知识图标",
"display.sidebar.files.icon": "显示文件图标", "display.sidebar.files.icon": "显示文件图标",
"display.sidebar.title": "侧边栏设置", "display.sidebar.title": "侧边栏设置",
"display.sidebar.visible": "显示我的侧边栏图标",
"display.sidebar.disabled": "隐藏我的侧边栏图标",
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
"display.topic.title": "话题设置", "display.topic.title": "话题设置",
"display.custom.css": "自定义 CSS", "display.custom.css": "自定义 CSS",
"display.custom.css.placeholder": "/* 这里写自定义CSS */", "display.custom.css.placeholder": "/* 这里写自定义CSS */",
@@ -486,7 +498,8 @@
"clear_shortcut": "清除快捷键", "clear_shortcut": "清除快捷键",
"toggle_show_assistants": "切换助手显示", "toggle_show_assistants": "切换助手显示",
"toggle_show_topics": "切换话题显示", "toggle_show_topics": "切换话题显示",
"copy_last_message": "复制上一条消息" "copy_last_message": "复制上一条消息",
"search_message": "搜索消息"
}, },
"theme.auto": "跟随系统", "theme.auto": "跟随系统",
"theme.dark": "深色主题", "theme.dark": "深色主题",
@@ -527,7 +540,7 @@
"show_window": "显示窗口", "show_window": "显示窗口",
"quit": "退出" "quit": "退出"
}, },
"knowledge_base": { "knowledge": {
"title": "知识库", "title": "知识库",
"search": "搜索知识库", "search": "搜索知识库",
"empty": "暂无知识库", "empty": "暂无知识库",

View File

@@ -82,7 +82,7 @@
"input.upload": "上傳圖片或文檔", "input.upload": "上傳圖片或文檔",
"input.web_search": "開啟網路搜索", "input.web_search": "開啟網路搜索",
"input.knowledge_base": "知識庫", "input.knowledge_base": "知識庫",
"message.new.branch": "分支", "message.new.branch": "分支",
"message.new.branch.created": "新分支已建立", "message.new.branch.created": "新分支已建立",
"message.regenerate.model": "切換模型", "message.regenerate.model": "切換模型",
"message.new.context": "新上下文", "message.new.context": "新上下文",
@@ -183,6 +183,7 @@
"name": "名稱", "name": "名稱",
"open": "打開", "open": "打開",
"size": "大小", "size": "大小",
"type": "類型",
"text": "文本", "text": "文本",
"title": "檔案", "title": "檔案",
"edit": "編輯", "edit": "編輯",
@@ -217,6 +218,10 @@
"png": "下載 PNG", "png": "下載 PNG",
"svg": "下載 SVG" "svg": "下載 SVG"
}, },
"resize": {
"zoom-in": "放大",
"zoom-out": "縮小"
},
"tabs": { "tabs": {
"preview": "預覽", "preview": "預覽",
"source": "原始碼" "source": "原始碼"
@@ -249,7 +254,7 @@
"reset.double.confirm.title": "資料將會丟失!!!", "reset.double.confirm.title": "資料將會丟失!!!",
"restore.success": "恢復成功", "restore.success": "恢復成功",
"save.success.title": "保存成功", "save.success.title": "保存成功",
"switch.disabled": "助手生成回覆時無法切換", "switch.disabled": "請等待當前回覆完成",
"topic.added": "新話題已添加", "topic.added": "新話題已添加",
"upgrade.success.button": "重新啟動", "upgrade.success.button": "重新啟動",
"upgrade.success.content": "請重新啟動應用以完成升級", "upgrade.success.content": "請重新啟動應用以完成升級",
@@ -390,10 +395,17 @@
"general.user_name.placeholder": "輸入您的名稱", "general.user_name.placeholder": "輸入您的名稱",
"general.view_webdav_settings": "查看 WebDAV 設定", "general.view_webdav_settings": "查看 WebDAV 設定",
"general.display.title": "顯示設定", "general.display.title": "顯示設定",
"display.sidebar.translate.icon": "顯示翻譯圖示",
"display.sidebar.painting.icon": "顯示繪圖圖示",
"display.sidebar.minapp.icon": "顯示小程序圖示", "display.sidebar.minapp.icon": "顯示小程序圖示",
"display.sidebar.knowledge.icon": "顯示知識圖示",
"display.sidebar.files.icon": "顯示文件圖示", "display.sidebar.files.icon": "顯示文件圖示",
"display.sidebar.title": "側邊欄設定", "display.sidebar.title": "側邊欄設定",
"display.topic.title": "話題設定", "display.topic.title": "話題設定",
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
"display.sidebar.visible": "顯示我的側邊欄圖標",
"display.sidebar.disabled": "隱藏我的側邊欄圖標",
"display.custom.css": "自定義 CSS", "display.custom.css": "自定義 CSS",
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */", "display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
"input.auto_translate_with_space": "快速敲擊3次空格翻譯", "input.auto_translate_with_space": "快速敲擊3次空格翻譯",
@@ -485,7 +497,8 @@
"clear_shortcut": "清除快捷鍵", "clear_shortcut": "清除快捷鍵",
"toggle_show_assistants": "切換助手顯示", "toggle_show_assistants": "切換助手顯示",
"toggle_show_topics": "切換話題顯示", "toggle_show_topics": "切換話題顯示",
"copy_last_message": "複製上一条消息" "copy_last_message": "複製上一条消息",
"search_message": "搜索消息"
}, },
"theme.auto": "自動", "theme.auto": "自動",
"theme.dark": "深色主題", "theme.dark": "深色主題",
@@ -526,7 +539,7 @@
"show_window": "顯示視窗", "show_window": "顯示視窗",
"quit": "退出" "quit": "退出"
}, },
"knowledge_base": { "knowledge": {
"title": "知識庫", "title": "知識庫",
"search": "搜尋知識庫", "search": "搜尋知識庫",
"empty": "暫無知識庫", "empty": "暫無知識庫",

View File

@@ -1,8 +1,19 @@
import KeyvStorage from '@kangfenmao/keyv-storage' import KeyvStorage from '@kangfenmao/keyv-storage'
function init() { import { startAutoSync } from './services/BackupService'
import store from './store'
function initKeyv() {
window.keyv = new KeyvStorage() window.keyv = new KeyvStorage()
window.keyv.init() window.keyv.init()
} }
init() function initAutoSync() {
const { webdavAutoSync } = store.getState().settings
if (webdavAutoSync) {
startAutoSync()
}
}
initKeyv()
initAutoSync()

View File

@@ -283,7 +283,7 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
} }
.ant-tabs-tab { .ant-tabs-tab {
margin: 0 !important; margin: 0 !important;
border-radius: 16px; border-radius: var(--list-item-border-radius);
margin-bottom: 5px !important; margin-bottom: 5px !important;
font-size: 13px; font-size: 13px;
justify-content: left; justify-content: left;

View File

@@ -124,6 +124,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
label={t('agents.add.prompt')} label={t('agents.add.prompt')}
rules={[{ required: true }]} rules={[{ required: true }]}
style={{ position: 'relative' }}> style={{ position: 'relative' }}>
spellCheck={false}
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={10} /> <TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={10} />
</Form.Item> </Form.Item>
<Button <Button

View File

@@ -73,14 +73,15 @@ const ContentContainer = styled.div`
` `
const AppsContainer = styled.div` const AppsContainer = styled.div`
display: flex; display: grid;
min-width: 930px; min-width: 0;
max-width: 930px; max-width: 930px;
width: 100%;
max-height: 520px; max-height: 520px;
min-height: 520px; min-height: 520px;
display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
grid-template-columns: repeat(8, minmax(90px, 1fr)); gap: 25px;
gap: 25px 25px; justify-content: center;
` `
export default AppsPage export default AppsPage

View File

@@ -0,0 +1,129 @@
import FileManager from '@renderer/services/FileManager'
import { FileType, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Col, Image, Row, Spin, Table } from 'antd'
import React, { memo } from 'react'
import styled from 'styled-components'
import GeminiFiles from './GeminiFiles'
interface ContentViewProps {
id: FileTypes | 'all' | string
files?: FileType[]
dataSource?: any[]
columns: any[]
}
const ContentView: React.FC<ContentViewProps> = ({ id, files, dataSource, columns }) => {
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
return (
<Image.PreviewGroup>
<Row gutter={[16, 16]}>
{files?.map((file) => (
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
}}
/>
<ImageInfo>
<div>{formatFileSize(file)}</div>
</ImageInfo>
</ImageWrapper>
</Col>
))}
</Row>
</Image.PreviewGroup>
)
}
if (id.startsWith('gemini_')) {
return <GeminiFiles id={id.replace('gemini_', '') as string} />
}
return (
<Table
dataSource={dataSource}
columns={columns}
style={{ width: '100%' }}
size="small"
pagination={{ pageSize: 100 }}
/>
)
}
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
export default memo(ContentView)

View File

@@ -10,21 +10,27 @@ import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup' import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import db from '@renderer/databases' import db from '@renderer/databases'
import { useProviders } from '@renderer/hooks/useProvider'
import FileManager from '@renderer/services/FileManager' import FileManager from '@renderer/services/FileManager'
import store from '@renderer/store' import store from '@renderer/store'
import { FileType, FileTypes } from '@renderer/types' import { FileType, FileTypes } from '@renderer/types'
import { formatFileSize } from '@renderer/utils' import { formatFileSize } from '@renderer/utils'
import type { MenuProps } from 'antd' import type { MenuProps } from 'antd'
import { Button, Col, Dropdown, Image, Menu, Row, Spin, Table } from 'antd' import { Button, Dropdown, Menu } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { FC, useState } from 'react' import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import ContentView from './ContentView'
const FilesPage: FC = () => { const FilesPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [fileType, setFileType] = useState<FileTypes | 'all'>('all') const [fileType, setFileType] = useState<FileTypes | 'all' | 'gemini'>('all')
const { providers } = useProviders()
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
const files = useLiveQuery<FileType[]>(() => { const files = useLiveQuery<FileType[]>(() => {
if (fileType === 'all') { if (fileType === 'all') {
@@ -111,58 +117,68 @@ const FilesPage: FC = () => {
created_at: dayjs(file.created_at).format('MM-DD HH:mm'), created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
created_at_unix: dayjs(file.created_at).unix(), created_at_unix: dayjs(file.created_at).unix(),
actions: ( actions: (
<Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']}> <Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']} placement="bottom" arrow>
<Button type="text" size="small" icon={<EllipsisOutlined />} /> <Button type="text" size="small" icon={<EllipsisOutlined />} />
</Dropdown> </Dropdown>
) )
} }
}) })
const columns = [ const columns = useMemo(
{ () => [
title: t('files.name'), {
dataIndex: 'file', title: t('files.name'),
key: 'file', dataIndex: 'file',
width: '300px' key: 'file',
}, width: '300px'
{ },
title: t('files.size'), {
dataIndex: 'size', title: t('files.size'),
key: 'size', dataIndex: 'size',
width: '80px', key: 'size',
sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes, width: '80px',
align: 'center' sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes,
}, align: 'center'
{ },
title: t('files.count'), {
dataIndex: 'count', title: t('files.count'),
key: 'count', dataIndex: 'count',
width: '60px', key: 'count',
sorter: (a: { count: number }, b: { count: number }) => b.count - a.count, width: '60px',
align: 'center' sorter: (a: { count: number }, b: { count: number }) => b.count - a.count,
}, align: 'center'
{ },
title: t('files.created_at'), {
dataIndex: 'created_at', title: t('files.created_at'),
key: 'created_at', dataIndex: 'created_at',
width: '120px', key: 'created_at',
align: 'center', width: '120px',
sorter: (a: { created_at_unix: number }, b: { created_at_unix: number }) => b.created_at_unix - a.created_at_unix align: 'center',
}, sorter: (a: { created_at_unix: number }, b: { created_at_unix: number }) =>
{ b.created_at_unix - a.created_at_unix
title: t('files.actions'), },
dataIndex: 'actions', {
key: 'actions', title: t('files.actions'),
width: '50px' dataIndex: 'actions',
} key: 'actions',
] width: '80px',
align: 'center'
}
],
[t]
)
const menuItems = [ const menuItems = [
{ key: 'all', label: t('files.all'), icon: <FileTextOutlined /> }, { key: 'all', label: t('files.all'), icon: <FileTextOutlined /> },
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> }, { key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> },
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> }, { key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> },
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> } { key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> },
] ...geminiProviders.map((provider) => ({
key: 'gemini_' + provider.id,
label: provider.name,
icon: <FilePdfOutlined />
}))
].filter(Boolean) as MenuProps['items']
return ( return (
<Container> <Container>
@@ -174,41 +190,7 @@ const FilesPage: FC = () => {
<Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} /> <Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} />
</SideNav> </SideNav>
<TableContainer right> <TableContainer right>
{fileType === FileTypes.IMAGE && files?.length && files?.length > 0 ? ( <ContentView id={fileType} files={files} dataSource={dataSource} columns={columns} />
<Image.PreviewGroup>
<Row gutter={[16, 16]}>
{files?.map((file) => (
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
<ImageWrapper>
<LoadingWrapper>
<Spin />
</LoadingWrapper>
<Image
src={FileManager.getFileUrl(file)}
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
preview={{ mask: false }}
onLoad={(e) => {
const img = e.target as HTMLImageElement
img.parentElement?.classList.add('loaded')
}}
/>
<ImageInfo>
<div>{formatFileSize(file)}</div>
</ImageInfo>
</ImageWrapper>
</Col>
))}
</Row>
</Image.PreviewGroup>
) : (
<Table
dataSource={dataSource}
columns={columns as any}
style={{ width: '100%' }}
size="small"
pagination={{ pageSize: 100 }}
/>
)}
</TableContainer> </TableContainer>
</ContentContainer> </ContentContainer>
</Container> </Container>
@@ -242,72 +224,6 @@ const FileNameText = styled.div`
cursor: pointer; cursor: pointer;
` `
const ImageWrapper = styled.div`
position: relative;
aspect-ratio: 1;
overflow: hidden;
border-radius: 8px;
background-color: var(--color-background-soft);
display: flex;
align-items: center;
justify-content: center;
border: 0.5px solid var(--color-border);
.ant-image {
height: 100%;
width: 100%;
opacity: 0;
transition:
opacity 0.3s ease,
transform 0.3s ease;
&.loaded {
opacity: 1;
}
}
&:hover {
.ant-image.loaded {
transform: scale(1.05);
}
div:last-child {
opacity: 1;
}
}
`
const LoadingWrapper = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-soft);
`
const ImageInfo = styled.div`
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 5px 8px;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 12px;
> div:first-child {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`
const SideNav = styled.div` const SideNav = styled.div`
width: var(--assistants-width); width: var(--assistants-width);
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
@@ -324,7 +240,7 @@ const SideNav = styled.div`
line-height: 36px; line-height: 36px;
margin: 4px 0; margin: 4px 0;
width: 100%; width: 100%;
border-radius: 16px; border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
&:hover { &:hover {

View File

@@ -0,0 +1,98 @@
import { DeleteOutlined } from '@ant-design/icons'
import type { FileMetadataResponse } from '@google/generative-ai/server'
import { useProvider } from '@renderer/hooks/useProvider'
import { runAsyncFunction } from '@renderer/utils'
import { Table } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface GeminiFilesProps {
id: string
}
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
const { provider } = useProvider(id)
const [files, setFiles] = useState<FileMetadataResponse[]>([])
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const fetchFiles = useCallback(async () => {
const { files } = await window.api.gemini.listFiles(provider.apiKey)
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
}, [provider])
const columns: ColumnsType<FileMetadataResponse> = [
{
title: t('files.name'),
dataIndex: 'displayName',
key: 'displayName'
},
{
title: t('files.type'),
dataIndex: 'mimeType',
key: 'mimeType'
},
{
title: t('files.size'),
dataIndex: 'sizeBytes',
key: 'sizeBytes',
render: (size: string) => `${(parseInt(size) / 1024 / 1024).toFixed(2)} MB`
},
{
title: t('files.created_at'),
dataIndex: 'createTime',
key: 'createTime',
render: (time: string) => new Date(time).toLocaleString()
},
{
title: t('files.actions'),
dataIndex: 'actions',
key: 'actions',
align: 'center',
render: (_, record) => {
return (
<DeleteOutlined
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
onClick={() => {
setFiles(files.filter((file) => file.name !== record.name))
window.api.gemini.deleteFile(provider.apiKey, record.name).catch((error) => {
console.error('Failed to delete file:', error)
setFiles((prev) => [...prev, record])
})
}}
/>
)
}
}
]
useEffect(() => {
runAsyncFunction(async () => {
try {
setLoading(true)
await fetchFiles()
setLoading(false)
} catch (error: any) {
console.error('Failed to fetch files:', error)
window.message.error(error.message)
setLoading(false)
}
})
}, [fetchFiles])
useEffect(() => {
setFiles([])
}, [id])
return (
<Container>
<Table columns={columns} dataSource={files} rowKey="name" loading={loading} />
</Container>
)
}
const Container = styled.div``
export default GeminiFiles

View File

@@ -1,5 +1,5 @@
import { useAssistants } from '@renderer/hooks/useAssistant' import { useAssistants } from '@renderer/hooks/useAssistant'
import { useShowAssistants } from '@renderer/hooks/useStore' import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic' import { useActiveTopic } from '@renderer/hooks/useTopic'
import NavigationService from '@renderer/services/NavigationService' import NavigationService from '@renderer/services/NavigationService'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
@@ -22,7 +22,7 @@ const HomePage: FC = () => {
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0]) const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic) const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { showAssistants } = useShowAssistants() const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant _activeAssistant = activeAssistant
@@ -35,6 +35,15 @@ const HomePage: FC = () => {
state?.topic && setActiveTopic(state?.topic) state?.topic && setActiveTopic(state?.topic)
}, [state]) }, [state])
useEffect(() => {
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => {
window.api.window.resetMinimumSize()
}
}, [showAssistants, showTopics, topicPosition])
return ( return (
<Container id="home-page"> <Container id="home-page">
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} /> <Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />

View File

@@ -17,16 +17,18 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
return ( return (
<Container> <Container>
<Upload <ContentContainer>
listType="picture-card" <Upload
fileList={files.map((file) => ({ listType={files.length > 20 ? 'text' : 'picture-card'}
uid: file.id, fileList={files.map((file) => ({
url: 'file://' + FileManager.getSafePath(file), uid: file.id,
status: 'done', url: 'file://' + FileManager.getSafePath(file),
name: file.name status: 'done',
}))} name: file.name
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))} }))}
/> onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
/>
</ContentContainer>
</Container> </Container>
) )
} }
@@ -35,9 +37,16 @@ const Container = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 10px; gap: 10px;
padding: 10px 20px; padding: 10px 0;
background: var(--color-background); background: var(--color-background);
border-top: 1px solid var(--color-border-mute); border-top: 1px solid var(--color-border-mute);
` `
const ContentContainer = styled.div`
max-height: 40vh;
width: 100%;
overflow-y: auto;
padding: 0 20px;
`
export default AttachmentPreview export default AttachmentPreview

View File

@@ -35,6 +35,7 @@ import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState }
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import NarrowLayout from '../Messages/NarrowLayout'
import AttachmentButton from './AttachmentButton' import AttachmentButton from './AttachmentButton'
import AttachmentPreview from './AttachmentPreview' import AttachmentPreview from './AttachmentPreview'
import KnowledgeBaseButton from './KnowledgeBaseButton' import KnowledgeBaseButton from './KnowledgeBaseButton'
@@ -62,7 +63,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
showInputEstimatedTokens, showInputEstimatedTokens,
clickAssistantToShowTopic, clickAssistantToShowTopic,
language, language,
autoTranslateWithSpace autoTranslateWithSpace,
sidebarIcons
} = useSettings() } = useSettings()
const [expended, setExpend] = useState(false) const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0) const [estimateTokenCount, setEstimateTokenCount] = useState(0)
@@ -84,6 +86,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
const isVision = useMemo(() => isVisionModel(model), [model]) const isVision = useMemo(() => isVisionModel(model), [model])
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision]) const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
const showKnowledgeIcon = sidebarIcons.visible.includes('knowledge')
const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), []) const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), [])
const inputTokenCount = useMemo( const inputTokenCount = useMemo(
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0), () => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
@@ -130,7 +134,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setTimeout(() => resizeTextArea(), 0) setTimeout(() => resizeTextArea(), 0)
setExpend(false) setExpend(false)
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files]) }, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
const translate = async () => { const translate = async () => {
if (isTranslating) { if (isTranslating) {
@@ -384,117 +388,116 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
return ( return (
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar"> <Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
<AttachmentPreview files={files} setFiles={setFiles} /> <NarrowLayout style={{ width: '100%' }}>
<InputBarContainer <AttachmentPreview files={files} setFiles={setFiles} />
id="inputbar" <InputBarContainer
className={classNames('inputbar-container', inputFocus && 'focus')} id="inputbar"
ref={containerRef}> className={classNames('inputbar-container', inputFocus && 'focus')}
<Textarea ref={containerRef}>
value={text} <Textarea
onChange={(e) => setText(e.target.value)} value={text}
onKeyDown={handleKeyDown} onChange={(e) => setText(e.target.value)}
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')} onKeyDown={handleKeyDown}
autoFocus placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
contextMenu="true" autoFocus
variant="borderless" contextMenu="true"
rows={textareaRows} variant="borderless"
ref={textareaRef} spellCheck={false}
style={{ fontSize }} rows={textareaRows}
styles={{ textarea: TextareaStyle }} ref={textareaRef}
onFocus={() => setInputFocus(true)} style={{ fontSize }}
onBlur={() => setInputFocus(false)} styles={{ textarea: TextareaStyle }}
onInput={onInput} onFocus={() => setInputFocus(true)}
disabled={searching} onBlur={() => setInputFocus(false)}
onPaste={(e) => onPaste(e.nativeEvent)} onInput={onInput}
onClick={() => searching && dispatch(setSearching(false))} disabled={searching}
/> onPaste={(e) => onPaste(e.nativeEvent)}
<Toolbar> onClick={() => searching && dispatch(setSearching(false))}
<ToolbarMenu> />
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow> <Toolbar>
<ToolbarButton type="text" onClick={addNewTopic}> <ToolbarMenu>
<FormOutlined /> <Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
</ToolbarButton> <ToolbarButton type="text" onClick={addNewTopic}>
</Tooltip> <FormOutlined />
{isWebSearchModel(model) && ( </ToolbarButton>
<Tooltip placement="top" title={t('chat.input.web_search')} arrow> </Tooltip>
{isWebSearchModel(model) && (
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
<ToolbarButton
type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
<GlobalOutlined
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</Tooltip>
)}
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
<Popconfirm
title={t('chat.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('chat.input.clear')}>
<ToolbarButton type="text">
<ClearOutlined />
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton <ToolbarButton
type="text" type="text"
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}> onClick={() => {
<GlobalOutlined !showTopics && toggleShowTopics()
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }} setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
/> }}>
<ControlOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} {showKnowledgeIcon && (
<Tooltip placement="top" title={t('chat.input.clear')} arrow> <KnowledgeBaseButton
<Popconfirm selectedBase={selectedKnowledgeBase}
title={t('chat.input.clear.content')} onSelect={handleKnowledgeBaseSelect}
placement="top" ToolbarButton={ToolbarButton}
onConfirm={clearTopic} disabled={files.length > 0}
okButtonProps={{ danger: true }} />
icon={<QuestionCircleOutlined style={{ color: 'red' }} />} )}
okText={t('chat.input.clear')}> <AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
<ToolbarButton type="text"> <ToolbarButton type="text" onClick={onNewContext}>
<ClearOutlined /> <Tooltip placement="top" title={t('chat.input.new.context')}>
</ToolbarButton> <PicCenterOutlined />
</Popconfirm> </Tooltip>
</Tooltip>
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
<ToolbarButton
type="text"
onClick={() => {
!showTopics && toggleShowTopics()
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
}}>
<ControlOutlined />
</ToolbarButton> </ToolbarButton>
</Tooltip> <Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<KnowledgeBaseButton <ToolbarButton type="text" onClick={onToggleExpended}>
selectedBase={selectedKnowledgeBase} {expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
onSelect={handleKnowledgeBaseSelect}
ToolbarButton={ToolbarButton}
disabled={files.length > 0}
/>
<AttachmentButton
model={model}
files={files}
setFiles={setFiles}
ToolbarButton={ToolbarButton}
disabled={!!selectedKnowledgeBase}
/>
<ToolbarButton type="text" onClick={onNewContext}>
<Tooltip placement="top" title={t('chat.input.new.context')}>
<PicCenterOutlined />
</Tooltip>
</ToolbarButton>
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
<ToolbarButton type="text" onClick={onToggleExpended}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
<TokenCount
estimateTokenCount={estimateTokenCount}
inputTokenCount={inputTokenCount}
contextCount={contextCount}
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton> </ToolbarButton>
</Tooltip> </Tooltip>
)} <TokenCount
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />} estimateTokenCount={estimateTokenCount}
</ToolbarMenu> inputTokenCount={inputTokenCount}
</Toolbar> contextCount={contextCount}
</InputBarContainer> ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
</ToolbarMenu>
<ToolbarMenu>
{!language.startsWith('en') && (
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
)}
{generating && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
</ToolbarButton>
</Tooltip>
)}
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
</ToolbarMenu>
</Toolbar>
</InputBarContainer>
</NarrowLayout>
</Container> </Container>
) )
} }

View File

@@ -66,6 +66,9 @@ const Container = styled.div`
font-size: 10px; font-size: 10px;
margin-right: 3px; margin-right: 3px;
} }
@media (max-width: 600px) {
display: none;
}
` `
const Text = styled.div` const Text = styled.div`

View File

@@ -3,7 +3,6 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider' import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import React, { memo, useEffect, useRef, useState } from 'react' import React, { memo, useEffect, useRef, useState } from 'react'
import DOMPurify from 'dompurify'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -38,7 +37,6 @@ const ExpandButton: React.FC<{
</ExpandButtonWrapper> </ExpandButtonWrapper>
) )
} }
const ALLOWED_TAGS = ['sub'] // 允许的HTML标签
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => { const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const match = /language-(\w+)/.exec(className || '') const match = /language-(\w+)/.exec(className || '')
@@ -135,15 +133,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
{language === 'html' && children?.includes('</html>') && <Artifacts html={children} />} {language === 'html' && children?.includes('</html>') && <Artifacts html={children} />}
</CodeBlockWrapper> </CodeBlockWrapper>
) : ( ) : (
<code <code className={className}>{children}</code>
className={className}
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(children, {
ALLOWED_TAGS,
ALLOWED_ATTR: [] // 不允许任何属性
})
}}
/>
) )
} }

View File

@@ -18,6 +18,7 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
const { t } = useTranslation() const { t } = useTranslation()
const mermaidId = `mermaid-popup-${Date.now()}` const mermaidId = `mermaid-popup-${Date.now()}`
const [activeTab, setActiveTab] = useState('preview') const [activeTab, setActiveTab] = useState('preview')
const [scale, setScale] = useState(1)
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)
@@ -31,6 +32,25 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
resolve({}) resolve({})
} }
const handleZoom = (delta: number) => {
const newScale = Math.max(0.1, Math.min(3, scale + delta))
setScale(newScale)
const element = document.getElementById(mermaidId)
if (!element) return
const svg = element.querySelector('svg')
if (!svg) return
const container = svg.parentElement
if (container) {
container.style.overflow = 'auto'
container.style.position = 'relative'
svg.style.transformOrigin = 'top left'
svg.style.transform = `scale(${newScale})`
}
}
const handleDownload = async (format: 'svg' | 'png') => { const handleDownload = async (format: 'svg' | 'png') => {
try { try {
const element = document.getElementById(mermaidId) const element = document.getElementById(mermaidId)
@@ -110,6 +130,8 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
{activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>} {activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>}
{activeTab === 'preview' && ( {activeTab === 'preview' && (
<> <>
<Button onClick={() => handleZoom(0.1)}>{t('mermaid.resize.zoom-in')}</Button>
<Button onClick={() => handleZoom(-0.1)}>{t('mermaid.resize.zoom-out')}</Button>
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button> <Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button> <Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
</> </>

View File

@@ -27,7 +27,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => { const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const avatar = useAvatar() const avatar = useAvatar()
const { theme } = useTheme() const { theme } = useTheme()
const { userName } = useSettings() const { userName, sidebarIcons } = useSettings()
const { t } = useTranslation() const { t } = useTranslation()
const { isBubbleStyle } = useMessageStyle() const { isBubbleStyle } = useMessageStyle()
@@ -40,11 +40,14 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
}, [message.modelId, message.role, model?.id, model?.name, t, userName]) }, [message.modelId, message.role, model?.id, model?.name, t, userName])
const isAssistantMessage = message.role === 'assistant' const isAssistantMessage = message.role === 'assistant'
const showMinappIcon = sidebarIcons.visible.includes('minapp')
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name]) const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName]) const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = useCallback(() => model?.provider && startMinAppById(model.provider), [model?.provider]) const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && startMinAppById(model.provider)
}, [model?.provider, showMinappIcon])
const avatarStyle: CSSProperties | undefined = isBubbleStyle const avatarStyle: CSSProperties | undefined = isBubbleStyle
? { ? {
@@ -62,7 +65,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
size={35} size={35}
style={{ style={{
borderRadius: '20%', borderRadius: '20%',
cursor: 'pointer', cursor: showMinappIcon ? 'pointer' : 'default',
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none', border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
filter: theme === 'dark' ? 'invert(0.05)' : undefined filter: theme === 'dark' ? 'invert(0.05)' : undefined
}} }}

View File

@@ -195,7 +195,7 @@ const MessageMenubar: FC<Props> = (props) => {
return ( return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && ( {message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}> <Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit}> <ActionButton className="message-action-button" onClick={onEdit}>
<EditOutlined /> <EditOutlined />
</ActionButton> </ActionButton>
@@ -224,7 +224,7 @@ const MessageMenubar: FC<Props> = (props) => {
{canRegenerate && ( {canRegenerate && (
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}> <Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}> <ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
<i className="iconfont icon-at1"></i> <i className="iconfont icon-at"></i>
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
@@ -335,7 +335,7 @@ const ActionButton = styled.div`
&:hover { &:hover {
color: var(--color-text-1); color: var(--color-text-1);
} }
.icon-at1 { .icon-at {
font-size: 16px; font-size: 16px;
} }
` `

View File

@@ -26,6 +26,7 @@ import styled from 'styled-components'
import Suggestions from '../components/Suggestions' import Suggestions from '../components/Suggestions'
import MessageItem from './Message' import MessageItem from './Message'
import NarrowLayout from './NarrowLayout'
import Prompt from './Prompt' import Prompt from './Prompt'
interface Props { interface Props {
@@ -283,33 +284,35 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
key={assistant.id} key={assistant.id}
ref={containerRef} ref={containerRef}
right={topicPosition === 'left'}> right={topicPosition === 'left'}>
<Suggestions assistant={assistant} messages={messages} /> <NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<InfiniteScroll <Suggestions assistant={assistant} messages={messages} />
dataLength={displayMessages.length} <InfiniteScroll
next={loadMoreMessages} dataLength={displayMessages.length}
hasMore={hasMore} next={loadMoreMessages}
loader={null} hasMore={hasMore}
inverse={true} loader={null}
scrollableTarget="messages"> inverse={true}
<ScrollContainer> scrollableTarget="messages">
<LoaderContainer $loading={isLoadingMore}> <ScrollContainer>
<BeatLoader size={8} color="var(--color-text-2)" /> <LoaderContainer $loading={isLoadingMore}>
</LoaderContainer> <BeatLoader size={8} color="var(--color-text-2)" />
{displayMessages.map((message, index) => ( </LoaderContainer>
<MessageItem {displayMessages.map((message, index) => (
key={message.id} <MessageItem
message={message} key={message.id}
topic={topic} message={message}
index={index} topic={topic}
hidePresetMessages={assistant.settings?.hideMessages} index={index}
onSetMessages={setMessages} hidePresetMessages={assistant.settings?.hideMessages}
onDeleteMessage={onDeleteMessage} onSetMessages={setMessages}
onGetMessages={onGetMessages} onDeleteMessage={onDeleteMessage}
/> onGetMessages={onGetMessages}
))} />
</ScrollContainer> ))}
</InfiniteScroll> </ScrollContainer>
<Prompt assistant={assistant} key={assistant.prompt} /> </InfiniteScroll>
<Prompt assistant={assistant} key={assistant.prompt} />
</NarrowLayout>
</Container> </Container>
) )
} }

View File

@@ -0,0 +1,25 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { FC, HTMLAttributes } from 'react'
import styled from 'styled-components'
interface Props extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode
}
const NarrowLayout: FC<Props> = ({ children, ...props }) => {
const { narrowMode } = useSettings()
if (narrowMode) {
return <Container {...props}>{children}</Container>
}
return children
}
const Container = styled.div`
max-width: 800px;
width: 100%;
margin: 0 auto;
`
export default NarrowLayout

View File

@@ -28,7 +28,7 @@ const Container = styled.div`
padding: 10px 20px; padding: 10px 20px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin-bottom: 20px; margin-bottom: 20px;
margin: 0 20px 0 20px; margin: 4px 20px 0 20px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
border: 0.5px solid var(--color-border); border: 0.5px solid var(--color-border);

View File

@@ -10,6 +10,8 @@ import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { FC } from 'react' import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
@@ -25,8 +27,9 @@ interface Props {
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => { const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id) const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { topicPosition } = useSettings() const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', () => { useShortcut('toggle_show_assistants', () => {
toggleShowAssistants() toggleShowAssistants()
@@ -40,11 +43,15 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
} }
}) })
useShortcut('search_message', () => {
SearchPopup.show()
})
return ( return (
<Navbar className="home-navbar"> <Navbar className="home-navbar">
{showAssistants && ( {showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}> <NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}> <NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 16 : 0 }}>
<i className="iconfont icon-hide-sidebar" /> <i className="iconfont icon-hide-sidebar" />
</NavbarIcon> </NavbarIcon>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}> <NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
@@ -57,9 +64,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
className="home-navbar-right"> className="home-navbar-right">
<HStack alignItems="center"> <HStack alignItems="center">
{!showAssistants && ( {!showAssistants && (
<NavbarIcon <NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8, marginLeft: isMac ? 4 : -12 }}>
onClick={() => toggleShowAssistants()}
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
<i className="iconfont icon-show-sidebar" /> <i className="iconfont icon-show-sidebar" />
</NavbarIcon> </NavbarIcon>
)} )}
@@ -71,19 +76,24 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
</TitleText> </TitleText>
<SelectModelButton assistant={assistant} /> <SelectModelButton assistant={assistant} />
</HStack> </HStack>
<HStack alignItems="center"> <HStack alignItems="center" gap={8}>
<NavbarIcon onClick={() => SearchPopup.show()}> <NarrowIcon onClick={() => SearchPopup.show()}>
<SearchOutlined /> <SearchOutlined />
</NavbarIcon> </NarrowIcon>
<AppStorePopover> <NarrowIcon onClick={() => dispatch(setNarrowMode(!narrowMode))}>
<NavbarIcon style={{ marginLeft: isMac ? 5 : 10 }}> <i className="iconfont icon-icon-adaptive-width"></i>
<i className="iconfont icon-appstore" /> </NarrowIcon>
</NavbarIcon> {sidebarIcons.visible.includes('minapp') && (
</AppStorePopover> <AppStorePopover>
<NarrowIcon>
<i className="iconfont icon-appstore" />
</NarrowIcon>
</AppStorePopover>
)}
{topicPosition === 'right' && ( {topicPosition === 'right' && (
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}> <NarrowIcon onClick={toggleShowTopics}>
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} /> <i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
</NavbarIcon> </NarrowIcon>
)} )}
</HStack> </HStack>
</NavbarRight> </NavbarRight>
@@ -128,8 +138,17 @@ export const NavbarIcon = styled.div`
const TitleText = styled.span` const TitleText = styled.span`
margin-left: 5px; margin-left: 5px;
font-family: Ubuntu; font-family: Ubuntu;
font-size: 13px; font-size: 12px;
user-select: none; user-select: none;
@media (max-width: 1080px) {
display: none;
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
` `
export default HeaderNavbar export default HeaderNavbar

View File

@@ -181,7 +181,7 @@ const AssistantItem = styled.div`
margin: 0 10px; margin: 0 10px;
padding-right: 35px; padding-right: 35px;
font-family: Ubuntu; font-family: Ubuntu;
border-radius: 16px; border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
cursor: pointer; cursor: pointer;
.iconfont { .iconfont {

View File

@@ -38,7 +38,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { t } = useTranslation() const { t } = useTranslation()
const { showTopicTime, topicPosition } = useSettings() const { showTopicTime, topicPosition } = useSettings()
const borderRadius = showTopicTime ? 12 : 17 const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
const onDeleteTopic = useCallback( const onDeleteTopic = useCallback(
async (topic: Topic) => { async (topic: Topic) => {
@@ -185,8 +185,8 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}> <Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem <TopicListItem
className={isActive ? 'active' : ''} className={isActive ? 'active' : ''}
style={{ borderRadius }} onClick={() => onSwitchTopic(topic)}
onClick={() => onSwitchTopic(topic)}> style={{ borderRadius }}>
<TopicName className="name">{topic.name.replace('`', '')}</TopicName> <TopicName className="name">{topic.name.replace('`', '')}</TopicName>
{showTopicTime && ( {showTopicTime && (
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime> <TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
@@ -223,8 +223,9 @@ const Container = styled(Scrollbar)`
const TopicListItem = styled.div` const TopicListItem = styled.div`
padding: 7px 12px; padding: 7px 12px;
margin: 0 10px; margin-left: 10px;
border-radius: 16px; margin-right: 4px;
border-radius: var(--list-item-border-radius);
font-family: Ubuntu; font-family: Ubuntu;
font-size: 13px; font-size: 13px;
display: flex; display: flex;

View File

@@ -1,8 +1,7 @@
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import VisionIcon from '@renderer/components/Icons/VisionIcon' import ModelTags from '@renderer/components/ModelTags'
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup' import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
import { isLocalAi } from '@renderer/config/env' import { isLocalAi } from '@renderer/config/env'
import { isVisionModel } from '@renderer/config/models'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { getProviderName } from '@renderer/services/ProviderService' import { getProviderName } from '@renderer/services/ProviderService'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
@@ -40,7 +39,7 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
<ModelName> <ModelName>
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''} {model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''}
</ModelName> </ModelName>
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />} <ModelTags model={model} showFree={false} />
</ButtonContent> </ButtonContent>
</DropdownButton> </DropdownButton>
) )
@@ -63,7 +62,6 @@ const ButtonContent = styled.div`
` `
const ModelName = styled.span` const ModelName = styled.span`
margin-left: -2px;
font-weight: 500; font-weight: 500;
` `

View File

@@ -126,9 +126,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
} }
const url = await PromptPopup.show({ const url = await PromptPopup.show({
title: t('knowledge_base.add_url'), title: t('knowledge.add_url'),
message: '', message: '',
inputPlaceholder: t('knowledge_base.url_placeholder'), inputPlaceholder: t('knowledge.url_placeholder'),
inputProps: { inputProps: {
maxLength: 1000, maxLength: 1000,
rows: 1 rows: 1
@@ -139,7 +139,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
try { try {
new URL(url) new URL(url)
if (urlItems.find((item) => item.content === url)) { if (urlItems.find((item) => item.content === url)) {
message.success(t('knowledge_base.url_added')) message.success(t('knowledge.url_added'))
return return
} }
addUrl(url) addUrl(url)
@@ -155,9 +155,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
} }
const url = await PromptPopup.show({ const url = await PromptPopup.show({
title: t('knowledge_base.add_sitemap'), title: t('knowledge.add_sitemap'),
message: '', message: '',
inputPlaceholder: t('knowledge_base.sitemap_placeholder'), inputPlaceholder: t('knowledge.sitemap_placeholder'),
inputProps: { inputProps: {
maxLength: 1000, maxLength: 1000,
rows: 1 rows: 1
@@ -168,7 +168,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
try { try {
new URL(url) new URL(url)
if (sitemapItems.find((item) => item.content === url)) { if (sitemapItems.find((item) => item.content === url)) {
message.success(t('knowledge_base.sitemap_added')) message.success(t('knowledge.sitemap_added'))
return return
} }
addSitemap(url) addSitemap(url)
@@ -209,16 +209,16 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
return ( return (
<MainContent> <MainContent>
{!base?.version && ( {!base?.version && (
<Alert message={t('knowledge_base.not_support')} type="error" style={{ marginBottom: 20 }} showIcon /> <Alert message={t('knowledge.not_support')} type="error" style={{ marginBottom: 20 }} showIcon />
)} )}
{!providerName && ( {!providerName && (
<Alert message={t('knowledge_base.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon /> <Alert message={t('knowledge.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon />
)} )}
<FileSection> <FileSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('files.title')}</Title> <Title level={5}>{t('files.title')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddFile} disabled={disabled}> <Button icon={<PlusOutlined />} onClick={handleAddFile} disabled={disabled}>
{t('knowledge_base.add_file')} {t('knowledge.add_file')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<Dragger <Dragger
@@ -227,9 +227,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
multiple={true} multiple={true}
accept={fileTypes.join(',')} accept={fileTypes.join(',')}
style={{ marginTop: 10, background: 'transparent' }}> style={{ marginTop: 10, background: 'transparent' }}>
<p className="ant-upload-text">{t('knowledge_base.drag_file')}</p> <p className="ant-upload-text">{t('knowledge.drag_file')}</p>
<p className="ant-upload-hint"> <p className="ant-upload-hint">
{t('knowledge_base.file_hint', { file_types: fileTypes.join(', ').replaceAll('.', '') })} {t('knowledge.file_hint', { file_types: fileTypes.join(', ').replaceAll('.', '') })}
</p> </p>
</Dragger> </Dragger>
</FileSection> </FileSection>
@@ -256,9 +256,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.directories')}</Title> <Title level={5}>{t('knowledge.directories')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddDirectory} disabled={disabled}> <Button icon={<PlusOutlined />} onClick={handleAddDirectory} disabled={disabled}>
{t('knowledge_base.add_directory')} {t('knowledge.add_directory')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -283,9 +283,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.urls')}</Title> <Title level={5}>{t('knowledge.urls')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddUrl} disabled={disabled}> <Button icon={<PlusOutlined />} onClick={handleAddUrl} disabled={disabled}>
{t('knowledge_base.add_url')} {t('knowledge.add_url')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -310,9 +310,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.sitemaps')}</Title> <Title level={5}>{t('knowledge.sitemaps')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddSitemap} disabled={disabled}> <Button icon={<PlusOutlined />} onClick={handleAddSitemap} disabled={disabled}>
{t('knowledge_base.add_sitemap')} {t('knowledge.add_sitemap')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -337,9 +337,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ContentSection> <ContentSection>
<TitleWrapper> <TitleWrapper>
<Title level={5}>{t('knowledge_base.notes')}</Title> <Title level={5}>{t('knowledge.notes')}</Title>
<Button icon={<PlusOutlined />} onClick={handleAddNote} disabled={disabled}> <Button icon={<PlusOutlined />} onClick={handleAddNote} disabled={disabled}>
{t('knowledge_base.add_note')} {t('knowledge.add_note')}
</Button> </Button>
</TitleWrapper> </TitleWrapper>
<FlexColumn> <FlexColumn>
@@ -363,7 +363,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<Divider style={{ margin: '10px 0' }} /> <Divider style={{ margin: '10px 0' }} />
<ModelInfo> <ModelInfo>
<label htmlFor="model-info">{t('knowledge_base.model_info')}</label> <label htmlFor="model-info">{t('knowledge.model_info')}</label>
<Tag color="blue">{base.model.name}</Tag> <Tag color="blue">{base.model.name}</Tag>
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag> <Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
{providerName && <Tag color="purple">{providerName}</Tag>} {providerName && <Tag color="purple">{providerName}</Tag>}
@@ -375,7 +375,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
onClick={() => KnowledgeSearchPopup.show({ base })} onClick={() => KnowledgeSearchPopup.show({ base })}
icon={<SearchOutlined />} icon={<SearchOutlined />}
disabled={disabled}> disabled={disabled}>
{t('knowledge_base.search')} {t('knowledge.search')}
</Button> </Button>
</IndexSection> </IndexSection>

View File

@@ -22,7 +22,7 @@ const KnowledgePage: FC = () => {
const prevLength = useRef(0) const prevLength = useRef(0)
const handleAddKnowledge = async () => { const handleAddKnowledge = async () => {
await AddKnowledgePopup.show({ title: t('knowledge_base.add.title') }) await AddKnowledgePopup.show({ title: t('knowledge.add.title') })
} }
useEffect(() => { useEffect(() => {
@@ -48,12 +48,12 @@ const KnowledgePage: FC = () => {
(base: KnowledgeBase) => { (base: KnowledgeBase) => {
const menus: MenuProps['items'] = [ const menus: MenuProps['items'] = [
{ {
label: t('knowledge_base.rename'), label: t('knowledge.rename'),
key: 'rename', key: 'rename',
icon: <EditOutlined />, icon: <EditOutlined />,
async onClick() { async onClick() {
const name = await PromptPopup.show({ const name = await PromptPopup.show({
title: t('knowledge_base.rename'), title: t('knowledge.rename'),
message: '', message: '',
defaultValue: base.name || '' defaultValue: base.name || ''
}) })
@@ -70,7 +70,7 @@ const KnowledgePage: FC = () => {
icon: <DeleteOutlined />, icon: <DeleteOutlined />,
onClick: () => { onClick: () => {
window.modal.confirm({ window.modal.confirm({
title: t('knowledge_base.delete_confirm'), title: t('knowledge.delete_confirm'),
centered: true, centered: true,
onOk: () => { onOk: () => {
deleteKnowledgeBase(base.id) deleteKnowledgeBase(base.id)
@@ -88,7 +88,7 @@ const KnowledgePage: FC = () => {
return ( return (
<Container> <Container>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge_base.title')}</NavbarCenter> <NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
</Navbar> </Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<SideNav> <SideNav>
@@ -125,7 +125,7 @@ const KnowledgePage: FC = () => {
</SideNav> </SideNav>
{bases.length === 0 ? ( {bases.length === 0 ? (
<MainContent> <MainContent>
<Empty description={t('knowledge_base.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} /> <Empty description={t('knowledge.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</MainContent> </MainContent>
) : selectedBase ? ( ) : selectedBase ? (
<KnowledgeContent selectedBase={selectedBase} /> <KnowledgeContent selectedBase={selectedBase} />
@@ -208,7 +208,7 @@ const AddKnowledgeItem = styled.div`
padding: 7px 12px; padding: 7px 12px;
position: relative; position: relative;
font-family: Ubuntu; font-family: Ubuntu;
border-radius: 16px; border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent; border: 0.5px solid transparent;
cursor: pointer; cursor: pointer;
&:hover { &:hover {

View File

@@ -72,6 +72,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
} catch (error) { } catch (error) {
console.error('Error getting embedding dimensions:', error) console.error('Error getting embedding dimensions:', error)
window.message.error(t('message.error.get_embedding_dimensions')) window.message.error(t('message.error.get_embedding_dimensions'))
setLoading(false)
return return
} }

View File

@@ -77,7 +77,7 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
return ( return (
<Modal <Modal
title={t('knowledge_base.search')} title={t('knowledge.search')}
open={open} open={open}
onOk={onOk} onOk={onOk}
onCancel={onCancel} onCancel={onCancel}
@@ -88,7 +88,7 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
transitionName="ant-move-down"> transitionName="ant-move-down">
<SearchContainer> <SearchContainer>
<Search <Search
placeholder={t('knowledge_base.search_placeholder')} placeholder={t('knowledge.search_placeholder')}
allowClear allowClear
enterButton enterButton
size="large" size="large"
@@ -109,7 +109,7 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
<Paragraph>{highlightText(item.pageContent)}</Paragraph> <Paragraph>{highlightText(item.pageContent)}</Paragraph>
<MetadataContainer> <MetadataContainer>
<Text type="secondary"> <Text type="secondary">
{t('knowledge_base.source')}:{' '} {t('knowledge.source')}:{' '}
{item.file ? ( {item.file ? (
<a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer"> <a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer">
{item.file.origin_name} {item.file.origin_name}

View File

@@ -21,13 +21,13 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
if (!status) { if (!status) {
if (item?.uniqueId) { if (item?.uniqueId) {
return ( return (
<Tooltip title={t('knowledge_base.status_completed')} placement="left"> <Tooltip title={t('knowledge.status_completed')} placement="left">
<CheckCircleOutlined style={{ color: '#52c41a' }} /> <CheckCircleOutlined style={{ color: '#52c41a' }} />
</Tooltip> </Tooltip>
) )
} }
return ( return (
<Tooltip title={t('knowledge_base.status_new')} placement="left"> <Tooltip title={t('knowledge.status_new')} placement="left">
<Center style={{ width: '16px', height: '16px' }}> <Center style={{ width: '16px', height: '16px' }}>
<StatusDot $status="new" /> <StatusDot $status="new" />
</Center> </Center>
@@ -38,25 +38,25 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
switch (status) { switch (status) {
case 'pending': case 'pending':
return ( return (
<Tooltip title={t('knowledge_base.status_pending')} placement="left"> <Tooltip title={t('knowledge.status_pending')} placement="left">
<StatusDot $status="pending" /> <StatusDot $status="pending" />
</Tooltip> </Tooltip>
) )
case 'processing': case 'processing':
return ( return (
<Tooltip title={t('knowledge_base.status_processing')} placement="left"> <Tooltip title={t('knowledge.status_processing')} placement="left">
<StatusDot $status="processing" /> <StatusDot $status="processing" />
</Tooltip> </Tooltip>
) )
case 'completed': case 'completed':
return ( return (
<Tooltip title={t('knowledge_base.status_completed')} placement="left"> <Tooltip title={t('knowledge.status_completed')} placement="left">
<CheckCircleOutlined style={{ color: '#52c41a' }} /> <CheckCircleOutlined style={{ color: '#52c41a' }} />
</Tooltip> </Tooltip>
) )
case 'failed': case 'failed':
return ( return (
<Tooltip title={errorText || t('knowledge_base.status_failed')} placement="left"> <Tooltip title={errorText || t('knowledge.status_failed')} placement="left">
<CloseCircleOutlined style={{ color: '#ff4d4f' }} /> <CloseCircleOutlined style={{ color: '#ff4d4f' }} />
</Tooltip> </Tooltip>
) )

View File

@@ -72,8 +72,8 @@ const Artboard: FC<ArtboardProps> = ({
preview={{ mask: false }} preview={{ mask: false }}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
style={{ style={{
width: '70vh', maxWidth: '70vh',
height: '70vh', maxHeight: '70vh',
objectFit: 'contain', objectFit: 'contain',
backgroundColor: 'var(--color-background-soft)', backgroundColor: 'var(--color-background-soft)',
cursor: 'pointer' cursor: 'pointer'

View File

@@ -407,6 +407,7 @@ const PaintingsPage: FC = () => {
<TextArea <TextArea
value={painting.negativePrompt} value={painting.negativePrompt}
onChange={(e) => updatePaintingState({ negativePrompt: e.target.value })} onChange={(e) => updatePaintingState({ negativePrompt: e.target.value })}
spellCheck={false}
rows={4} rows={4}
/> />
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}> <SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
@@ -437,6 +438,7 @@ const PaintingsPage: FC = () => {
variant="borderless" variant="borderless"
disabled={isLoading} disabled={isLoading}
value={painting.prompt} value={painting.prompt}
spellCheck={false}
onChange={(e) => updatePaintingState({ prompt: e.target.value })} onChange={(e) => updatePaintingState({ prompt: e.target.value })}
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')} placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}

View File

@@ -103,6 +103,7 @@ const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, upda
value={messages[index].content} value={messages[index].content}
onChange={(e) => updateMessages(index, 'user', e.target.value)} onChange={(e) => updateMessages(index, 'user', e.target.value)}
placeholder={t('agents.edit.message.user.placeholder')} placeholder={t('agents.edit.message.user.placeholder')}
spellCheck={false}
rows={1} rows={1}
/> />
</Col> </Col>
@@ -116,6 +117,7 @@ const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, upda
value={messages[index + 1]?.content || ''} value={messages[index + 1]?.content || ''}
onChange={(e) => updateMessages(index + 1, 'assistant', e.target.value)} onChange={(e) => updateMessages(index + 1, 'assistant', e.target.value)}
placeholder={t('agents.edit.message.assistant.placeholder')} placeholder={t('agents.edit.message.assistant.placeholder')}
spellCheck={false}
rows={3} rows={3}
/> />
</Col> </Col>

View File

@@ -43,6 +43,7 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant,
value={prompt} value={prompt}
onChange={(e) => setPrompt(e.target.value)} onChange={(e) => setPrompt(e.target.value)}
onBlur={onUpdate} onBlur={onUpdate}
spellCheck={false}
style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }} style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }}
/> />
<HStack width="100%" justifyContent="flex-end" mt="10px"> <HStack width="100%" justifyContent="flex-end" mt="10px">

View File

@@ -3,7 +3,7 @@ import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { backup, reset, restore } from '@renderer/services/BackupService' import { backup, reset, restore } from '@renderer/services/BackupService'
import { AppInfo } from '@renderer/types' import { AppInfo } from '@renderer/types'
import { Button, message, Modal, Typography } from 'antd' import { Button, Modal, Typography } from 'antd'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -42,9 +42,9 @@ const DataSettings: FC = () => {
onOk: async () => { onOk: async () => {
try { try {
await window.api.clearCache() await window.api.clearCache()
message.success(t('settings.data.clear_cache.success')) window.message.success(t('settings.data.clear_cache.success'))
} catch (error) { } catch (error) {
message.error(t('settings.data.clear_cache.error')) window.message.error(t('settings.data.clear_cache.error'))
} }
} }
}) })

View File

@@ -3,18 +3,20 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { import {
DEFAULT_SIDEBAR_ICONS,
setClickAssistantToShowTopic, setClickAssistantToShowTopic,
setCustomCss, setCustomCss,
setShowFilesIcon, setShowTopicTime,
setShowMinappIcon, setSidebarIcons
setShowTopicTime
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types' import { ThemeMode } from '@renderer/types'
import { Input, Select, Switch } from 'antd' import { Button, Input, Select, Switch } from 'antd'
import { FC } from 'react' import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import SidebarIconsManager from './SidebarIconsManager'
const DisplaySettings: FC = () => { const DisplaySettings: FC = () => {
const { const {
@@ -22,22 +24,33 @@ const DisplaySettings: FC = () => {
theme, theme,
windowStyle, windowStyle,
setWindowStyle, setWindowStyle,
showMinappIcon,
showFilesIcon,
topicPosition, topicPosition,
setTopicPosition, setTopicPosition,
clickAssistantToShowTopic, clickAssistantToShowTopic,
showTopicTime, showTopicTime,
customCss customCss,
sidebarIcons
} = useSettings() } = useSettings()
const { theme: themeMode } = useTheme() const { theme: themeMode } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const handleWindowStyleChange = (checked: boolean) => { const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
setWindowStyle(checked ? 'transparent' : 'opaque') const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
}
// 使用useCallback优化回调函数
const handleWindowStyleChange = useCallback(
(checked: boolean) => {
setWindowStyle(checked ? 'transparent' : 'opaque')
},
[setWindowStyle]
)
const handleReset = useCallback(() => {
setVisibleIcons([...DEFAULT_SIDEBAR_ICONS])
setDisabledIcons([])
dispatch(setSidebarIcons({ visible: DEFAULT_SIDEBAR_ICONS, disabled: [] }))
}, [dispatch])
return ( return (
<SettingContainer theme={themeMode}> <SettingContainer theme={themeMode}>
@@ -47,7 +60,7 @@ const DisplaySettings: FC = () => {
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
<Select <Select
defaultValue={theme} value={theme}
style={{ width: 120 }} style={{ width: 120 }}
onChange={setTheme} onChange={setTheme}
options={[ options={[
@@ -73,7 +86,7 @@ const DisplaySettings: FC = () => {
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle> <SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
<Select <Select
defaultValue={topicPosition || 'right'} value={topicPosition || 'right'}
style={{ width: 120 }} style={{ width: 120 }}
onChange={setTopicPosition} onChange={setTopicPosition}
options={[ options={[
@@ -101,24 +114,27 @@ const DisplaySettings: FC = () => {
</SettingRow> </SettingRow>
</SettingGroup> </SettingGroup>
<SettingGroup theme={theme}> <SettingGroup theme={theme}>
<SettingTitle>{t('settings.display.sidebar.title')}</SettingTitle> <SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.display.sidebar.title')}</span>
<ResetButtonWrapper>
<Button onClick={handleReset}>{t('common.reset')}</Button>
</ResetButtonWrapper>
</SettingTitle>
<SettingDivider /> <SettingDivider />
<SettingRow> <SidebarIconsManager
<SettingRowTitle>{t('settings.display.sidebar.minapp.icon')}</SettingRowTitle> visibleIcons={visibleIcons}
<Switch checked={showMinappIcon} onChange={(value) => dispatch(setShowMinappIcon(value))} /> disabledIcons={disabledIcons}
</SettingRow> setVisibleIcons={setVisibleIcons}
<SettingDivider /> setDisabledIcons={setDisabledIcons}
<SettingRow> />
<SettingRowTitle>{t('settings.display.sidebar.files.icon')}</SettingRowTitle>
<Switch checked={showFilesIcon} onChange={(value) => dispatch(setShowFilesIcon(value))} />
</SettingRow>
</SettingGroup> </SettingGroup>
<SettingGroup theme={theme}> <SettingGroup theme={theme}>
<SettingTitle>{t('settings.display.custom.css')}</SettingTitle> <SettingTitle>{t('settings.display.custom.css')}</SettingTitle>
<SettingDivider /> <SettingDivider />
<Input.TextArea <Input.TextArea
defaultValue={customCss} value={customCss}
onBlur={(e) => dispatch(setCustomCss(e.target.value))} onChange={(e) => dispatch(setCustomCss(e.target.value))}
placeholder={t('settings.display.custom.css.placeholder')} placeholder={t('settings.display.custom.css.placeholder')}
style={{ style={{
minHeight: 200, minHeight: 200,
@@ -130,4 +146,10 @@ const DisplaySettings: FC = () => {
) )
} }
const ResetButtonWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
export default DisplaySettings export default DisplaySettings

View File

@@ -0,0 +1,272 @@
import { CloseOutlined } from '@ant-design/icons'
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import {
DragDropContext,
Draggable,
DraggableProvided,
Droppable,
DroppableProvided,
DropResult
} from '@hello-pangea/dnd'
import { useAppDispatch } from '@renderer/store'
import { setSidebarIcons } from '@renderer/store/settings'
import { message } from 'antd'
import { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SidebarIcon } from '../../../store/settings'
interface SidebarIconsManagerProps {
visibleIcons: SidebarIcon[]
disabledIcons: SidebarIcon[]
setVisibleIcons: (icons: SidebarIcon[]) => void
setDisabledIcons: (icons: SidebarIcon[]) => void
}
const SidebarIconsManager: FC<SidebarIconsManagerProps> = ({
visibleIcons,
disabledIcons,
setVisibleIcons,
setDisabledIcons
}) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const onDragEnd = useCallback(
(result: DropResult) => {
if (!result.destination) return
const { source, destination } = result
// 如果是chat图标且目标是disabled区域,则不允许移动并提示
const draggedItem = source.droppableId === 'visible' ? visibleIcons[source.index] : disabledIcons[source.index]
if (draggedItem === 'assistants' && destination.droppableId === 'disabled') {
message.warning(t('settings.display.sidebar.chat.hiddenMessage'))
return
}
if (source.droppableId === destination.droppableId) {
const list = source.droppableId === 'visible' ? [...visibleIcons] : [...disabledIcons]
const [removed] = list.splice(source.index, 1)
list.splice(destination.index, 0, removed)
if (source.droppableId === 'visible') {
setVisibleIcons(list)
dispatch(setSidebarIcons({ visible: list, disabled: disabledIcons }))
} else {
setDisabledIcons(list)
dispatch(setSidebarIcons({ visible: visibleIcons, disabled: list }))
}
return
}
const sourceList = source.droppableId === 'visible' ? [...visibleIcons] : [...disabledIcons]
const destList = destination.droppableId === 'visible' ? [...visibleIcons] : [...disabledIcons]
const [removed] = sourceList.splice(source.index, 1)
const targetList = destList.filter((icon) => icon !== removed)
targetList.splice(destination.index, 0, removed)
const newVisibleIcons = destination.droppableId === 'visible' ? targetList : sourceList
const newDisabledIcons = destination.droppableId === 'disabled' ? targetList : sourceList
setVisibleIcons(newVisibleIcons)
setDisabledIcons(newDisabledIcons)
dispatch(setSidebarIcons({ visible: newVisibleIcons, disabled: newDisabledIcons }))
},
[visibleIcons, disabledIcons, dispatch, setVisibleIcons, setDisabledIcons, t]
)
const onMoveIcon = useCallback(
(icon: SidebarIcon, fromList: 'visible' | 'disabled') => {
// 如果是chat图标且要移动到disabled列表,则不允许并提示
if (icon === 'assistants' && fromList === 'visible') {
message.warning(t('settings.display.sidebar.chat.hiddenMessage'))
return
}
if (fromList === 'visible') {
const newVisibleIcons = visibleIcons.filter((i) => i !== icon)
const newDisabledIcons = disabledIcons.some((i) => i === icon) ? disabledIcons : [...disabledIcons, icon]
setVisibleIcons(newVisibleIcons)
setDisabledIcons(newDisabledIcons)
dispatch(setSidebarIcons({ visible: newVisibleIcons, disabled: newDisabledIcons }))
} else {
const newDisabledIcons = disabledIcons.filter((i) => i !== icon)
const newVisibleIcons = visibleIcons.some((i) => i === icon) ? visibleIcons : [...visibleIcons, icon]
setDisabledIcons(newDisabledIcons)
setVisibleIcons(newVisibleIcons)
dispatch(setSidebarIcons({ visible: newVisibleIcons, disabled: newDisabledIcons }))
}
},
[t, visibleIcons, disabledIcons, setVisibleIcons, setDisabledIcons, dispatch]
)
// 使用useMemo缓存图标映射
const iconMap = useMemo(
() => ({
assistants: <i className="iconfont icon-chat" />,
agents: <i className="iconfont icon-business-smart-assistant" />,
paintings: <PictureOutlined style={{ fontSize: 14 }} />,
translate: <TranslationOutlined />,
minapp: <i className="iconfont icon-appstore" />,
knowledge: <FileSearchOutlined />,
files: <FolderOutlined />
}),
[]
)
const renderIcon = (icon: SidebarIcon) => iconMap[icon] || <i className={`iconfont ${icon}`} />
return (
<DragDropContext onDragEnd={onDragEnd}>
<IconSection>
<IconColumn>
<h4>{t('settings.display.sidebar.visible')}</h4>
<Droppable droppableId="visible">
{(provided: DroppableProvided) => (
<IconList ref={provided.innerRef} {...provided.droppableProps}>
{visibleIcons.map((icon, index) => (
<Draggable key={icon} draggableId={icon} index={index}>
{(provided: DraggableProvided) => (
<IconItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<IconContent>
{renderIcon(icon)}
<span>{t(`${icon}.title`)}</span>
</IconContent>
{icon !== 'assistants' && (
<CloseButton onClick={() => onMoveIcon(icon, 'visible')}>
<CloseOutlined />
</CloseButton>
)}
</IconItem>
)}
</Draggable>
))}
{provided.placeholder}
</IconList>
)}
</Droppable>
</IconColumn>
<IconColumn>
<h4>{t('settings.display.sidebar.disabled')}</h4>
<Droppable droppableId="disabled">
{(provided: DroppableProvided) => (
<IconList ref={provided.innerRef} {...provided.droppableProps}>
{disabledIcons.length === 0 ? (
<EmptyPlaceholder>{t('settings.display.sidebar.empty')}</EmptyPlaceholder>
) : (
disabledIcons.map((icon, index) => (
<Draggable key={icon} draggableId={icon} index={index}>
{(provided: DraggableProvided) => (
<IconItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<IconContent>
{renderIcon(icon)}
<span>{t(`${icon}.title`)}</span>
</IconContent>
<CloseButton onClick={() => onMoveIcon(icon, 'disabled')}>
<CloseOutlined />
</CloseButton>
</IconItem>
)}
</Draggable>
))
)}
{provided.placeholder}
</IconList>
)}
</Droppable>
</IconColumn>
</IconSection>
</DragDropContext>
)
}
// Styled components remain the same
const IconSection = styled.div`
display: flex;
gap: 20px;
padding: 10px;
background: var(--color-background);
`
const IconColumn = styled.div`
flex: 1;
h4 {
margin-bottom: 10px;
color: var(--color-text);
font-weight: normal;
}
`
const IconList = styled.div`
height: 365px;
min-height: 365px;
padding: 10px;
background: var(--color-background-soft);
border-radius: 8px;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
overflow-y: hidden;
`
const IconItem = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin-bottom: 8px;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 4px;
cursor: move;
`
const IconContent = styled.div`
display: flex;
align-items: center;
gap: 10px;
.iconfont {
font-size: 16px;
color: var(--color-text);
}
span {
color: var(--color-text);
}
`
const CloseButton = styled.div`
cursor: pointer;
color: var(--color-text-2);
opacity: 0;
transition: all 0.2s;
&:hover {
color: var(--color-text);
}
${IconItem}:hover & {
opacity: 1;
}
`
const EmptyPlaceholder = styled.div`
display: flex;
flex: 1;
align-items: center;
justify-content: center;
color: var(--color-text-2);
text-align: center;
padding: 20px;
font-size: 14px;
`
export default SidebarIconsManager

View File

@@ -89,6 +89,7 @@ const AssistantSettings: FC = () => {
value={defaultAssistant.prompt} value={defaultAssistant.prompt}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })} onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
style={{ margin: '10px 0' }} style={{ margin: '10px 0' }}
spellCheck={false}
/> />
<SettingSubtitle <SettingSubtitle
style={{ style={{

View File

@@ -1,13 +1,12 @@
import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons' import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon'
import WebSearchIcon from '@renderer/components/Icons/WebSearchIcon'
import { Center } from '@renderer/components/Layout' import { Center } from '@renderer/components/Layout'
import ModelTags from '@renderer/components/ModelTags'
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel, SYSTEM_MODELS } from '@renderer/config/models' import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel, SYSTEM_MODELS } from '@renderer/config/models'
import { useProvider } from '@renderer/hooks/useProvider' import { useProvider } from '@renderer/hooks/useProvider'
import { fetchModels } from '@renderer/services/ApiService' import { fetchModels } from '@renderer/services/ApiService'
import { Model, Provider } from '@renderer/types' import { Model, Provider } from '@renderer/types'
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils' import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tag, Tooltip } from 'antd' import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tooltip } from 'antd'
import Search from 'antd/es/input/Search' import Search from 'antd/es/input/Search'
import { groupBy, isEmpty, uniqBy } from 'lodash' import { groupBy, isEmpty, uniqBy } from 'lodash'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@@ -82,16 +81,18 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
setLoading(true) setLoading(true)
const models = await fetchModels(_provider) const models = await fetchModels(_provider)
setListModels( setListModels(
models.map((model) => ({ models
id: model.id, .map((model) => ({
// @ts-ignore name id: model.id,
name: model.name || model.id, // @ts-ignore name
provider: _provider.id, name: model.name || model.id,
group: getDefaultGroupName(model.id), provider: _provider.id,
// @ts-ignore name group: getDefaultGroupName(model.id),
description: model?.description, // @ts-ignore name
owned_by: model?.owned_by description: model?.description,
})) owned_by: model?.owned_by
}))
.filter((model) => !isEmpty(model.id))
) )
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
@@ -156,18 +157,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
<Tooltip title={model.id} placement="top"> <Tooltip title={model.id} placement="top">
<span style={{ cursor: 'help' }}>{model.name}</span> <span style={{ cursor: 'help' }}>{model.name}</span>
</Tooltip> </Tooltip>
{isVisionModel(model) && <VisionIcon />} <ModelTags model={model} />
{isWebSearchModel(model) && <WebSearchIcon />}
{isFreeModel(model) && (
<Tag style={{ marginLeft: 10 }} color="green">
{t('models.free')}
</Tag>
)}
{isEmbeddingModel(model) && (
<Tag style={{ marginLeft: 10 }} color="orange">
{t('models.embedding')}
</Tag>
)}
{!isEmpty(model.description) && ( {!isEmpty(model.description) && (
<Popover <Popover
trigger="click" trigger="click"

View File

@@ -7,16 +7,8 @@ import {
PlusOutlined, PlusOutlined,
SettingOutlined SettingOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import VisionIcon from '@renderer/components/Icons/VisionIcon' import ModelTags from '@renderer/components/ModelTags'
import WebSearchIcon from '@renderer/components/Icons/WebSearchIcon' import { EMBEDDING_REGEX, getModelLogo, VISION_REGEX } from '@renderer/config/models'
import {
EMBEDDING_REGEX,
getModelLogo,
isEmbeddingModel,
isVisionModel,
isWebSearchModel,
VISION_REGEX
} from '@renderer/config/models'
import { PROVIDER_CONFIG } from '@renderer/config/providers' import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
@@ -27,7 +19,7 @@ import { checkApi } from '@renderer/services/ApiService'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setModel } from '@renderer/store/assistants' import { setModel } from '@renderer/store/assistants'
import { Model, ModelType, Provider } from '@renderer/types' import { Model, ModelType, Provider } from '@renderer/types'
import { Avatar, Button, Card, Checkbox, Divider, Flex, Input, Popover, Space, Switch, Tag } from 'antd' import { Avatar, Button, Card, Checkbox, Divider, Flex, Input, Popover, Space, Switch } from 'antd'
import Link from 'antd/es/typography/Link' import Link from 'antd/es/typography/Link'
import { groupBy, isEmpty } from 'lodash' import { groupBy, isEmpty } from 'lodash'
import { FC, useEffect, useState } from 'react' import { FC, useEffect, useState } from 'react'
@@ -278,13 +270,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}> <Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
{model.name[0].toUpperCase()} {model.name[0].toUpperCase()}
</Avatar> </Avatar>
{model.name} {isVisionModel(model) && <VisionIcon />} {model.name}
{isWebSearchModel(model) && <WebSearchIcon />} <ModelTags model={model} />
{isEmbeddingModel(model) && (
<Tag style={{ marginLeft: 10 }} color="orange">
{t('models.embedding')}
</Tag>
)}
<Popover content={modelTypeContent(model)} title={t('models.type.select')} trigger="click"> <Popover content={modelTypeContent(model)} title={t('models.type.select')} trigger="click">
<SettingIcon /> <SettingIcon />
</Popover> </Popover>

View File

@@ -124,7 +124,7 @@ const ProvidersList: FC = () => {
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name} {provider.isSystem ? t(`provider.${provider.id}`) : provider.name}
</ProviderItemName> </ProviderItemName>
{provider.enabled && ( {provider.enabled && (
<Tag color="green" style={{ marginLeft: 'auto', borderRadius: 16 }}> <Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
ON ON
</Tag> </Tag>
)} )}
@@ -163,7 +163,7 @@ const Container = styled.div`
const ProviderListContainer = styled.div` const ProviderListContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: var(--assistants-width); min-width: calc(var(--settings-width) + 10px);
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
` `
@@ -173,16 +173,17 @@ const ProviderList = styled.div`
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
padding: 8px; padding: 8px;
padding-right: 5px;
` `
const ProviderListItem = styled.div` const ProviderListItem = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding: 5px 8px; padding: 5px 10px;
width: 100%; width: 100%;
cursor: grab; cursor: grab;
border-radius: 16px; border-radius: var(--list-item-border-radius);
font-size: 14px; font-size: 14px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
border: 0.5px solid transparent; border: 0.5px solid transparent;

View File

@@ -15,7 +15,7 @@ import styled from 'styled-components'
import AboutSettings from './AboutSettings' import AboutSettings from './AboutSettings'
import DataSettings from './DataSettings/DataSettings' import DataSettings from './DataSettings/DataSettings'
import DisplaySettings from './DisplaySettings' import DisplaySettings from './DisplaySettings/DisplaySettings'
import GeneralSettings from './GeneralSettings' import GeneralSettings from './GeneralSettings'
import ModelSettings from './ModalSettings/ModelSettings' import ModelSettings from './ModalSettings/ModelSettings'
import ProvidersList from './ProviderSettings' import ProvidersList from './ProviderSettings'
@@ -132,7 +132,7 @@ const MenuItem = styled.li`
padding: 6px 10px; padding: 6px 10px;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
border-radius: 16px; border-radius: var(--list-item-border-radius);
font-weight: 500; font-weight: 500;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
border: 0.5px solid transparent; border: 0.5px solid transparent;

View File

@@ -211,6 +211,7 @@ const TranslatePage: FC = () => {
value={text} value={text}
onChange={(e) => setText(e.target.value)} onChange={(e) => setText(e.target.value)}
disabled={loading} disabled={loading}
spellCheck={false}
allowClear allowClear
/> />
<TranslateButton <TranslateButton

View File

@@ -8,6 +8,7 @@ import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@rende
import { EVENT_NAMES } from '@renderer/services/EventService' import { EVENT_NAMES } from '@renderer/services/EventService'
import { filterContextMessages } from '@renderer/services/MessagesService' import { filterContextMessages } from '@renderer/services/MessagesService'
import { Assistant, FileTypes, Message, Provider, Suggestion } from '@renderer/types' import { Assistant, FileTypes, Message, Provider, Suggestion } from '@renderer/types'
import { removeSpecialCharacters } from '@renderer/utils'
import { first, flatten, last, sum, takeRight } from 'lodash' import { first, flatten, last, sum, takeRight } from 'lodash'
import OpenAI from 'openai' import OpenAI from 'openai'
@@ -206,7 +207,9 @@ export default class AnthropicProvider extends BaseProvider {
max_tokens: 4096 max_tokens: 4096
}) })
return message.content[0].type === 'text' ? message.content[0].text : '' const content = message.content[0].type === 'text' ? message.content[0].text : ''
return removeSpecialCharacters(content)
} }
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> { public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {

View File

@@ -1,5 +1,6 @@
import { import {
Content, Content,
FileDataPart,
GoogleGenerativeAI, GoogleGenerativeAI,
HarmBlockThreshold, HarmBlockThreshold,
HarmCategory, HarmCategory,
@@ -14,7 +15,8 @@ import i18n from '@renderer/i18n'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService' import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES } from '@renderer/services/EventService' import { EVENT_NAMES } from '@renderer/services/EventService'
import { filterContextMessages } from '@renderer/services/MessagesService' import { filterContextMessages } from '@renderer/services/MessagesService'
import { Assistant, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types' import { Assistant, FileType, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types'
import { removeSpecialCharacters } from '@renderer/utils'
import axios from 'axios' import axios from 'axios'
import { first, isEmpty, last, takeRight } from 'lodash' import { first, isEmpty, last, takeRight } from 'lodash'
import OpenAI from 'openai' import OpenAI from 'openai'
@@ -38,6 +40,43 @@ export default class GeminiProvider extends BaseProvider {
return this.provider.apiHost return this.provider.apiHost
} }
private async handlePdfFile(file: FileType): Promise<Part> {
const smallFileSize = 20 * 1024 * 1024
const isSmallFile = file.size < smallFileSize
if (isSmallFile) {
const { data, mimeType } = await window.api.gemini.base64File(file)
return {
inlineData: {
data,
mimeType
}
} as InlineDataPart
}
// Retrieve file from Gemini uploaded files
const fileMetadata = await window.api.gemini.retrieveFile(file, this.apiKey)
if (fileMetadata) {
return {
fileData: {
fileUri: fileMetadata.uri,
mimeType: fileMetadata.mimeType
}
} as FileDataPart
}
// If file is not found, upload it to Gemini
const uploadResult = await window.api.gemini.uploadFile(file, this.apiKey)
return {
fileData: {
fileUri: uploadResult.file.uri,
mimeType: uploadResult.file.mimeType
}
} as FileDataPart
}
private async getMessageContents(message: Message): Promise<Content> { private async getMessageContents(message: Message): Promise<Content> {
const role = message.role === 'user' ? 'user' : 'model' const role = message.role === 'user' ? 'user' : 'model'
@@ -53,6 +92,12 @@ export default class GeminiProvider extends BaseProvider {
} }
} as InlineDataPart) } as InlineDataPart)
} }
if (file.ext === '.pdf') {
parts.push(await this.handlePdfFile(file))
continue
}
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) { if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim() const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
parts.push({ parts.push({
@@ -92,7 +137,7 @@ export default class GeminiProvider extends BaseProvider {
model: model.id, model: model.id,
systemInstruction: assistant.prompt, systemInstruction: assistant.prompt,
// @ts-ignore googleSearch is not a valid tool for Gemini // @ts-ignore googleSearch is not a valid tool for Gemini
tools: assistant.enableWebSearch && isWebSearchModel(model) ? [{ googleSearch: {} }] : [], tools: assistant.enableWebSearch && isWebSearchModel(model) ? [{ googleSearch: {} }] : undefined,
generationConfig: { generationConfig: {
maxOutputTokens: maxTokens, maxOutputTokens: maxTokens,
temperature: assistant?.settings?.temperature, temperature: assistant?.settings?.temperature,
@@ -223,7 +268,7 @@ export default class GeminiProvider extends BaseProvider {
const { response } = await chat.sendMessage(userMessage.content) const { response } = await chat.sendMessage(userMessage.content)
return response.text() return removeSpecialCharacters(response.text())
} }
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> { public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {

View File

@@ -1,11 +1,11 @@
import { isEmbeddingModel, isSupportedModel, isVisionModel, isWebSearchModel } from '@renderer/config/models' import { getWebSearchParams, isEmbeddingModel, isSupportedModel, isVisionModel } from '@renderer/config/models'
import { getStoreSetting } from '@renderer/hooks/useSettings' import { getStoreSetting } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService' import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
import { EVENT_NAMES } from '@renderer/services/EventService' import { EVENT_NAMES } from '@renderer/services/EventService'
import { filterContextMessages } from '@renderer/services/MessagesService' import { filterContextMessages } from '@renderer/services/MessagesService'
import { Assistant, FileTypes, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types' import { Assistant, FileTypes, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
import { removeQuotes } from '@renderer/utils' import { removeSpecialCharacters } from '@renderer/utils'
import { last, takeRight } from 'lodash' import { last, takeRight } from 'lodash'
import OpenAI, { AzureOpenAI } from 'openai' import OpenAI, { AzureOpenAI } from 'openai'
import { import {
@@ -133,7 +133,13 @@ export default class OpenAIProvider extends BaseProvider {
} }
const isOpenAIo1 = model.id.includes('o1-') const isOpenAIo1 = model.id.includes('o1-')
const isSupportStreamOutput = streamOutput
const isSupportStreamOutput = () => {
if (this.provider.id === 'github' && isOpenAIo1) {
return false
}
return streamOutput
}
let time_first_token_millsec = 0 let time_first_token_millsec = 0
const start_time_millsec = new Date().getTime() const start_time_millsec = new Date().getTime()
@@ -148,12 +154,12 @@ export default class OpenAIProvider extends BaseProvider {
top_p: assistant?.settings?.topP, top_p: assistant?.settings?.topP,
max_tokens: maxTokens, max_tokens: maxTokens,
keep_alive: this.keepAliveTime, keep_alive: this.keepAliveTime,
stream: isSupportStreamOutput, stream: isSupportStreamOutput(),
...(isWebSearchModel(model) ? { enable_enhancement: true } : {}), ...(assistant.enableWebSearch ? getWebSearchParams(model) : {}),
...this.getCustomParameters(assistant) ...this.getCustomParameters(assistant)
}) })
if (!isSupportStreamOutput) { if (!isSupportStreamOutput()) {
const time_completion_millsec = new Date().getTime() - start_time_millsec const time_completion_millsec = new Date().getTime() - start_time_millsec
return onChunk({ return onChunk({
text: stream.choices[0].message?.content || '', text: stream.choices[0].message?.content || '',
@@ -240,7 +246,7 @@ export default class OpenAIProvider extends BaseProvider {
max_tokens: 1000 max_tokens: 1000
}) })
return removeQuotes(response.choices[0].message?.content?.substring(0, 50) || '') return removeSpecialCharacters(response.choices[0].message?.content?.substring(0, 50) || '')
} }
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> { public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {

View File

@@ -1,4 +1,4 @@
import { Metrics } from "@renderer/types" import type { Assistant, Metrics } from '@renderer/types'
interface ChunkCallbackData { interface ChunkCallbackData {
text?: string text?: string

View File

@@ -230,12 +230,8 @@ export async function fetchModels(provider: Provider) {
function formatErrorMessage(error: any): string { function formatErrorMessage(error: any): string {
try { try {
return ( return '```json\n' + JSON.stringify(error, null, 2) + '\n```'
'```json\n' +
JSON.stringify(error?.response?.data || error?.response || error?.request || error, null, 2) +
'\n```'
)
} catch (e) { } catch (e) {
return 'Error: ' + error.message return 'Error: ' + error?.message
} }
} }

View File

@@ -105,6 +105,7 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
maxTokens: getAssistantMaxTokens(), maxTokens: getAssistantMaxTokens(),
streamOutput: assistant?.settings?.streamOutput ?? true, streamOutput: assistant?.settings?.streamOutput ?? true,
hideMessages: assistant?.settings?.hideMessages ?? false, hideMessages: assistant?.settings?.hideMessages ?? false,
defaultModel: assistant?.defaultModel ?? undefined,
autoResetModel: assistant?.settings?.autoResetModel ?? false, autoResetModel: assistant?.settings?.autoResetModel ?? false,
customParameters: assistant?.settings?.customParameters ?? [] customParameters: assistant?.settings?.customParameters ?? []
} }

View File

@@ -1,7 +1,7 @@
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces' import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
import AiProvider from '@renderer/providers/AiProvider' import AiProvider from '@renderer/providers/AiProvider'
import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types' import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types'
import { isEmpty, take } from 'lodash' import { take } from 'lodash'
import { getProviderByModel } from './AssistantService' import { getProviderByModel } from './AssistantService'
import FileManager from './FileManager' import FileManager from './FileManager'
@@ -10,10 +10,6 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
const provider = getProviderByModel(base.model) const provider = getProviderByModel(base.model)
const aiProvider = new AiProvider(provider) const aiProvider = new AiProvider(provider)
if (provider.id === 'ollama' && isEmpty(provider.apiKey)) {
provider.apiKey = 'empty'
}
let host = aiProvider.getBaseURL() let host = aiProvider.getBaseURL()
if (provider.type === 'gemini') { if (provider.type === 'gemini') {
@@ -24,7 +20,7 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
id: base.id, id: base.id,
model: base.model.id, model: base.model.id,
dimensions: base.dimensions, dimensions: base.dimensions,
apiKey: aiProvider.getApiKey(), apiKey: aiProvider.getApiKey() || 'secret',
apiVersion: provider.apiVersion, apiVersion: provider.apiVersion,
baseURL: host baseURL: host
} }

View File

@@ -28,7 +28,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 51, version: 54,
blacklist: ['runtime'], blacklist: ['runtime'],
migrate migrate
}, },

View File

@@ -33,6 +33,16 @@ const initialState: LlmState = {
isSystem: true, isSystem: true,
enabled: true enabled: true
}, },
{
id: 'aihubmix',
name: 'AiHubMix',
type: 'openai',
apiKey: '',
apiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix,
isSystem: true,
enabled: false
},
{ {
id: 'ollama', id: 'ollama',
name: 'Ollama', name: 'Ollama',
@@ -313,16 +323,6 @@ const initialState: LlmState = {
models: SYSTEM_MODELS.jina, models: SYSTEM_MODELS.jina,
isSystem: true, isSystem: true,
enabled: false enabled: false
},
{
id: 'aihubmix',
name: 'AiHubMix',
type: 'openai',
apiKey: '',
apiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix,
isSystem: true,
enabled: false
} }
], ],
settings: { settings: {

View File

@@ -9,6 +9,7 @@ import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist' import { createMigrate } from 'redux-persist'
import { RootState } from '.' import { RootState } from '.'
import { DEFAULT_SIDEBAR_ICONS } from './settings'
const migrateConfig = { const migrateConfig = {
'2': (state: RootState) => { '2': (state: RootState) => {
@@ -742,8 +743,6 @@ const migrateConfig = {
return state return state
}, },
'49': (state: RootState) => { '49': (state: RootState) => {
state.settings.showMinappIcon = true
state.settings.showFilesIcon = true
state.settings.pasteLongTextThreshold = 1500 state.settings.pasteLongTextThreshold = 1500
if (state.shortcuts) { if (state.shortcuts) {
state.shortcuts.shortcuts = [ state.shortcuts.shortcuts = [
@@ -775,6 +774,22 @@ const migrateConfig = {
'51': (state: RootState) => { '51': (state: RootState) => {
state.settings.topicNamingPrompt = '' state.settings.topicNamingPrompt = ''
return state return state
},
'54': (state: RootState) => {
if (state.shortcuts) {
state.shortcuts.shortcuts.push({
key: 'search_message',
shortcut: [isMac ? 'Command' : 'Ctrl', 'F'],
editable: true,
enabled: true,
system: false
})
}
state.settings.sidebarIcons = {
visible: DEFAULT_SIDEBAR_ICONS,
disabled: []
}
return state
} }
} }

View File

@@ -4,6 +4,18 @@ import { CodeStyleVarious, LanguageVarious, ThemeMode } from '@renderer/types'
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter' export type SendMessageShortcut = 'Enter' | 'Shift+Enter' | 'Ctrl+Enter' | 'Command+Enter'
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'assistants',
'agents',
'paintings',
'translate',
'minapp',
'knowledge',
'files'
]
export interface SettingsState { export interface SettingsState {
showAssistants: boolean showAssistants: boolean
showTopics: boolean showTopics: boolean
@@ -41,11 +53,14 @@ export interface SettingsState {
translateModelPrompt: string translateModelPrompt: string
autoTranslateWithSpace: boolean autoTranslateWithSpace: boolean
enableTopicNaming: boolean enableTopicNaming: boolean
// Sidebar icons
showMinappIcon: boolean
showFilesIcon: boolean
customCss: string customCss: string
topicNamingPrompt: string topicNamingPrompt: string
// Sidebar icons
sidebarIcons: {
visible: SidebarIcon[]
disabled: SidebarIcon[]
}
narrowMode: boolean
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@@ -84,10 +99,13 @@ const initialState: SettingsState = {
translateModelPrompt: TRANSLATE_PROMPT, translateModelPrompt: TRANSLATE_PROMPT,
autoTranslateWithSpace: false, autoTranslateWithSpace: false,
enableTopicNaming: true, enableTopicNaming: true,
showMinappIcon: true,
showFilesIcon: true,
customCss: '', customCss: '',
topicNamingPrompt: '' topicNamingPrompt: '',
sidebarIcons: {
visible: DEFAULT_SIDEBAR_ICONS,
disabled: []
},
narrowMode: false
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@@ -203,12 +221,6 @@ const settingsSlice = createSlice({
setEnableTopicNaming: (state, action: PayloadAction<boolean>) => { setEnableTopicNaming: (state, action: PayloadAction<boolean>) => {
state.enableTopicNaming = action.payload state.enableTopicNaming = action.payload
}, },
setShowMinappIcon: (state, action: PayloadAction<boolean>) => {
state.showMinappIcon = action.payload
},
setShowFilesIcon: (state, action: PayloadAction<boolean>) => {
state.showFilesIcon = action.payload
},
setPasteLongTextThreshold: (state, action: PayloadAction<number>) => { setPasteLongTextThreshold: (state, action: PayloadAction<number>) => {
state.pasteLongTextThreshold = action.payload state.pasteLongTextThreshold = action.payload
}, },
@@ -217,6 +229,12 @@ const settingsSlice = createSlice({
}, },
setTopicNamingPrompt: (state, action: PayloadAction<string>) => { setTopicNamingPrompt: (state, action: PayloadAction<string>) => {
state.topicNamingPrompt = action.payload state.topicNamingPrompt = action.payload
},
setSidebarIcons: (state, action: PayloadAction<{ visible: SidebarIcon[]; disabled: SidebarIcon[] }>) => {
state.sidebarIcons = action.payload
},
setNarrowMode: (state, action: PayloadAction<boolean>) => {
state.narrowMode = action.payload
} }
} }
}) })
@@ -258,11 +276,11 @@ export const {
setTranslateModelPrompt, setTranslateModelPrompt,
setAutoTranslateWithSpace, setAutoTranslateWithSpace,
setEnableTopicNaming, setEnableTopicNaming,
setShowMinappIcon,
setShowFilesIcon,
setPasteLongTextThreshold, setPasteLongTextThreshold,
setCustomCss, setCustomCss,
setTopicNamingPrompt setTopicNamingPrompt,
setSidebarIcons,
setNarrowMode
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@@ -44,6 +44,13 @@ const initialState: ShortcutsState = {
editable: true, editable: true,
enabled: false, enabled: false,
system: false system: false
},
{
key: 'search_message',
shortcut: [isMac ? 'Command' : 'Ctrl', 'F'],
editable: true,
enabled: true,
system: false
} }
] ]
} }

View File

@@ -35,6 +35,7 @@ export type AssistantSettings = {
enableMaxTokens: boolean enableMaxTokens: boolean
streamOutput: boolean streamOutput: boolean
hideMessages: boolean hideMessages: boolean
defaultModel?: Model
autoResetModel: boolean autoResetModel: boolean
customParameters?: AssistantSettingCustomParameters[] customParameters?: AssistantSettingCustomParameters[]
} }

View File

@@ -152,6 +152,11 @@ export function removeQuotes(str) {
return str.replace(/['"]+/g, '') return str.replace(/['"]+/g, '')
} }
export function removeSpecialCharacters(str: string) {
// First remove newlines and quotes, then remove other special characters
return str.replace(/[\n"]/g, '').replace(/[^\p{L}\p{M}\p{N}\p{P}\p{S}]/gu, '')
}
export function generateColorFromChar(char: string) { export function generateColorFromChar(char: string) {
// 使用字符的Unicode值作为随机种子 // 使用字符的Unicode值作为随机种子
const seed = char.charCodeAt(0) const seed = char.charCodeAt(0)

3586
yarn.lock

File diff suppressed because it is too large Load Diff