Compare commits
1 Commits
v0.9.5
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf61b1ce9 |
@@ -1,19 +0,0 @@
|
||||
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
|
||||
index 8a17cb7f5a68d90d2be21682db6e95ce22a3e71c..9ee868ef9d4ff3dc914b3abc3c8006deb1e9c6c6 100644
|
||||
--- a/src/markdown-loader.js
|
||||
+++ b/src/markdown-loader.js
|
||||
@@ -1,5 +1,4 @@
|
||||
import { micromark } from 'micromark';
|
||||
-import { mdxJsx } from 'micromark-extension-mdx-jsx';
|
||||
import { gfmHtml, gfm } from 'micromark-extension-gfm';
|
||||
import createDebugMessages from 'debug';
|
||||
import fs from 'node:fs';
|
||||
@@ -21,7 +20,7 @@ export class MarkdownLoader extends BaseLoader {
|
||||
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
|
||||
: await stream2buffer(fs.createReadStream(this.filePathOrUrl));
|
||||
this.debug('MarkdownLoader stream created');
|
||||
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
|
||||
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
|
||||
this.debug('Markdown parsed...');
|
||||
const webLoader = new WebLoader({
|
||||
urlOrContent: result,
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 353 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 210 KiB |
BIN
build/logo.png
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.9 KiB |
@@ -32,10 +32,6 @@ asarUnpack:
|
||||
- '**/*.{node,dll,metal,exp,lib}'
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-portable.${ext}
|
||||
target:
|
||||
- target: nsis
|
||||
- target: portable
|
||||
nsis:
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
@@ -47,7 +43,6 @@ nsis:
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
@@ -62,8 +57,9 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
linux:
|
||||
dmg:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- target: AppImage
|
||||
arch:
|
||||
@@ -71,6 +67,8 @@ linux:
|
||||
- x64
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://cherrystudio.ocool.online
|
||||
@@ -80,14 +78,4 @@ afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
全新的应用图标
|
||||
首页窗口宽度支持调整
|
||||
内容显示支持宽窄模式
|
||||
增加搜索快捷键
|
||||
可以自定义侧边栏图标 @hxp0618
|
||||
Mermaid 预览增加放大缩小功能 @hxp0618
|
||||
支持 AiHubMix 和智普联网模型
|
||||
支持使用 Gemini PDF 附件使用官方 API 进行处理
|
||||
文件模块增加 Gemini 文件列表
|
||||
修复 Ollma 嵌入模型无法创建知识库问题
|
||||
其他错误修复
|
||||
增加 Genspark 小程序
|
||||
|
||||
@@ -50,7 +50,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js', 'chunk-AFE5XGNG.js']
|
||||
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.9.5",
|
||||
"version": "0.9.2",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -50,11 +50,10 @@
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.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-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-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch",
|
||||
"@llm-tools/embedjs-loader-markdown": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-msoffice": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-pdf": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-sitemap": "^0.1.25",
|
||||
@@ -81,6 +80,7 @@
|
||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
@@ -95,7 +95,7 @@
|
||||
"@types/tinycolor2": "^1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.22.5",
|
||||
"axios": "^1.7.3",
|
||||
"axios": "^1.7.9",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import vm from 'node:vm'
|
||||
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import axios from 'axios'
|
||||
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
@@ -11,7 +13,6 @@ import BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { windowService } from './services/WindowService'
|
||||
@@ -156,23 +157,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
|
||||
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
|
||||
|
||||
// window
|
||||
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
|
||||
mainWindow?.setMinimumSize(width, height)
|
||||
// vm
|
||||
ipcMain.handle('run-js', (_, code: string) => {
|
||||
const context = vm.createContext(Object.assign({ fetch: fetch, URL: URL, axios: axios }, global))
|
||||
return vm.runInContext(code, context)
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -45,14 +45,14 @@ class KnowledgeService {
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||
dimensions,
|
||||
batchSize: 10
|
||||
batchSize: 15
|
||||
})
|
||||
: new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
configuration: { baseURL },
|
||||
dimensions,
|
||||
batchSize: 10
|
||||
batchSize: 15
|
||||
})
|
||||
)
|
||||
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
|
||||
@@ -89,14 +89,12 @@ class KnowledgeService {
|
||||
if (item.type === 'url') {
|
||||
const content = item.content as string
|
||||
if (content.startsWith('http')) {
|
||||
// @ts-ignore loader type
|
||||
return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.type === 'sitemap') {
|
||||
const content = item.content as string
|
||||
// @ts-ignore loader type
|
||||
return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload)
|
||||
}
|
||||
|
||||
@@ -124,7 +122,7 @@ class KnowledgeService {
|
||||
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
}
|
||||
|
||||
if (['.md'].includes(file.ext)) {
|
||||
if (['.md', '.mdx'].includes(file.ext)) {
|
||||
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ import { configManager } from './ConfigManager'
|
||||
export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
private isQuitting: boolean = false
|
||||
private wasFullScreen: boolean = false
|
||||
|
||||
public static getInstance(): WindowService {
|
||||
if (!WindowService.instance) {
|
||||
@@ -44,7 +42,7 @@ export class WindowService {
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: false, // 初始不显示
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
@@ -120,20 +118,9 @@ export class WindowService {
|
||||
}
|
||||
|
||||
private setupWindowEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.on('ready-to-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) {
|
||||
@@ -195,11 +182,6 @@ export class WindowService {
|
||||
}
|
||||
|
||||
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
|
||||
// 监听应用退出事件
|
||||
app.on('before-quit', () => {
|
||||
this.isQuitting = true
|
||||
})
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
const notInTray = !configManager.isTray()
|
||||
|
||||
@@ -209,15 +191,9 @@ export class WindowService {
|
||||
}
|
||||
|
||||
// Mac
|
||||
if (!this.isQuitting) {
|
||||
if (this.wasFullScreen) {
|
||||
// 如果是全屏状态,直接退出
|
||||
this.isQuitting = true
|
||||
app.quit()
|
||||
} else {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
}
|
||||
if (!app.isQuitting) {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
13
src/preload/index.d.ts
vendored
@@ -1,5 +1,4 @@
|
||||
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 { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
@@ -77,16 +76,8 @@ declare global {
|
||||
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
|
||||
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>
|
||||
vm: {
|
||||
run: (code: string) => Promise<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@@ -71,16 +71,8 @@ const api = {
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
|
||||
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)
|
||||
vm: {
|
||||
run: (code: string) => ipcRenderer.invoke('run-js', code)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,91 +1,88 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4753420 */
|
||||
src: url('iconfont.woff2?t=1736309723926') format('woff2'),
|
||||
url('iconfont.woff?t=1736309723926') format('woff'),
|
||||
url('iconfont.ttf?t=1736309723926') format('truetype');
|
||||
font-family: 'iconfont'; /* Project id 4753420 */
|
||||
src: url('iconfont.woff2?t=1733224456443') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-at:before {
|
||||
content: "\e623";
|
||||
.icon-at1:before {
|
||||
content: '\e7df';
|
||||
}
|
||||
|
||||
.icon-icon-adaptive-width:before {
|
||||
content: "\e87a";
|
||||
.icon-at:before {
|
||||
content: '\e630';
|
||||
}
|
||||
|
||||
.icon-a-darkmode:before {
|
||||
content: "\e6cd";
|
||||
content: '\e6cd';
|
||||
}
|
||||
|
||||
.icon-ai-model:before {
|
||||
content: "\e827";
|
||||
content: '\e827';
|
||||
}
|
||||
|
||||
.icon-ai-model1:before {
|
||||
content: "\ec09";
|
||||
content: '\ec09';
|
||||
}
|
||||
|
||||
.icon-gridlines:before {
|
||||
content: "\e942";
|
||||
content: '\e942';
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: "\e869";
|
||||
content: '\e869';
|
||||
}
|
||||
|
||||
.icon-business-smart-assistant:before {
|
||||
content: "\e601";
|
||||
content: '\e601';
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: "\e6ae";
|
||||
content: '\e6ae';
|
||||
}
|
||||
|
||||
.icon-ic_send:before {
|
||||
content: "\e795";
|
||||
content: '\e795';
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
content: '\e72f';
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
content: '\e6b7';
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
content: '\e7de';
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: "\e758";
|
||||
content: '\e758';
|
||||
}
|
||||
|
||||
.icon-hide-sidebar:before {
|
||||
content: "\e8eb";
|
||||
content: '\e8eb';
|
||||
}
|
||||
|
||||
.icon-show-sidebar:before {
|
||||
content: "\e944";
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: "\e792";
|
||||
content: '\e792';
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e615";
|
||||
content: '\e615';
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e78e";
|
||||
content: '\e78e';
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 44 KiB |
@@ -60,8 +60,6 @@
|
||||
--chat-background-user: #28b561;
|
||||
--chat-background-assistant: #2c2c2c;
|
||||
--chat-text-user: var(--color-black);
|
||||
|
||||
--list-item-border-radius: 16px;
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
@@ -171,9 +169,12 @@ body,
|
||||
#content-container {
|
||||
background-color: var(--color-background);
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
}
|
||||
|
||||
#content-container {
|
||||
border-top-left-radius: 12px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.loader {
|
||||
@@ -215,7 +216,10 @@ body,
|
||||
background-color: var(--chat-background);
|
||||
}
|
||||
#inputbar {
|
||||
margin: -5px 15px 15px 15px;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-mute);
|
||||
background: var(--color-background);
|
||||
}
|
||||
.system-prompt {
|
||||
|
||||
@@ -10,8 +10,9 @@ interface ListItemProps {
|
||||
}
|
||||
|
||||
const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => {
|
||||
const borderRadius = subtitle ? '10px' : '16px'
|
||||
return (
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick}>
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={{ borderRadius }}>
|
||||
<ListItemContent>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<TextContainer>
|
||||
@@ -25,7 +26,7 @@ const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) =>
|
||||
|
||||
const ListItemContainer = styled.div`
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
@@ -48,7 +48,7 @@ const AppStorePopover: FC<Props> = ({ children }) => {
|
||||
content={content}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
styles={{ body: { padding: 25 } }}>
|
||||
overlayInnerStyle={{ padding: 25 }}>
|
||||
{children}
|
||||
</Popover>
|
||||
)
|
||||
@@ -59,7 +59,7 @@ const PopoverContent = styled(Scrollbar)``
|
||||
const AppsContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(90px, 1fr));
|
||||
gap: 18px;
|
||||
gap: 25px;
|
||||
`
|
||||
|
||||
export default AppStorePopover
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { getModelLogo, isEmbeddingModel, isVisionModel } from '@renderer/config/models'
|
||||
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
@@ -12,8 +12,8 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import WebSearchIcon from '../Icons/WebSearchIcon'
|
||||
import { HStack } from '../Layout'
|
||||
import ModelTags from '../ModelTags'
|
||||
import Scrollbar from '../Scrollbar'
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number]
|
||||
@@ -75,7 +75,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
label: (
|
||||
<ModelItem>
|
||||
<span>
|
||||
{m?.name} <ModelTags model={m} />
|
||||
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />}
|
||||
</span>
|
||||
<PinIcon
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -70,7 +70,6 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
||||
ref={textareaRef}
|
||||
rows={2}
|
||||
autoFocus
|
||||
spellCheck={false}
|
||||
{...textareaProps}
|
||||
value={textValue}
|
||||
onInput={resizeTextArea}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Avatar } from 'antd'
|
||||
@@ -19,9 +19,10 @@ const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
const avatar = useAvatar()
|
||||
const { minappShow } = useRuntime()
|
||||
const { generating } = useRuntime()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { windowStyle, sidebarIcons } = useSettings()
|
||||
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
@@ -32,46 +33,14 @@ const Sidebar: FC = () => {
|
||||
const macTransparentWindow = isMac && windowStyle === 'transparent'
|
||||
const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)'
|
||||
|
||||
const to = async (path: string) => {
|
||||
await modelGenerating()
|
||||
const to = (path: string) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
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 (
|
||||
<Container
|
||||
id="app-sidebar"
|
||||
@@ -81,7 +50,61 @@ const Sidebar: FC = () => {
|
||||
}}>
|
||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||
<MainMenus>
|
||||
<Menus onClick={MinApp.onClose}>{renderMainMenus()}</Menus>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<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>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
|
||||
|
||||
@@ -8,7 +8,6 @@ import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
|
||||
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
|
||||
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg'
|
||||
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
|
||||
import HikaLogo from '@renderer/assets/images/apps/hika.webp'
|
||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
|
||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
|
||||
@@ -227,13 +226,6 @@ const _apps: MinAppType[] = [
|
||||
url: 'https://thinkany.ai/',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'hika',
|
||||
name: 'Hika',
|
||||
logo: HikaLogo,
|
||||
url: 'https://hika.fyi/',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'github-copilot',
|
||||
name: 'GitHub Copilot',
|
||||
|
||||
@@ -123,11 +123,8 @@ import YiModelLogo from '@renderer/assets/images/models/yi.png'
|
||||
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { isEmpty } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { getWebSearchTools } from './tools'
|
||||
|
||||
const visionAllowedModels = [
|
||||
'llava',
|
||||
'moondream',
|
||||
@@ -265,44 +262,6 @@ export function getModelLogo(modelId: string) {
|
||||
}
|
||||
|
||||
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: [],
|
||||
silicon: [
|
||||
{
|
||||
@@ -315,7 +274,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
id: 'Qwen/Qwen2.5-7B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2.5-7B-Instruct',
|
||||
group: 'Qwen'
|
||||
group: 'Qwen2.5'
|
||||
},
|
||||
{
|
||||
id: 'meta-llama/Llama-3.3-70B-Instruct',
|
||||
@@ -562,21 +521,9 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
],
|
||||
zhipu: [
|
||||
{
|
||||
id: 'glm-zero-preview',
|
||||
id: 'glm-4',
|
||||
provider: 'zhipu',
|
||||
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',
|
||||
name: 'GLM-4',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
@@ -620,12 +567,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AllTools',
|
||||
group: 'GLM-4-AllTools'
|
||||
},
|
||||
{
|
||||
id: 'embedding-3',
|
||||
provider: 'zhipu',
|
||||
name: 'Embedding-3',
|
||||
group: 'Embedding'
|
||||
}
|
||||
],
|
||||
moonshot: [
|
||||
@@ -809,6 +750,20 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
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: [
|
||||
{
|
||||
id: 'accounts/fireworks/models/mythomax-l2-13b',
|
||||
@@ -1006,11 +961,6 @@ export const TEXT_TO_IMAGES_MODELS = [
|
||||
}
|
||||
]
|
||||
|
||||
export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
|
||||
'stabilityai/stable-diffusion-2-1',
|
||||
'stabilityai/stable-diffusion-xl-base-1.0'
|
||||
]
|
||||
|
||||
export function isTextToImageModel(model: Model): boolean {
|
||||
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
||||
}
|
||||
@@ -1054,39 +1004,5 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider.id === 'gemini' || provider?.type === 'gemini') {
|
||||
return model?.id === 'gemini-2.0-flash-exp'
|
||||
}
|
||||
|
||||
if (provider.id === 'hunyuan') {
|
||||
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
|
||||
}
|
||||
|
||||
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 {}
|
||||
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp'
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
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 []
|
||||
}
|
||||
@@ -54,12 +54,13 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
||||
const codeToHtml = async (code: string, language: string) => {
|
||||
if (!highlighter) return ''
|
||||
|
||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
|
||||
try {
|
||||
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
|
||||
if (language in bundledLanguages || language === 'text') {
|
||||
await highlighter.loadLanguage(language as BundledLanguage)
|
||||
console.log(`Loaded language: ${language}`)
|
||||
} else {
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
@@ -15,7 +16,16 @@ import useUpdateHandler from './useUpdateHandler'
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode, customCss } = useSettings()
|
||||
const {
|
||||
proxyUrl,
|
||||
language,
|
||||
windowStyle,
|
||||
manualUpdateCheck,
|
||||
proxyMode,
|
||||
webdavAutoSync,
|
||||
webdavSyncInterval,
|
||||
customCss
|
||||
} = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
@@ -74,6 +84,10 @@ export function useAppInit() {
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
webdavAutoSync ? startAutoSync() : stopAutoSync()
|
||||
}, [webdavAutoSync, webdavSyncInterval])
|
||||
|
||||
useEffect(() => {
|
||||
import('@renderer/queue/KnowledgeQueue')
|
||||
}, [])
|
||||
|
||||
@@ -37,32 +37,4 @@ export const useMermaid = () => {
|
||||
|
||||
setTimeout(renderMermaid, 100)
|
||||
}, [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)
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ export function usePaintings() {
|
||||
paintings,
|
||||
addPainting: () => {
|
||||
const newPainting: Painting = {
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id,
|
||||
id: uuid(),
|
||||
urls: [],
|
||||
files: [],
|
||||
@@ -25,7 +24,7 @@ export function usePaintings() {
|
||||
seed: generateRandomSeed(),
|
||||
steps: 25,
|
||||
guidanceScale: 4.5,
|
||||
promptEnhancement: true
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id
|
||||
}
|
||||
dispatch(addPainting(newPainting))
|
||||
return newPainting
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
|
||||
export function useRuntime() {
|
||||
return useAppSelector((state) => state.runtime)
|
||||
}
|
||||
|
||||
export function modelGenerating() {
|
||||
const generating = store.getState().runtime.generating
|
||||
|
||||
if (generating) {
|
||||
window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'model-generating' })
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "Name",
|
||||
"open": "Open",
|
||||
"size": "Size",
|
||||
"type": "Type",
|
||||
"text": "Text",
|
||||
"title": "Files",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"delete.title": "Delete File",
|
||||
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
|
||||
"delete.paintings.warning": "Image contains this file, deletion is not possible"
|
||||
"title": "Files"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Continue Chatting",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "Download PNG",
|
||||
"svg": "Download SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "Preview",
|
||||
"source": "Source"
|
||||
@@ -254,7 +244,7 @@
|
||||
"reset.double.confirm.title": "DATA LOST !!!",
|
||||
"restore.success": "Restored successfully",
|
||||
"save.success.title": "Saved successfully",
|
||||
"switch.disabled": "Please wait for the current reply to complete",
|
||||
"switch.disabled": "Switching is disabled while the assistant is generating",
|
||||
"topic.added": "New topic added",
|
||||
"upgrade.success.button": "Restart",
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
@@ -289,9 +279,7 @@
|
||||
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
|
||||
"seed": "Seed",
|
||||
"seed_tip": "The same seed and prompt can produce similar images",
|
||||
"title": "Images",
|
||||
"prompt_enhancement": "Prompt Enhancement",
|
||||
"prompt_enhancement_tip": "Rewrite prompts into detailed, model-friendly versions when switched on"
|
||||
"title": "Images"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -374,12 +362,7 @@
|
||||
"webdav.minutes": "Minutes",
|
||||
"webdav.restore.button": "Restore from WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV User",
|
||||
"webdav.syncStatus": "Sync Status",
|
||||
"webdav.autoSync.off": "Off",
|
||||
"webdav.noSync": "Waiting for next sync",
|
||||
"webdav.syncError": "Sync Error",
|
||||
"webdav.lastSync": "Last Sync"
|
||||
"webdav.user": "WebDAV User"
|
||||
},
|
||||
"display.title": "Display Settings",
|
||||
"font_size.title": "Message font size",
|
||||
@@ -395,16 +378,9 @@
|
||||
"general.user_name.placeholder": "Enter your name",
|
||||
"general.view_webdav_settings": "View WebDAV 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.knowledge.icon": "Show Knowledge icon",
|
||||
"display.sidebar.files.icon": "Show Files icon",
|
||||
"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.custom.css": "Custom CSS",
|
||||
"display.custom.css.placeholder": "/* Put custom CSS here */",
|
||||
@@ -415,7 +391,7 @@
|
||||
"messages.input.show_estimated_tokens": "Show estimated tokens",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
||||
"messages.input.title": "Input Settings",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input message",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input msg",
|
||||
"messages.math_engine": "Math render engine",
|
||||
"messages.model.title": "Model Settings",
|
||||
"messages.title": "Message Settings",
|
||||
@@ -509,8 +485,7 @@
|
||||
"clear_shortcut": "Clear Shortcut",
|
||||
"toggle_show_assistants": "Toggle Assistants",
|
||||
"toggle_show_topics": "Toggle Topics",
|
||||
"copy_last_message": "Copy Last Message",
|
||||
"search_message": "Search Message"
|
||||
"copy_last_message": "Copy Last Message"
|
||||
},
|
||||
"theme.auto": "Auto",
|
||||
"theme.dark": "Dark",
|
||||
@@ -551,7 +526,7 @@
|
||||
"show_window": "Show Window",
|
||||
"quit": "Quit"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "Knowledge Base",
|
||||
"search": "Search knowledge base",
|
||||
"empty": "No knowledge base found",
|
||||
@@ -593,7 +568,6 @@
|
||||
"directory_placeholder": "Enter Directory Path",
|
||||
"model_info": "Model Info",
|
||||
"not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base",
|
||||
"no_provider": "Knowledge base model provider is not set, the knowledge base will no longer be supported, please create a new knowledge base",
|
||||
"source": "Source"
|
||||
},
|
||||
"models": {
|
||||
@@ -620,8 +594,7 @@
|
||||
"parameter_type": {
|
||||
"string": "Text",
|
||||
"number": "Number",
|
||||
"boolean": "Boolean",
|
||||
"json": "JSON"
|
||||
"boolean": "Boolean"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "名前",
|
||||
"open": "開く",
|
||||
"size": "サイズ",
|
||||
"type": "タイプ",
|
||||
"text": "テキスト",
|
||||
"title": "ファイル",
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
"delete.title": "ファイルを削除",
|
||||
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
|
||||
"delete.paintings.warning": "画像に含まれているため、削除できません"
|
||||
"title": "ファイル"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "チャットを続ける",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "PNGをダウンロード",
|
||||
"svg": "SVGをダウンロード"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "拡大する",
|
||||
"zoom-out": "ズームアウト"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "プレビュー",
|
||||
"source": "ソース"
|
||||
@@ -253,7 +243,7 @@
|
||||
"reset.double.confirm.title": "データが失われます!!!",
|
||||
"restore.success": "復元に成功しました",
|
||||
"save.success.title": "保存に成功しました",
|
||||
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
||||
"switch.disabled": "アシスタントが生成中は切り替えが無効です",
|
||||
"topic.added": "新しいトピックが追加されました",
|
||||
"upgrade.success.button": "再起動",
|
||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||
@@ -287,9 +277,7 @@
|
||||
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
|
||||
"seed": "シード",
|
||||
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
|
||||
"title": "画像",
|
||||
"prompt_enhancement": "プロンプト強化",
|
||||
"prompt_enhancement_tip": "オンにすると、プロンプトを詳細でモデルに適したバージョンに書き直します"
|
||||
"title": "画像"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -372,12 +360,7 @@
|
||||
"webdav.minutes": "分",
|
||||
"webdav.restore.button": "WebDAVから復元",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAVユーザー",
|
||||
"webdav.syncStatus": "同期状態",
|
||||
"webdav.autoSync.off": "オフ",
|
||||
"webdav.noSync": "次回の同期を待っています",
|
||||
"webdav.syncError": "同期エラー",
|
||||
"webdav.lastSync": "最終同期"
|
||||
"webdav.user": "WebDAVユーザー"
|
||||
},
|
||||
"display.title": "表示設定",
|
||||
"font_size.title": "メッセージのフォントサイズ",
|
||||
@@ -393,16 +376,9 @@
|
||||
"general.user_name.placeholder": "ユーザー名を入力",
|
||||
"general.view_webdav_settings": "WebDAV設定を表示",
|
||||
"general.display.title": "表示設定",
|
||||
"display.sidebar.translate.icon": "翻訳のアイコンを表示",
|
||||
"display.sidebar.painting.icon": "絵画のアイコンを表示",
|
||||
"display.sidebar.minapp.icon": "ミニアプリのアイコンを表示",
|
||||
"display.sidebar.knowledge.icon": "ナレッジのアイコンを表示",
|
||||
"display.sidebar.files.icon": "ファイルのアイコンを表示",
|
||||
"display.sidebar.title": "サイドバー設定",
|
||||
"display.sidebar.visible": "サイドバーのアイコンを表示する",
|
||||
"display.sidebar.disabled": "サイドバーのアイコンを非表示にする",
|
||||
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
|
||||
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
|
||||
"display.topic.title": "トピック設定",
|
||||
"display.custom.css": "カスタムCSS",
|
||||
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
|
||||
@@ -495,8 +471,7 @@
|
||||
"clear_shortcut": "ショートカットをクリア",
|
||||
"toggle_show_assistants": "アシスタントの表示を切り替え",
|
||||
"toggle_show_topics": "トピックの表示を切り替え",
|
||||
"copy_last_message": "最後のメッセージをコピー",
|
||||
"search_message": "メッセージを検索"
|
||||
"copy_last_message": "最後のメッセージをコピー"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "ダークテーマ",
|
||||
@@ -537,7 +512,7 @@
|
||||
"show_window": "ウィンドウを表示",
|
||||
"quit": "終了"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "ナレッジベース",
|
||||
"search": "ナレッジベースを検索",
|
||||
"empty": "ナレッジベースが見つかりません",
|
||||
@@ -576,11 +551,7 @@
|
||||
"sitemap_placeholder": "サイトマップURLを入力",
|
||||
"directories": "ディレクトリ",
|
||||
"add_directory": "ディレクトリを追加",
|
||||
"directory_placeholder": "ディレクトリパスを入力",
|
||||
"model_info": "モデル情報",
|
||||
"not_support": "ナレッジベースデータベースエンジンが更新されました。このナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
|
||||
"no_provider": "ナレッジベースモデルプロバイダーが設定されていません。ナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
|
||||
"source": "ソース"
|
||||
"directory_placeholder": "ディレクトリパスを入力"
|
||||
},
|
||||
"models": {
|
||||
"pinned": "固定済み",
|
||||
@@ -606,8 +577,7 @@
|
||||
"parameter_type": {
|
||||
"string": "テキスト",
|
||||
"number": "数値",
|
||||
"boolean": "真偽値",
|
||||
"json": "JSON"
|
||||
"boolean": "真偽値"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "Имя",
|
||||
"open": "Открыть",
|
||||
"size": "Размер",
|
||||
"type": "Тип",
|
||||
"text": "Текст",
|
||||
"title": "Файлы",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
"delete.title": "Удалить файл",
|
||||
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
|
||||
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно"
|
||||
"title": "Файлы"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Продолжить чат",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "Скачать PNG",
|
||||
"svg": "Скачать SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "Yвеличить",
|
||||
"zoom-out": "Yменьшить масштаб"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "Предпросмотр",
|
||||
"source": "Исходный код"
|
||||
@@ -254,7 +244,7 @@
|
||||
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
|
||||
"restore.success": "Успешно восстановлено",
|
||||
"save.success.title": "Успешно сохранено",
|
||||
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
||||
"switch.disabled": "Переключение отключено, пока ассистент генерирует",
|
||||
"topic.added": "Новый топик добавлен",
|
||||
"upgrade.success.button": "Перезапустить",
|
||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||
@@ -289,9 +279,7 @@
|
||||
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
|
||||
"seed": "Ключ генерации",
|
||||
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
|
||||
"title": "Изображения",
|
||||
"prompt_enhancement": "Улучшение промпта",
|
||||
"prompt_enhancement_tip": "При включении переписывает промпт в более детальную, модель-ориентированную версию"
|
||||
"title": "Изображения"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -374,12 +362,7 @@
|
||||
"webdav.minutes": "минут",
|
||||
"webdav.restore.button": "Восстановление с WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "Пользователь WebDAV",
|
||||
"webdav.syncStatus": "Статус синхронизации",
|
||||
"webdav.autoSync.off": "Выключено",
|
||||
"webdav.noSync": "Ожидание следующей синхронизации",
|
||||
"webdav.syncError": "Ошибка синхронизации",
|
||||
"webdav.lastSync": "Последняя синхронизация"
|
||||
"webdav.user": "Пользователь WebDAV"
|
||||
},
|
||||
"display.title": "Настройки отображения",
|
||||
"font_size.title": "Размер шрифта сообщений",
|
||||
@@ -395,16 +378,9 @@
|
||||
"general.user_name.placeholder": "Введите ваше имя",
|
||||
"general.view_webdav_settings": "Просмотр настроек WebDAV",
|
||||
"general.display.title": "Настройки отображения",
|
||||
"display.sidebar.translate.icon": "Показывать иконку перевода",
|
||||
"display.sidebar.painting.icon": "Показывать иконку рисования",
|
||||
"display.sidebar.minapp.icon": "Показывать иконку мини-приложения",
|
||||
"display.sidebar.knowledge.icon": "Показывать иконку знаний",
|
||||
"display.sidebar.files.icon": "Показывать иконку файлов",
|
||||
"display.sidebar.title": "Настройки боковой панели",
|
||||
"display.sidebar.visible": "Показать мои значки на боковой панели",
|
||||
"display.sidebar.disabled": "Скрыть значок на боковой панели",
|
||||
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
|
||||
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
|
||||
"display.topic.title": "Настройки топиков",
|
||||
"display.custom.css": "Пользовательский CSS",
|
||||
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
|
||||
@@ -509,8 +485,7 @@
|
||||
"clear_shortcut": "Очистить сочетание клавиш",
|
||||
"toggle_show_assistants": "Переключить отображение ассистентов",
|
||||
"toggle_show_topics": "Переключить отображение топиков",
|
||||
"copy_last_message": "Копировать последнее сообщение",
|
||||
"search_message": "Поиск сообщения"
|
||||
"copy_last_message": "Копировать последнее сообщение"
|
||||
},
|
||||
"theme.auto": "Автоматически",
|
||||
"theme.dark": "Темная",
|
||||
@@ -551,7 +526,7 @@
|
||||
"show_window": "Показать окно",
|
||||
"quit": "Выйти"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "База знаний",
|
||||
"search": "Поиск в базе знаний",
|
||||
"empty": "База знаний не найдена",
|
||||
@@ -593,7 +568,6 @@
|
||||
"directory_placeholder": "Введите путь к директории",
|
||||
"model_info": "Модель информации",
|
||||
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
|
||||
"no_provider": "База знаний модель поставщика не настроена, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
|
||||
"source": "Источник"
|
||||
},
|
||||
"models": {
|
||||
@@ -620,8 +594,7 @@
|
||||
"parameter_type": {
|
||||
"string": "Текст",
|
||||
"number": "Число",
|
||||
"boolean": "Логическое",
|
||||
"json": "JSON"
|
||||
"boolean": "Логическое"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"input.upload": "上传图片或文档",
|
||||
"input.web_search": "开启网络搜索",
|
||||
"input.knowledge_base": "知识库",
|
||||
"message.new.branch": "分支",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已创建",
|
||||
"message.regenerate.model": "切换模型",
|
||||
"message.new.context": "清除上下文",
|
||||
@@ -184,14 +184,8 @@
|
||||
"name": "文件名",
|
||||
"open": "打开",
|
||||
"size": "大小",
|
||||
"type": "类型",
|
||||
"text": "文本",
|
||||
"title": "文件",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"delete.title": "删除文件",
|
||||
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
|
||||
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除"
|
||||
"title": "文件"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "继续聊天",
|
||||
@@ -219,10 +213,6 @@
|
||||
"png": "下载 PNG",
|
||||
"svg": "下载 SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "放大",
|
||||
"zoom-out": "缩小"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "预览",
|
||||
"source": "源码"
|
||||
@@ -255,7 +245,7 @@
|
||||
"reset.double.confirm.title": "数据丢失!!!",
|
||||
"restore.success": "恢复成功",
|
||||
"save.success.title": "保存成功",
|
||||
"switch.disabled": "请等待当前回复完成后操作",
|
||||
"switch.disabled": "模型回复完成后才能切换",
|
||||
"topic.added": "话题添加成功",
|
||||
"upgrade.success.button": "重启",
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
@@ -290,9 +280,7 @@
|
||||
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
|
||||
"seed": "随机种子",
|
||||
"seed_tip": "相同的种子和提示词可以生成相似的图片",
|
||||
"title": "图片",
|
||||
"prompt_enhancement": "提示词增强",
|
||||
"prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本"
|
||||
"title": "图片"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -375,12 +363,7 @@
|
||||
"webdav.minutes": "分钟",
|
||||
"webdav.restore.button": "从 WebDAV 恢复",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 用户名",
|
||||
"webdav.syncStatus": "同步状态",
|
||||
"webdav.autoSync.off": "关闭",
|
||||
"webdav.noSync": "等待下次同步",
|
||||
"webdav.syncError": "同步错误",
|
||||
"webdav.lastSync": "上次同步时间"
|
||||
"webdav.user": "WebDAV 用户名"
|
||||
},
|
||||
"display.title": "显示设置",
|
||||
"font_size.title": "消息字体大小",
|
||||
@@ -396,16 +379,9 @@
|
||||
"general.user_name.placeholder": "请输入用户名",
|
||||
"general.view_webdav_settings": "查看 WebDAV 设置",
|
||||
"general.display.title": "显示设置",
|
||||
"display.sidebar.translate.icon": "显示翻译图标",
|
||||
"display.sidebar.painting.icon": "显示绘画图标",
|
||||
"display.sidebar.minapp.icon": "显示小程序图标",
|
||||
"display.sidebar.knowledge.icon": "显示知识图标",
|
||||
"display.sidebar.files.icon": "显示文件图标",
|
||||
"display.sidebar.title": "侧边栏设置",
|
||||
"display.sidebar.visible": "显示我的侧边栏图标",
|
||||
"display.sidebar.disabled": "隐藏我的侧边栏图标",
|
||||
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
|
||||
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
|
||||
"display.topic.title": "话题设置",
|
||||
"display.custom.css": "自定义 CSS",
|
||||
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
|
||||
@@ -498,8 +474,7 @@
|
||||
"clear_shortcut": "清除快捷键",
|
||||
"toggle_show_assistants": "切换助手显示",
|
||||
"toggle_show_topics": "切换话题显示",
|
||||
"copy_last_message": "复制上一条消息",
|
||||
"search_message": "搜索消息"
|
||||
"copy_last_message": "复制上一条消息"
|
||||
},
|
||||
"theme.auto": "跟随系统",
|
||||
"theme.dark": "深色主题",
|
||||
@@ -540,7 +515,7 @@
|
||||
"show_window": "显示窗口",
|
||||
"quit": "退出"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "知识库",
|
||||
"search": "搜索知识库",
|
||||
"empty": "暂无知识库",
|
||||
@@ -582,7 +557,6 @@
|
||||
"directory_placeholder": "请输入目录路径",
|
||||
"model_info": "模型信息",
|
||||
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
|
||||
"no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库",
|
||||
"source": "来源"
|
||||
},
|
||||
"models": {
|
||||
@@ -609,8 +583,7 @@
|
||||
"parameter_type": {
|
||||
"string": "文本",
|
||||
"number": "数字",
|
||||
"boolean": "布尔值",
|
||||
"json": "JSON"
|
||||
"boolean": "布尔值"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
"input.upload": "上傳圖片或文檔",
|
||||
"input.web_search": "開啟網路搜索",
|
||||
"input.knowledge_base": "知識庫",
|
||||
"message.new.branch": "分支",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已建立",
|
||||
"message.regenerate.model": "切換模型",
|
||||
"message.new.context": "新上下文",
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "名稱",
|
||||
"open": "打開",
|
||||
"size": "大小",
|
||||
"type": "類型",
|
||||
"text": "文本",
|
||||
"title": "檔案",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除",
|
||||
"delete.title": "刪除檔案",
|
||||
"delete.content": "刪除檔案會刪除檔案在所有消息中的引用,確定要刪除此檔案嗎?",
|
||||
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除"
|
||||
"title": "檔案"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "繼續聊天",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "下載 PNG",
|
||||
"svg": "下載 SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "放大",
|
||||
"zoom-out": "縮小"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "預覽",
|
||||
"source": "原始碼"
|
||||
@@ -254,7 +244,7 @@
|
||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||
"restore.success": "恢復成功",
|
||||
"save.success.title": "保存成功",
|
||||
"switch.disabled": "請等待當前回覆完成",
|
||||
"switch.disabled": "助手生成回覆時無法切換",
|
||||
"topic.added": "新話題已添加",
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"upgrade.success.content": "請重新啟動應用以完成升級",
|
||||
@@ -289,9 +279,7 @@
|
||||
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
|
||||
"seed": "隨機種子",
|
||||
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
|
||||
"title": "繪圖",
|
||||
"prompt_enhancement": "提示詞增強",
|
||||
"prompt_enhancement_tip": "開啟後將提示重寫為詳細的、適合模型的版本"
|
||||
"title": "繪圖"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -374,12 +362,7 @@
|
||||
"webdav.minutes": "分鐘",
|
||||
"webdav.restore.button": "從 WebDAV 恢復",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 使用者名稱",
|
||||
"webdav.syncStatus": "同步狀態",
|
||||
"webdav.autoSync.off": "關閉",
|
||||
"webdav.noSync": "等待下次同步",
|
||||
"webdav.syncError": "同步錯誤",
|
||||
"webdav.lastSync": "上次同步時間"
|
||||
"webdav.user": "WebDAV 使用者名稱"
|
||||
},
|
||||
"display.title": "顯示設定",
|
||||
"font_size.title": "訊息字體大小",
|
||||
@@ -395,17 +378,10 @@
|
||||
"general.user_name.placeholder": "輸入您的名稱",
|
||||
"general.view_webdav_settings": "查看 WebDAV 設定",
|
||||
"general.display.title": "顯示設定",
|
||||
"display.sidebar.translate.icon": "顯示翻譯圖示",
|
||||
"display.sidebar.painting.icon": "顯示繪圖圖示",
|
||||
"display.sidebar.minapp.icon": "顯示小程序圖示",
|
||||
"display.sidebar.knowledge.icon": "顯示知識圖示",
|
||||
"display.sidebar.files.icon": "顯示文件圖示",
|
||||
"display.sidebar.title": "側邊欄設定",
|
||||
"display.topic.title": "話題設定",
|
||||
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
|
||||
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
|
||||
"display.sidebar.visible": "顯示我的側邊欄圖標",
|
||||
"display.sidebar.disabled": "隱藏我的側邊欄圖標",
|
||||
"display.custom.css": "自定義 CSS",
|
||||
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
|
||||
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
|
||||
@@ -497,8 +473,7 @@
|
||||
"clear_shortcut": "清除快捷鍵",
|
||||
"toggle_show_assistants": "切換助手顯示",
|
||||
"toggle_show_topics": "切換話題顯示",
|
||||
"copy_last_message": "複製上一条消息",
|
||||
"search_message": "搜索消息"
|
||||
"copy_last_message": "複製上一条消息"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "深色主題",
|
||||
@@ -539,7 +514,7 @@
|
||||
"show_window": "顯示視窗",
|
||||
"quit": "退出"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "知識庫",
|
||||
"search": "搜尋知識庫",
|
||||
"empty": "暫無知識庫",
|
||||
@@ -581,7 +556,6 @@
|
||||
"directory_placeholder": "請輸入目錄路徑",
|
||||
"model_info": "模型信息",
|
||||
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
|
||||
"no_provider": "知識庫模型提供商遺失,該知識庫將不再支持,請重新創建知識庫",
|
||||
"source": "來源"
|
||||
},
|
||||
"models": {
|
||||
@@ -608,8 +582,7 @@
|
||||
"parameter_type": {
|
||||
"string": "文字",
|
||||
"number": "數字",
|
||||
"boolean": "布林值",
|
||||
"json": "JSON"
|
||||
"boolean": "布林值"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
|
||||
import { startAutoSync } from './services/BackupService'
|
||||
import store from './store'
|
||||
|
||||
function initKeyv() {
|
||||
function init() {
|
||||
window.keyv = new KeyvStorage()
|
||||
window.keyv.init()
|
||||
}
|
||||
|
||||
function initAutoSync() {
|
||||
const { webdavAutoSync } = store.getState().settings
|
||||
if (webdavAutoSync) {
|
||||
startAutoSync()
|
||||
}
|
||||
}
|
||||
|
||||
initKeyv()
|
||||
initAutoSync()
|
||||
init()
|
||||
|
||||
@@ -283,7 +283,7 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
margin: 0 !important;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
margin-bottom: 5px !important;
|
||||
font-size: 13px;
|
||||
justify-content: left;
|
||||
|
||||
@@ -124,7 +124,6 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
label={t('agents.add.prompt')}
|
||||
rules={[{ required: true }]}
|
||||
style={{ position: 'relative' }}>
|
||||
spellCheck={false}
|
||||
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={10} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
|
||||
@@ -73,15 +73,13 @@ const ContentContainer = styled.div`
|
||||
`
|
||||
|
||||
const AppsContainer = styled.div`
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
min-width: 930px;
|
||||
max-width: 930px;
|
||||
width: 100%;
|
||||
max-height: 520px;
|
||||
min-height: 520px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
|
||||
gap: 25px;
|
||||
justify-content: center;
|
||||
max-height: 500px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(90px, 1fr));
|
||||
gap: 25px 25px;
|
||||
`
|
||||
|
||||
export default AppsPage
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
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)
|
||||
@@ -1,36 +1,20 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
FileImageOutlined,
|
||||
FilePdfOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { FileImageOutlined, FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import db from '@renderer/databases'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import store from '@renderer/store'
|
||||
import { FileType, FileTypes } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Button, Dropdown, Menu } from 'antd'
|
||||
import { Col, Image, Menu, Row, Spin, Table } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ContentView from './ContentView'
|
||||
|
||||
const FilesPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [fileType, setFileType] = useState<FileTypes | 'all' | 'gemini'>('all')
|
||||
const { providers } = useProviders()
|
||||
|
||||
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
|
||||
const [fileType, setFileType] = useState<FileTypes | 'all'>('all')
|
||||
|
||||
const files = useLiveQuery<FileType[]>(() => {
|
||||
if (fileType === 'all') {
|
||||
@@ -39,146 +23,56 @@ const FilesPage: FC = () => {
|
||||
return db.files.where('type').equals(fileType).sortBy('count')
|
||||
}, [fileType])
|
||||
|
||||
const handleDelete = async (fileId: string) => {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
|
||||
const paintings = await store.getState().paintings.paintings
|
||||
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||
|
||||
if (paintingsFiles.some((p) => p.id === fileId)) {
|
||||
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
|
||||
return
|
||||
}
|
||||
|
||||
if (file) {
|
||||
await FileManager.deleteFile(fileId, true)
|
||||
}
|
||||
|
||||
const topics = await db.topics
|
||||
.filter((topic) => topic.messages.some((message) => message.files?.some((f) => f.id === fileId)))
|
||||
.toArray()
|
||||
|
||||
if (topics.length > 0) {
|
||||
for (const topic of topics) {
|
||||
const updatedMessages = topic.messages.map((message) => ({
|
||||
...message,
|
||||
files: message.files?.filter((f) => f.id !== fileId)
|
||||
}))
|
||||
await db.topics.update(topic.id, { messages: updatedMessages })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRename = async (fileId: string) => {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
if (file) {
|
||||
const newName = await TextEditPopup.show({ text: file.origin_name })
|
||||
if (newName) {
|
||||
FileManager.updateFile({ ...file, origin_name: newName })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getActionMenu = (fileId: string): MenuProps['items'] => [
|
||||
{
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
label: t('files.edit'),
|
||||
onClick: () => handleRename(fileId)
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
label: t('files.delete'),
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('files.delete.title'),
|
||||
content: t('files.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => handleDelete(fileId)
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const dataSource = files?.map((file) => {
|
||||
return {
|
||||
key: file.id,
|
||||
file: (
|
||||
<FileNameText className="text-nowrap" onClick={() => window.api.file.openPath(file.path)}>
|
||||
{file.origin_name}
|
||||
</FileNameText>
|
||||
),
|
||||
file: <FileNameText className="text-nowrap">{file.origin_name}</FileNameText>,
|
||||
size: formatFileSize(file),
|
||||
size_bytes: file.size,
|
||||
count: file.count,
|
||||
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
|
||||
created_at_unix: dayjs(file.created_at).unix(),
|
||||
actions: (
|
||||
<Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']} placement="bottom" arrow>
|
||||
<Button type="text" size="small" icon={<EllipsisOutlined />} />
|
||||
</Dropdown>
|
||||
)
|
||||
actions: <a href={'file://' + FileManager.getSafePath(file)}>{t('files.open')}</a>
|
||||
}
|
||||
})
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('files.name'),
|
||||
dataIndex: 'file',
|
||||
key: 'file',
|
||||
width: '300px'
|
||||
},
|
||||
{
|
||||
title: t('files.size'),
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: '80px',
|
||||
sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: t('files.count'),
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
width: '60px',
|
||||
sorter: (a: { count: number }, b: { count: number }) => b.count - a.count,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: t('files.created_at'),
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: '120px',
|
||||
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',
|
||||
width: '80px',
|
||||
align: 'center'
|
||||
}
|
||||
],
|
||||
[t]
|
||||
)
|
||||
const columns = [
|
||||
{
|
||||
title: t('files.name'),
|
||||
dataIndex: 'file',
|
||||
key: 'file',
|
||||
width: '300px'
|
||||
},
|
||||
{
|
||||
title: t('files.size'),
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: '80px'
|
||||
},
|
||||
{
|
||||
title: t('files.count'),
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
width: '60px'
|
||||
},
|
||||
{
|
||||
title: t('files.created_at'),
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: '120px'
|
||||
},
|
||||
{
|
||||
title: t('files.actions'),
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
width: '50px'
|
||||
}
|
||||
]
|
||||
|
||||
const menuItems = [
|
||||
{ key: 'all', label: t('files.all'), icon: <FileTextOutlined /> },
|
||||
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> },
|
||||
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> },
|
||||
{ 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']
|
||||
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> }
|
||||
]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -190,7 +84,41 @@ const FilesPage: FC = () => {
|
||||
<Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} />
|
||||
</SideNav>
|
||||
<TableContainer right>
|
||||
<ContentView id={fileType} files={files} dataSource={dataSource} columns={columns} />
|
||||
{fileType === FileTypes.IMAGE && files?.length && files?.length > 0 ? (
|
||||
<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}
|
||||
style={{ width: '100%' }}
|
||||
size="small"
|
||||
pagination={{ pageSize: 100 }}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
@@ -221,7 +149,72 @@ const TableContainer = styled(Scrollbar)`
|
||||
const FileNameText = styled.div`
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
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`
|
||||
@@ -240,7 +233,7 @@ const SideNav = styled.div`
|
||||
line-height: 36px;
|
||||
margin: 4px 0;
|
||||
width: 100%;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
border: 0.5px solid transparent;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
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
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Assistant } from '@renderer/types'
|
||||
@@ -22,7 +22,7 @@ const HomePage: FC = () => {
|
||||
|
||||
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
|
||||
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
|
||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||
const { showAssistants } = useShowAssistants()
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
@@ -35,17 +35,8 @@ const HomePage: FC = () => {
|
||||
state?.topic && setActiveTopic(state?.topic)
|
||||
}, [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 (
|
||||
<Container id="home-page">
|
||||
<Container>
|
||||
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||
<ContentContainer id="content-container">
|
||||
{showAssistants && (
|
||||
|
||||
@@ -17,18 +17,16 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ContentContainer>
|
||||
<Upload
|
||||
listType={files.length > 20 ? 'text' : 'picture-card'}
|
||||
fileList={files.map((file) => ({
|
||||
uid: file.id,
|
||||
url: 'file://' + FileManager.getSafePath(file),
|
||||
status: 'done',
|
||||
name: file.name
|
||||
}))}
|
||||
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
|
||||
/>
|
||||
</ContentContainer>
|
||||
<Upload
|
||||
listType="picture-card"
|
||||
fileList={files.map((file) => ({
|
||||
uid: file.id,
|
||||
url: 'file://' + FileManager.getSafePath(file),
|
||||
status: 'done',
|
||||
name: file.name
|
||||
}))}
|
||||
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -37,16 +35,9 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
padding: 10px 20px;
|
||||
background: var(--color-background);
|
||||
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
|
||||
|
||||
@@ -13,7 +13,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
@@ -25,7 +25,7 @@ import { translateText } from '@renderer/services/TranslateService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||
import { Assistant, FileType, KnowledgeBase, Message, Topic } from '@renderer/types'
|
||||
import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
|
||||
import { delay, getFileExtension, uuid } from '@renderer/utils'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
@@ -35,7 +35,6 @@ import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState }
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import NarrowLayout from '../Messages/NarrowLayout'
|
||||
import AttachmentButton from './AttachmentButton'
|
||||
import AttachmentPreview from './AttachmentPreview'
|
||||
import KnowledgeBaseButton from './KnowledgeBaseButton'
|
||||
@@ -63,8 +62,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
showInputEstimatedTokens,
|
||||
clickAssistantToShowTopic,
|
||||
language,
|
||||
autoTranslateWithSpace,
|
||||
sidebarIcons
|
||||
autoTranslateWithSpace
|
||||
} = useSettings()
|
||||
const [expended, setExpend] = useState(false)
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
@@ -86,8 +84,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||
|
||||
const showKnowledgeIcon = sidebarIcons.visible.includes('knowledge')
|
||||
|
||||
const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), [])
|
||||
const inputTokenCount = useMemo(
|
||||
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
|
||||
@@ -101,7 +97,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
_base = selectedKnowledgeBase
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
if (generating) {
|
||||
return
|
||||
}
|
||||
|
||||
if (inputEmpty) {
|
||||
return
|
||||
@@ -134,7 +132,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
|
||||
setExpend(false)
|
||||
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
|
||||
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
@@ -209,7 +207,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}
|
||||
|
||||
const addNewTopic = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||
return
|
||||
}
|
||||
|
||||
const topic = getDefaultTopic(assistant.id)
|
||||
|
||||
@@ -225,7 +226,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
setActiveTopic(topic)
|
||||
|
||||
clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||
}, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel])
|
||||
}, [addTopic, assistant, clickAssistantToShowTopic, generating, setActiveTopic, setModel, t])
|
||||
|
||||
const clearTopic = async () => {
|
||||
if (generating) {
|
||||
@@ -387,117 +388,115 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
|
||||
<NarrowLayout style={{ width: '100%' }}>
|
||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||
<InputBarContainer
|
||||
id="inputbar"
|
||||
className={classNames('inputbar-container', inputFocus && 'focus')}
|
||||
ref={containerRef}>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
spellCheck={false}
|
||||
rows={textareaRows}
|
||||
ref={textareaRef}
|
||||
style={{ fontSize }}
|
||||
styles={{ textarea: TextareaStyle }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
onInput={onInput}
|
||||
disabled={searching}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onClick={() => searching && dispatch(setSearching(false))}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||
<FormOutlined />
|
||||
</ToolbarButton>
|
||||
</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>
|
||||
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||
<InputBarContainer id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
rows={textareaRows}
|
||||
ref={textareaRef}
|
||||
style={{ fontSize }}
|
||||
styles={{ textarea: TextareaStyle }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
onInput={onInput}
|
||||
disabled={searching}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onClick={() => searching && dispatch(setSearching(false))}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic', { Command: newTopicShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||
<FormOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{isWebSearchModel(model) && (
|
||||
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
||||
<ToolbarButton
|
||||
type="text"
|
||||
onClick={() => {
|
||||
!showTopics && toggleShowTopics()
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
|
||||
}}>
|
||||
<ControlOutlined />
|
||||
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
|
||||
<GlobalOutlined
|
||||
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
|
||||
/>
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{showKnowledgeIcon && (
|
||||
<KnowledgeBaseButton
|
||||
selectedBase={selectedKnowledgeBase}
|
||||
onSelect={handleKnowledgeBaseSelect}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={files.length > 0}
|
||||
/>
|
||||
)}
|
||||
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||
<PicCenterOutlined />
|
||||
</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
|
||||
type="text"
|
||||
onClick={() => {
|
||||
!showTopics && toggleShowTopics()
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
|
||||
}}>
|
||||
<ControlOutlined />
|
||||
</ToolbarButton>
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
</Tooltip>
|
||||
<KnowledgeBaseButton
|
||||
selectedBase={selectedKnowledgeBase}
|
||||
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>
|
||||
</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>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputBarContainer>
|
||||
</NarrowLayout>
|
||||
)}
|
||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputBarContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, Tool
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
|
||||
overlayStyle={{ maxWidth: 400 }}
|
||||
trigger="click">
|
||||
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
|
||||
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
||||
|
||||
@@ -66,9 +66,6 @@ const Container = styled.div`
|
||||
font-size: 10px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Text = styled.div`
|
||||
|
||||
@@ -18,7 +18,6 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
const { t } = useTranslation()
|
||||
const mermaidId = `mermaid-popup-${Date.now()}`
|
||||
const [activeTab, setActiveTab] = useState('preview')
|
||||
const [scale, setScale] = useState(1)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
@@ -32,25 +31,6 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
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') => {
|
||||
try {
|
||||
const element = document.getElementById(mermaidId)
|
||||
@@ -130,8 +110,6 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
{activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>}
|
||||
{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('png')}>{t('mermaid.download.png')}</Button>
|
||||
</>
|
||||
|
||||
@@ -27,7 +27,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
|
||||
const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
const avatar = useAvatar()
|
||||
const { theme } = useTheme()
|
||||
const { userName, sidebarIcons } = useSettings()
|
||||
const { userName } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
|
||||
@@ -40,14 +40,11 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
}, [message.modelId, message.role, model?.id, model?.name, t, userName])
|
||||
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const showMinappIcon = sidebarIcons.visible.includes('minapp')
|
||||
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||
|
||||
const showMiniApp = useCallback(() => {
|
||||
showMinappIcon && model?.provider && startMinAppById(model.provider)
|
||||
}, [model?.provider, showMinappIcon])
|
||||
const showMiniApp = useCallback(() => model?.provider && startMinAppById(model.provider), [model?.provider])
|
||||
|
||||
const avatarStyle: CSSProperties | undefined = isBubbleStyle
|
||||
? {
|
||||
@@ -57,7 +54,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Container className="message-header">
|
||||
<Container>
|
||||
<AvatarWrapper style={avatarStyle}>
|
||||
{isAssistantMessage ? (
|
||||
<Avatar
|
||||
@@ -65,7 +62,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
size={35}
|
||||
style={{
|
||||
borderRadius: '20%',
|
||||
cursor: showMinappIcon ? 'pointer' : 'default',
|
||||
cursor: 'pointer',
|
||||
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
|
||||
filter: theme === 'dark' ? 'invert(0.05)' : undefined
|
||||
}}
|
||||
|
||||
@@ -11,11 +11,10 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils'
|
||||
import { removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
@@ -70,8 +69,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
[setModel]
|
||||
)
|
||||
|
||||
const onNewBranch = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
const onNewBranch = useCallback(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||
window.message.success({
|
||||
content: t('chat.message.new.branch.created'),
|
||||
@@ -79,8 +77,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
})
|
||||
}, [index, t])
|
||||
|
||||
const onResend = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
const onResend = useCallback(() => {
|
||||
const _messages = onGetMessages?.() || []
|
||||
const index = _messages.findIndex((m) => m.id === message.id)
|
||||
const nextIndex = index + 1
|
||||
@@ -95,12 +92,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
translatedContent: undefined
|
||||
})
|
||||
}
|
||||
|
||||
if (!nextMessage) {
|
||||
onDeleteMessage?.(message)
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { ...message, id: uuid() })
|
||||
}
|
||||
}, [assistantModel?.id, message, model?.id, onDeleteMessage, onGetMessages])
|
||||
}, [assistantModel?.id, message.id, model?.id, onGetMessages])
|
||||
|
||||
const onEdit = useCallback(async () => {
|
||||
let resendMessage = false
|
||||
@@ -166,23 +158,57 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
onClick: onEdit
|
||||
},
|
||||
{
|
||||
label: t('chat.message.new.branch'),
|
||||
key: 'new-branch',
|
||||
icon: <ForkOutlined />,
|
||||
onClick: onNewBranch
|
||||
label: t('chat.translate'),
|
||||
key: 'translate',
|
||||
icon: isTranslating ? <SyncOutlined spin /> : <TranslationOutlined />,
|
||||
children: [
|
||||
{
|
||||
label: '🇨🇳 ' + t('languages.chinese'),
|
||||
key: 'translate-chinese',
|
||||
onClick: () => handleTranslate('chinese')
|
||||
},
|
||||
{
|
||||
label: '🇭🇰 ' + t('languages.chinese-traditional'),
|
||||
key: 'translate-chinese-traditional',
|
||||
onClick: () => handleTranslate('chinese-traditional')
|
||||
},
|
||||
{
|
||||
label: '🇬🇧 ' + t('languages.english'),
|
||||
key: 'translate-english',
|
||||
onClick: () => handleTranslate('english')
|
||||
},
|
||||
{
|
||||
label: '🇯🇵 ' + t('languages.japanese'),
|
||||
key: 'translate-japanese',
|
||||
onClick: () => handleTranslate('japanese')
|
||||
},
|
||||
{
|
||||
label: '🇰🇷 ' + t('languages.korean'),
|
||||
key: 'translate-korean',
|
||||
onClick: () => handleTranslate('korean')
|
||||
},
|
||||
{
|
||||
label: '🇷🇺 ' + t('languages.russian'),
|
||||
key: 'translate-russian',
|
||||
onClick: () => handleTranslate('russian')
|
||||
},
|
||||
{
|
||||
label: '✖ ' + t('translate.close'),
|
||||
key: 'translate-close',
|
||||
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[message, onEdit, onNewBranch, t]
|
||||
[handleTranslate, isTranslating, message, onEdit, onEditMessage, t]
|
||||
)
|
||||
|
||||
const onAtModelRegenerate = async () => {
|
||||
await modelGenerating()
|
||||
const selectedModel = await SelectModelPopup.show({ model })
|
||||
selectedModel && onRegenerate(selectedModel)
|
||||
}
|
||||
|
||||
const onDeleteAndRegenerate = async () => {
|
||||
await modelGenerating()
|
||||
const onDeleteAndRegenerate = () => {
|
||||
onEditMessage?.({
|
||||
...message,
|
||||
content: '',
|
||||
@@ -195,7 +221,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
return (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
@@ -224,60 +250,16 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
|
||||
<i className="iconfont icon-at"></i>
|
||||
<i className="iconfont icon-at1"></i>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isUserMessage && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: '🇨🇳 ' + t('languages.chinese'),
|
||||
key: 'translate-chinese',
|
||||
onClick: () => handleTranslate('chinese')
|
||||
},
|
||||
{
|
||||
label: '🇭🇰 ' + t('languages.chinese-traditional'),
|
||||
key: 'translate-chinese-traditional',
|
||||
onClick: () => handleTranslate('chinese-traditional')
|
||||
},
|
||||
{
|
||||
label: '🇬🇧 ' + t('languages.english'),
|
||||
key: 'translate-english',
|
||||
onClick: () => handleTranslate('english')
|
||||
},
|
||||
{
|
||||
label: '🇯🇵 ' + t('languages.japanese'),
|
||||
key: 'translate-japanese',
|
||||
onClick: () => handleTranslate('japanese')
|
||||
},
|
||||
{
|
||||
label: '🇰🇷 ' + t('languages.korean'),
|
||||
key: 'translate-korean',
|
||||
onClick: () => handleTranslate('korean')
|
||||
},
|
||||
{
|
||||
label: '🇷🇺 ' + t('languages.russian'),
|
||||
key: 'translate-russian',
|
||||
onClick: () => handleTranslate('russian')
|
||||
},
|
||||
{
|
||||
label: '✖ ' + t('translate.close'),
|
||||
key: 'translate-close',
|
||||
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="topRight"
|
||||
arrow>
|
||||
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
|
||||
<ActionButton className="message-action-button">
|
||||
<TranslationOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onNewBranch}>
|
||||
<ForkOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
@@ -335,7 +317,7 @@ const ActionButton = styled.div`
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.icon-at {
|
||||
.icon-at1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -26,7 +26,6 @@ import styled from 'styled-components'
|
||||
|
||||
import Suggestions from '../components/Suggestions'
|
||||
import MessageItem from './Message'
|
||||
import NarrowLayout from './NarrowLayout'
|
||||
import Prompt from './Prompt'
|
||||
|
||||
interface Props {
|
||||
@@ -137,7 +136,6 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
(message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
setDisplayMessages(_messages)
|
||||
db.topics.update(topic.id, { messages: _messages })
|
||||
deleteMessageFiles(message)
|
||||
},
|
||||
@@ -284,35 +282,33 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
key={assistant.id}
|
||||
ref={containerRef}
|
||||
right={topicPosition === 'left'}>
|
||||
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
|
||||
<Suggestions assistant={assistant} messages={messages} />
|
||||
<InfiniteScroll
|
||||
dataLength={displayMessages.length}
|
||||
next={loadMoreMessages}
|
||||
hasMore={hasMore}
|
||||
loader={null}
|
||||
inverse={true}
|
||||
scrollableTarget="messages">
|
||||
<ScrollContainer>
|
||||
<LoaderContainer $loading={isLoadingMore}>
|
||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||
</LoaderContainer>
|
||||
{displayMessages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={index}
|
||||
hidePresetMessages={assistant.settings?.hideMessages}
|
||||
onSetMessages={setMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
</InfiniteScroll>
|
||||
<Prompt assistant={assistant} key={assistant.prompt} />
|
||||
</NarrowLayout>
|
||||
<Suggestions assistant={assistant} messages={messages} />
|
||||
<InfiniteScroll
|
||||
dataLength={displayMessages.length}
|
||||
next={loadMoreMessages}
|
||||
hasMore={hasMore}
|
||||
loader={null}
|
||||
inverse={true}
|
||||
scrollableTarget="messages">
|
||||
<ScrollContainer>
|
||||
<LoaderContainer $loading={isLoadingMore}>
|
||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||
</LoaderContainer>
|
||||
{displayMessages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={index}
|
||||
hidePresetMessages={assistant.settings?.hideMessages}
|
||||
onSetMessages={setMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
</InfiniteScroll>
|
||||
<Prompt assistant={assistant} key={assistant.prompt} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
@@ -28,7 +28,7 @@ const Container = styled.div`
|
||||
padding: 10px 20px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-bottom: 20px;
|
||||
margin: 4px 20px 0 20px;
|
||||
margin: 0 20px 0 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border: 0.5px solid var(--color-border);
|
||||
|
||||
@@ -10,8 +10,6 @@ import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
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 { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@@ -27,9 +25,8 @@ interface Props {
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
|
||||
const { topicPosition } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useShortcut('toggle_show_assistants', () => {
|
||||
toggleShowAssistants()
|
||||
@@ -43,15 +40,11 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('search_message', () => {
|
||||
SearchPopup.show()
|
||||
})
|
||||
|
||||
return (
|
||||
<Navbar className="home-navbar">
|
||||
<Navbar>
|
||||
{showAssistants && (
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
|
||||
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 16 : 0 }}>
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
|
||||
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
|
||||
<i className="iconfont icon-hide-sidebar" />
|
||||
</NavbarIcon>
|
||||
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||
@@ -59,12 +52,12 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</NavbarIcon>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarRight
|
||||
style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}
|
||||
className="home-navbar-right">
|
||||
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
|
||||
<HStack alignItems="center">
|
||||
{!showAssistants && (
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8, marginLeft: isMac ? 4 : -12 }}>
|
||||
<NavbarIcon
|
||||
onClick={() => toggleShowAssistants()}
|
||||
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
|
||||
<i className="iconfont icon-show-sidebar" />
|
||||
</NavbarIcon>
|
||||
)}
|
||||
@@ -76,24 +69,19 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</TitleText>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<HStack alignItems="center">
|
||||
<NavbarIcon onClick={() => SearchPopup.show()}>
|
||||
<SearchOutlined />
|
||||
</NarrowIcon>
|
||||
<NarrowIcon onClick={() => dispatch(setNarrowMode(!narrowMode))}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NarrowIcon>
|
||||
{sidebarIcons.visible.includes('minapp') && (
|
||||
<AppStorePopover>
|
||||
<NarrowIcon>
|
||||
<i className="iconfont icon-appstore" />
|
||||
</NarrowIcon>
|
||||
</AppStorePopover>
|
||||
)}
|
||||
</NavbarIcon>
|
||||
<AppStorePopover>
|
||||
<NavbarIcon style={{ marginLeft: isMac ? 5 : 10 }}>
|
||||
<i className="iconfont icon-appstore" />
|
||||
</NavbarIcon>
|
||||
</AppStorePopover>
|
||||
{topicPosition === 'right' && (
|
||||
<NarrowIcon onClick={toggleShowTopics}>
|
||||
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}>
|
||||
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
|
||||
</NarrowIcon>
|
||||
</NavbarIcon>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarRight>
|
||||
@@ -138,17 +126,8 @@ export const NavbarIcon = styled.div`
|
||||
const TitleText = styled.span`
|
||||
margin-left: 5px;
|
||||
font-family: Ubuntu;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
@media (max-width: 1080px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@@ -4,11 +4,11 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Dropdown } from 'antd'
|
||||
@@ -32,6 +32,7 @@ const Assistants: FC<Props> = ({
|
||||
onCreateDefaultAssistant
|
||||
}) => {
|
||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const { removeAllTopics } = useAssistant(activeAssistant.id)
|
||||
const { clickAssistantToShowTopic, topicPosition } = useSettings()
|
||||
@@ -40,7 +41,7 @@ const Assistants: FC<Props> = ({
|
||||
|
||||
const onDelete = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
const _assistant: Assistant | undefined = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
_assistant ? setActiveAssistant(_assistant) : onCreateDefaultAssistant()
|
||||
removeAssistant(assistant.id)
|
||||
},
|
||||
@@ -116,8 +117,13 @@ const Assistants: FC<Props> = ({
|
||||
)
|
||||
|
||||
const onSwitchAssistant = useCallback(
|
||||
async (assistant: Assistant) => {
|
||||
await modelGenerating()
|
||||
(assistant: Assistant): any => {
|
||||
if (generating) {
|
||||
return window.message.warning({
|
||||
content: t('message.switch.disabled'),
|
||||
key: 'switch-assistant'
|
||||
})
|
||||
}
|
||||
|
||||
if (topicPosition === 'left' && clickAssistantToShowTopic) {
|
||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||
@@ -125,11 +131,11 @@ const Assistants: FC<Props> = ({
|
||||
|
||||
setActiveAssistant(assistant)
|
||||
},
|
||||
[clickAssistantToShowTopic, setActiveAssistant, topicPosition]
|
||||
[clickAssistantToShowTopic, generating, setActiveAssistant, t, topicPosition]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab">
|
||||
<Container>
|
||||
<DragableList
|
||||
list={assistants}
|
||||
onUpdate={updateAssistants}
|
||||
@@ -181,7 +187,7 @@ const AssistantItem = styled.div`
|
||||
margin: 0 10px;
|
||||
padding-right: 35px;
|
||||
font-family: Ubuntu;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
border: 0.5px solid transparent;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, DeleteOutlined, PlusOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import {
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
setShowMessageDivider
|
||||
} from '@renderer/store/settings'
|
||||
import { Assistant, AssistantSettings, ThemeMode } from '@renderer/types'
|
||||
import { Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { Button, Col, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -117,7 +117,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
}, [assistant])
|
||||
|
||||
return (
|
||||
<Container className="settings-tab">
|
||||
<Container>
|
||||
<SettingGroup style={{ marginTop: 10 }}>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>
|
||||
{t('settings.messages.model.title')}{' '}
|
||||
@@ -203,6 +203,106 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{assistant?.settings?.customParameters?.map((param, index) => (
|
||||
<ParameterCard key={index}>
|
||||
<Row align="middle" gutter={8} style={{ marginBottom: 8 }}>
|
||||
<Col span={14}>
|
||||
<Input
|
||||
placeholder={t('models.parameter_name')}
|
||||
value={param.name}
|
||||
onChange={(e) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, name: e.target.value }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Select
|
||||
value={param.type}
|
||||
onChange={(value: 'string' | 'number' | 'boolean') => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
let defaultValue: any = ''
|
||||
switch (value) {
|
||||
case 'number':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
default:
|
||||
defaultValue = ''
|
||||
}
|
||||
newParams[index] = { ...param, type: value, value: defaultValue }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
style={{ width: '100%' }}>
|
||||
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
|
||||
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
|
||||
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={20}>
|
||||
{param.type === 'boolean' ? (
|
||||
<Switch
|
||||
checked={param.value as boolean}
|
||||
onChange={(checked) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: checked }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
) : param.type === 'number' ? (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={param.value as number}
|
||||
onChange={(value) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: value || 0 }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
step={0.01}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={typeof param.value === 'string' ? param.value : JSON.stringify(param.value)}
|
||||
onChange={(e) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: e.target.value }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams.splice(index, 1)
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
danger
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ParameterCard>
|
||||
))}
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
const newParams = [
|
||||
...(assistant?.settings?.customParameters || []),
|
||||
{ name: '', value: '', type: 'string' as const }
|
||||
]
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
style={{ marginBottom: 0, width: '100%', borderStyle: 'dashed' }}>
|
||||
{t('models.add_parameter')}
|
||||
</Button>
|
||||
</SettingGroup>
|
||||
<SettingGroup>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
|
||||
@@ -391,7 +491,6 @@ const Container = styled(Scrollbar)`
|
||||
padding: 0 10px;
|
||||
padding-right: 5px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 10px;
|
||||
`
|
||||
|
||||
const Label = styled.p`
|
||||
@@ -411,11 +510,24 @@ const SettingRowTitleSmall = styled(SettingRowTitle)`
|
||||
`
|
||||
|
||||
export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
|
||||
padding: 0 5px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
background: var(--color-group-background);
|
||||
`
|
||||
|
||||
const ParameterCard = styled.div`
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-background);
|
||||
&:last-child {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
export default SettingsTab
|
||||
|
||||
@@ -10,12 +10,11 @@ import DragableList from '@renderer/components/DragableList'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import store from '@renderer/store'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export'
|
||||
@@ -36,36 +35,46 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
const { assistants } = useAssistants()
|
||||
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
|
||||
const { t } = useTranslation()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const { showTopicTime, topicPosition } = useSettings()
|
||||
|
||||
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
|
||||
const borderRadius = showTopicTime ? 12 : 17
|
||||
|
||||
const onDeleteTopic = useCallback(
|
||||
async (topic: Topic) => {
|
||||
await modelGenerating()
|
||||
(topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||
return
|
||||
}
|
||||
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
||||
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
|
||||
removeTopic(topic)
|
||||
},
|
||||
[assistant.topics, removeTopic, setActiveTopic]
|
||||
[assistant.topics, generating, removeTopic, setActiveTopic, t]
|
||||
)
|
||||
|
||||
const onMoveTopic = useCallback(
|
||||
async (topic: Topic, toAssistant: Assistant) => {
|
||||
await modelGenerating()
|
||||
(topic: Topic, toAssistant: Assistant) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||
return
|
||||
}
|
||||
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
||||
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
|
||||
moveTopic(topic, toAssistant)
|
||||
},
|
||||
[assistant.topics, moveTopic, setActiveTopic]
|
||||
[assistant.topics, generating, moveTopic, setActiveTopic, t]
|
||||
)
|
||||
|
||||
const onSwitchTopic = useCallback(
|
||||
async (topic: Topic) => {
|
||||
await modelGenerating()
|
||||
(topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
},
|
||||
[setActiveTopic]
|
||||
[generating, setActiveTopic, t]
|
||||
)
|
||||
|
||||
const onClearMessages = useCallback(() => {
|
||||
@@ -177,7 +186,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
)
|
||||
|
||||
return (
|
||||
<Container right={topicPosition === 'right'} className="topics-tab">
|
||||
<Container right={topicPosition === 'right'}>
|
||||
<DragableList list={assistant.topics} onUpdate={updateTopics}>
|
||||
{(topic) => {
|
||||
const isActive = topic.id === activeTopic?.id
|
||||
@@ -185,8 +194,8 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
|
||||
<TopicListItem
|
||||
className={isActive ? 'active' : ''}
|
||||
onClick={() => onSwitchTopic(topic)}
|
||||
style={{ borderRadius }}>
|
||||
style={{ borderRadius }}
|
||||
onClick={() => onSwitchTopic(topic)}>
|
||||
<TopicName className="name">{topic.name.replace('`', '')}</TopicName>
|
||||
{showTopicTime && (
|
||||
<TopicTime className="time">{dayjs(topic.createdAt).format('MM/DD HH:mm')}</TopicTime>
|
||||
@@ -223,9 +232,8 @@ const Container = styled(Scrollbar)`
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 7px 12px;
|
||||
margin-left: 10px;
|
||||
margin-right: 4px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
margin: 0 10px;
|
||||
border-radius: 16px;
|
||||
font-family: Ubuntu;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
|
||||
@@ -94,7 +94,7 @@ const HomeTabs: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant,
|
||||
}, [position, tab, topicPosition])
|
||||
|
||||
return (
|
||||
<Container style={border} className="home-tabs">
|
||||
<Container style={border}>
|
||||
{showTab && (
|
||||
<Segmented
|
||||
value={tab}
|
||||
@@ -125,7 +125,7 @@ const HomeTabs: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant,
|
||||
block
|
||||
/>
|
||||
)}
|
||||
<TabContent className="home-tabs-content">
|
||||
<TabContent>
|
||||
{tab === 'assistants' && (
|
||||
<Assistants
|
||||
activeAssistant={activeAssistant}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import ModelTags from '@renderer/components/ModelTags'
|
||||
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import { isVisionModel } from '@renderer/config/models'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { FC } from 'react'
|
||||
@@ -30,16 +31,15 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const providerName = getProviderName(model?.provider)
|
||||
|
||||
return (
|
||||
<DropdownButton size="small" type="default" onClick={onSelectModel}>
|
||||
<ButtonContent>
|
||||
<ModelAvatar model={model} size={20} />
|
||||
<ModelName>
|
||||
{model ? model.name : t('button.select_model')} {providerName ? '| ' + providerName : ''}
|
||||
{model ? model.name : t('button.select_model')} |{' '}
|
||||
{t(`provider.${model?.provider}`, { defaultValue: getProviderByModel(model)?.name })}
|
||||
</ModelName>
|
||||
<ModelTags model={model} showFree={false} />
|
||||
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
|
||||
</ButtonContent>
|
||||
</DropdownButton>
|
||||
)
|
||||
@@ -62,6 +62,7 @@ const ButtonContent = styled.div`
|
||||
`
|
||||
|
||||
const ModelName = styled.span`
|
||||
margin-left: -2px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { FileType, FileTypes, KnowledgeBase } from '@renderer/types'
|
||||
import { Alert, Button, Card, Divider, message, Tag, Typography, Upload } from 'antd'
|
||||
import { FC } from 'react'
|
||||
@@ -30,7 +29,7 @@ interface KnowledgeContentProps {
|
||||
selectedBase: KnowledgeBase
|
||||
}
|
||||
|
||||
const fileTypes = ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md']
|
||||
const fileTypes = ['.pdf', '.docx', '.pptx', '.xlsx', '.txt', '.md', '.mdx']
|
||||
|
||||
const FlexColumn = styled.div`
|
||||
display: flex;
|
||||
@@ -75,17 +74,11 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
addDirectory
|
||||
} = useKnowledge(selectedBase.id || '')
|
||||
|
||||
const providerName = getProviderName(base?.model.provider || '')
|
||||
const disabled = !base?.version || !providerName
|
||||
|
||||
if (!base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleAddFile = () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.multiple = true
|
||||
@@ -98,10 +91,6 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
}
|
||||
|
||||
const handleDrop = async (files: File[]) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
if (files) {
|
||||
const _files: FileType[] = files.map((file) => ({
|
||||
id: file.name,
|
||||
@@ -121,14 +110,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
}
|
||||
|
||||
const handleAddUrl = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = await PromptPopup.show({
|
||||
title: t('knowledge.add_url'),
|
||||
title: t('knowledge_base.add_url'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.url_placeholder'),
|
||||
inputPlaceholder: t('knowledge_base.url_placeholder'),
|
||||
inputProps: {
|
||||
maxLength: 1000,
|
||||
rows: 1
|
||||
@@ -139,7 +124,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
try {
|
||||
new URL(url)
|
||||
if (urlItems.find((item) => item.content === url)) {
|
||||
message.success(t('knowledge.url_added'))
|
||||
message.success(t('knowledge_base.url_added'))
|
||||
return
|
||||
}
|
||||
addUrl(url)
|
||||
@@ -150,14 +135,10 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
}
|
||||
|
||||
const handleAddSitemap = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = await PromptPopup.show({
|
||||
title: t('knowledge.add_sitemap'),
|
||||
title: t('knowledge_base.add_sitemap'),
|
||||
message: '',
|
||||
inputPlaceholder: t('knowledge.sitemap_placeholder'),
|
||||
inputPlaceholder: t('knowledge_base.sitemap_placeholder'),
|
||||
inputProps: {
|
||||
maxLength: 1000,
|
||||
rows: 1
|
||||
@@ -168,7 +149,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
try {
|
||||
new URL(url)
|
||||
if (sitemapItems.find((item) => item.content === url)) {
|
||||
message.success(t('knowledge.sitemap_added'))
|
||||
message.success(t('knowledge_base.sitemap_added'))
|
||||
return
|
||||
}
|
||||
addSitemap(url)
|
||||
@@ -179,28 +160,16 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
}
|
||||
|
||||
const handleAddNote = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const note = await TextEditPopup.show({ text: '', textareaProps: { rows: 20 } })
|
||||
note && addNote(note)
|
||||
}
|
||||
|
||||
const handleEditNote = async (note: any) => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const editedText = await TextEditPopup.show({ text: note.content as string, textareaProps: { rows: 20 } })
|
||||
editedText && updateNoteContent(note.id, editedText)
|
||||
}
|
||||
|
||||
const handleAddDirectory = async () => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = await window.api.file.selectFolder()
|
||||
console.log('[KnowledgeContent] Selected directory:', path)
|
||||
path && addDirectory(path)
|
||||
@@ -209,16 +178,13 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
return (
|
||||
<MainContent>
|
||||
{!base?.version && (
|
||||
<Alert message={t('knowledge.not_support')} type="error" style={{ marginBottom: 20 }} showIcon />
|
||||
)}
|
||||
{!providerName && (
|
||||
<Alert message={t('knowledge.no_provider')} type="error" style={{ marginBottom: 20 }} showIcon />
|
||||
<Alert message={t('knowledge_base.not_support')} type="error" style={{ marginBottom: 20 }} showIcon />
|
||||
)}
|
||||
<FileSection>
|
||||
<TitleWrapper>
|
||||
<Title level={5}>{t('files.title')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddFile} disabled={disabled}>
|
||||
{t('knowledge.add_file')}
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddFile}>
|
||||
{t('knowledge_base.add_file')}
|
||||
</Button>
|
||||
</TitleWrapper>
|
||||
<Dragger
|
||||
@@ -227,9 +193,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
multiple={true}
|
||||
accept={fileTypes.join(',')}
|
||||
style={{ marginTop: 10, background: 'transparent' }}>
|
||||
<p className="ant-upload-text">{t('knowledge.drag_file')}</p>
|
||||
<p className="ant-upload-text">{t('knowledge_base.drag_file')}</p>
|
||||
<p className="ant-upload-hint">
|
||||
{t('knowledge.file_hint', { file_types: fileTypes.join(', ').replaceAll('.', '') })}
|
||||
{t('knowledge_base.file_hint', { file_types: fileTypes.join(', ').replaceAll('.', '') })}
|
||||
</p>
|
||||
</Dragger>
|
||||
</FileSection>
|
||||
@@ -256,9 +222,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
|
||||
<ContentSection>
|
||||
<TitleWrapper>
|
||||
<Title level={5}>{t('knowledge.directories')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddDirectory} disabled={disabled}>
|
||||
{t('knowledge.add_directory')}
|
||||
<Title level={5}>{t('knowledge_base.directories')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddDirectory}>
|
||||
{t('knowledge_base.add_directory')}
|
||||
</Button>
|
||||
</TitleWrapper>
|
||||
<FlexColumn>
|
||||
@@ -283,9 +249,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
|
||||
<ContentSection>
|
||||
<TitleWrapper>
|
||||
<Title level={5}>{t('knowledge.urls')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddUrl} disabled={disabled}>
|
||||
{t('knowledge.add_url')}
|
||||
<Title level={5}>{t('knowledge_base.urls')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddUrl}>
|
||||
{t('knowledge_base.add_url')}
|
||||
</Button>
|
||||
</TitleWrapper>
|
||||
<FlexColumn>
|
||||
@@ -310,9 +276,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
|
||||
<ContentSection>
|
||||
<TitleWrapper>
|
||||
<Title level={5}>{t('knowledge.sitemaps')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddSitemap} disabled={disabled}>
|
||||
{t('knowledge.add_sitemap')}
|
||||
<Title level={5}>{t('knowledge_base.sitemaps')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddSitemap}>
|
||||
{t('knowledge_base.add_sitemap')}
|
||||
</Button>
|
||||
</TitleWrapper>
|
||||
<FlexColumn>
|
||||
@@ -337,9 +303,9 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
|
||||
<ContentSection>
|
||||
<TitleWrapper>
|
||||
<Title level={5}>{t('knowledge.notes')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddNote} disabled={disabled}>
|
||||
{t('knowledge.add_note')}
|
||||
<Title level={5}>{t('knowledge_base.notes')}</Title>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddNote}>
|
||||
{t('knowledge_base.add_note')}
|
||||
</Button>
|
||||
</TitleWrapper>
|
||||
<FlexColumn>
|
||||
@@ -363,19 +329,15 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
|
||||
<ModelInfo>
|
||||
<label htmlFor="model-info">{t('knowledge.model_info')}</label>
|
||||
<label htmlFor="model-info">{t('knowledge_base.model_info')}</label>
|
||||
<Tag color="blue">{base.model.name}</Tag>
|
||||
<Tag color="cyan">{t('models.dimensions', { dimensions: base.dimensions || 0 })}</Tag>
|
||||
{providerName && <Tag color="purple">{providerName}</Tag>}
|
||||
<Tag color="purple">{base.model.provider}</Tag>
|
||||
</ModelInfo>
|
||||
|
||||
<IndexSection>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => KnowledgeSearchPopup.show({ base })}
|
||||
icon={<SearchOutlined />}
|
||||
disabled={disabled}>
|
||||
{t('knowledge.search')}
|
||||
<Button type="primary" onClick={() => KnowledgeSearchPopup.show({ base })} icon={<SearchOutlined />}>
|
||||
{t('knowledge_base.search')}
|
||||
</Button>
|
||||
</IndexSection>
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const KnowledgePage: FC = () => {
|
||||
const prevLength = useRef(0)
|
||||
|
||||
const handleAddKnowledge = async () => {
|
||||
await AddKnowledgePopup.show({ title: t('knowledge.add.title') })
|
||||
await AddKnowledgePopup.show({ title: t('knowledge_base.add.title') })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,12 +48,12 @@ const KnowledgePage: FC = () => {
|
||||
(base: KnowledgeBase) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
{
|
||||
label: t('knowledge.rename'),
|
||||
label: t('knowledge_base.rename'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('knowledge.rename'),
|
||||
title: t('knowledge_base.rename'),
|
||||
message: '',
|
||||
defaultValue: base.name || ''
|
||||
})
|
||||
@@ -70,7 +70,7 @@ const KnowledgePage: FC = () => {
|
||||
icon: <DeleteOutlined />,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('knowledge.delete_confirm'),
|
||||
title: t('knowledge_base.delete_confirm'),
|
||||
centered: true,
|
||||
onOk: () => {
|
||||
deleteKnowledgeBase(base.id)
|
||||
@@ -88,7 +88,7 @@ const KnowledgePage: FC = () => {
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge_base.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<SideNav>
|
||||
@@ -125,7 +125,7 @@ const KnowledgePage: FC = () => {
|
||||
</SideNav>
|
||||
{bases.length === 0 ? (
|
||||
<MainContent>
|
||||
<Empty description={t('knowledge.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty description={t('knowledge_base.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</MainContent>
|
||||
) : selectedBase ? (
|
||||
<KnowledgeContent selectedBase={selectedBase} />
|
||||
@@ -208,7 +208,7 @@ const AddKnowledgeItem = styled.div`
|
||||
padding: 7px 12px;
|
||||
position: relative;
|
||||
font-family: Ubuntu;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
border: 0.5px solid transparent;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
|
||||
@@ -72,7 +72,6 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
} catch (error) {
|
||||
console.error('Error getting embedding dimensions:', error)
|
||||
window.message.error(t('message.error.get_embedding_dimensions'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('knowledge.search')}
|
||||
title={t('knowledge_base.search')}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
@@ -88,7 +88,7 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
|
||||
transitionName="ant-move-down">
|
||||
<SearchContainer>
|
||||
<Search
|
||||
placeholder={t('knowledge.search_placeholder')}
|
||||
placeholder={t('knowledge_base.search_placeholder')}
|
||||
allowClear
|
||||
enterButton
|
||||
size="large"
|
||||
@@ -109,7 +109,7 @@ const PopupContainer: React.FC<Props> = ({ base, resolve }) => {
|
||||
<Paragraph>{highlightText(item.pageContent)}</Paragraph>
|
||||
<MetadataContainer>
|
||||
<Text type="secondary">
|
||||
{t('knowledge.source')}:{' '}
|
||||
{t('knowledge_base.source')}:{' '}
|
||||
{item.file ? (
|
||||
<a href={`http://file/${item.file.name}`} target="_blank" rel="noreferrer">
|
||||
{item.file.origin_name}
|
||||
|
||||
@@ -21,13 +21,13 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
|
||||
if (!status) {
|
||||
if (item?.uniqueId) {
|
||||
return (
|
||||
<Tooltip title={t('knowledge.status_completed')} placement="left">
|
||||
<Tooltip title={t('knowledge_base.status_completed')} placement="left">
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tooltip title={t('knowledge.status_new')} placement="left">
|
||||
<Tooltip title={t('knowledge_base.status_new')} placement="left">
|
||||
<Center style={{ width: '16px', height: '16px' }}>
|
||||
<StatusDot $status="new" />
|
||||
</Center>
|
||||
@@ -38,25 +38,25 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return (
|
||||
<Tooltip title={t('knowledge.status_pending')} placement="left">
|
||||
<Tooltip title={t('knowledge_base.status_pending')} placement="left">
|
||||
<StatusDot $status="pending" />
|
||||
</Tooltip>
|
||||
)
|
||||
case 'processing':
|
||||
return (
|
||||
<Tooltip title={t('knowledge.status_processing')} placement="left">
|
||||
<Tooltip title={t('knowledge_base.status_processing')} placement="left">
|
||||
<StatusDot $status="processing" />
|
||||
</Tooltip>
|
||||
)
|
||||
case 'completed':
|
||||
return (
|
||||
<Tooltip title={t('knowledge.status_completed')} placement="left">
|
||||
<Tooltip title={t('knowledge_base.status_completed')} placement="left">
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
</Tooltip>
|
||||
)
|
||||
case 'failed':
|
||||
return (
|
||||
<Tooltip title={errorText || t('knowledge.status_failed')} placement="left">
|
||||
<Tooltip title={errorText || t('knowledge_base.status_failed')} placement="left">
|
||||
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -72,8 +72,8 @@ const Artboard: FC<ArtboardProps> = ({
|
||||
preview={{ mask: false }}
|
||||
onContextMenu={handleContextMenu}
|
||||
style={{
|
||||
maxWidth: '70vh',
|
||||
maxHeight: '70vh',
|
||||
width: '70vh',
|
||||
height: '70vh',
|
||||
objectFit: 'contain',
|
||||
backgroundColor: 'var(--color-background-soft)',
|
||||
cursor: 'pointer'
|
||||
|
||||
@@ -6,7 +6,7 @@ import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
|
||||
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
|
||||
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
|
||||
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack, VStack } from '@renderer/components/Layout'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
@@ -25,7 +25,7 @@ import { DEFAULT_PAINTING } from '@renderer/store/paintings'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { FileType, Painting } from '@renderer/types'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { Button, Input, InputNumber, Radio, Select, Slider, Tooltip } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -149,13 +149,8 @@ const PaintingsPage: FC = () => {
|
||||
dispatch(setGenerating(true))
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
if (!painting.model) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const urls = await AI.generateImage({
|
||||
model: painting.model,
|
||||
prompt,
|
||||
negativePrompt: painting.negativePrompt || '',
|
||||
imageSize: painting.imageSize || '1024x1024',
|
||||
@@ -163,8 +158,7 @@ const PaintingsPage: FC = () => {
|
||||
seed: painting.seed || undefined,
|
||||
numInferenceSteps: painting.steps || 25,
|
||||
guidanceScale: painting.guidanceScale || 4.5,
|
||||
signal: controller.signal,
|
||||
promptEnhancement: painting.promptEnhancement || false
|
||||
signal: controller.signal
|
||||
})
|
||||
|
||||
if (urls.length > 0) {
|
||||
@@ -366,15 +360,13 @@ const PaintingsPage: FC = () => {
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<SliderContainer>
|
||||
<Slider min={1} max={50} value={painting.steps} onChange={(v) => updatePaintingState({ steps: v })} />
|
||||
<StyledInputNumber
|
||||
min={1}
|
||||
max={50}
|
||||
value={painting.steps}
|
||||
onChange={(v) => updatePaintingState({ steps: (v as number) || 25 })}
|
||||
/>
|
||||
</SliderContainer>
|
||||
<Slider min={1} max={50} value={painting.steps} onChange={(v) => updatePaintingState({ steps: v })} />
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={50}
|
||||
value={painting.steps}
|
||||
onChange={(v) => updatePaintingState({ steps: v || 25 })}
|
||||
/>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t('paintings.guidance_scale')}
|
||||
@@ -382,22 +374,21 @@ const PaintingsPage: FC = () => {
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<SliderContainer>
|
||||
<Slider
|
||||
min={1}
|
||||
max={20}
|
||||
step={0.1}
|
||||
value={painting.guidanceScale}
|
||||
onChange={(v) => updatePaintingState({ guidanceScale: v })}
|
||||
/>
|
||||
<StyledInputNumber
|
||||
min={1}
|
||||
max={20}
|
||||
step={0.1}
|
||||
value={painting.guidanceScale}
|
||||
onChange={(v) => updatePaintingState({ guidanceScale: (v as number) || 4.5 })}
|
||||
/>
|
||||
</SliderContainer>
|
||||
<Slider
|
||||
min={1}
|
||||
max={20}
|
||||
step={0.1}
|
||||
value={painting.guidanceScale}
|
||||
onChange={(v) => updatePaintingState({ guidanceScale: v })}
|
||||
/>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={20}
|
||||
step={0.1}
|
||||
value={painting.guidanceScale}
|
||||
onChange={(v) => updatePaintingState({ guidanceScale: v || 4.5 })}
|
||||
/>
|
||||
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t('paintings.negative_prompt')}
|
||||
<Tooltip title={t('paintings.negative_prompt_tip')}>
|
||||
@@ -407,21 +398,8 @@ const PaintingsPage: FC = () => {
|
||||
<TextArea
|
||||
value={painting.negativePrompt}
|
||||
onChange={(e) => updatePaintingState({ negativePrompt: e.target.value })}
|
||||
spellCheck={false}
|
||||
rows={4}
|
||||
/>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t('paintings.prompt_enhancement')}
|
||||
<Tooltip title={t('paintings.prompt_enhancement_tip')}>
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
</SettingTitle>
|
||||
<HStack>
|
||||
<Switch
|
||||
checked={painting.promptEnhancement}
|
||||
onChange={(checked) => updatePaintingState({ promptEnhancement: checked })}
|
||||
/>
|
||||
</HStack>
|
||||
</LeftContainer>
|
||||
<MainContainer>
|
||||
<Artboard
|
||||
@@ -438,7 +416,6 @@ const PaintingsPage: FC = () => {
|
||||
variant="borderless"
|
||||
disabled={isLoading}
|
||||
value={painting.prompt}
|
||||
spellCheck={false}
|
||||
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
|
||||
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -570,18 +547,4 @@ const InfoIcon = styled(QuestionCircleOutlined)`
|
||||
}
|
||||
`
|
||||
|
||||
const SliderContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.ant-slider {
|
||||
flex: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const StyledInputNumber = styled(InputNumber)`
|
||||
width: 70px;
|
||||
`
|
||||
|
||||
export default PaintingsPage
|
||||
|
||||
@@ -103,7 +103,6 @@ const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, upda
|
||||
value={messages[index].content}
|
||||
onChange={(e) => updateMessages(index, 'user', e.target.value)}
|
||||
placeholder={t('agents.edit.message.user.placeholder')}
|
||||
spellCheck={false}
|
||||
rows={1}
|
||||
/>
|
||||
</Col>
|
||||
@@ -117,7 +116,6 @@ const AssistantMessagesSettings: FC<Props> = ({ assistant, updateAssistant, upda
|
||||
value={messages[index + 1]?.content || ''}
|
||||
onChange={(e) => updateMessages(index + 1, 'assistant', e.target.value)}
|
||||
placeholder={t('agents.edit.message.assistant.placeholder')}
|
||||
spellCheck={false}
|
||||
rows={3}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -4,9 +4,9 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { SettingRow } from '@renderer/pages/settings'
|
||||
import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '@renderer/types'
|
||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||
import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -25,14 +25,13 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
|
||||
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
|
||||
const [customParameters, setCustomParameters] = useState<AssistantSettingCustomParameters[]>(
|
||||
assistant?.settings?.customParameters ?? []
|
||||
)
|
||||
|
||||
const customParametersRef = useRef(customParameters)
|
||||
|
||||
customParametersRef.current = customParameters
|
||||
|
||||
const [customParameters, setCustomParameters] = useState<
|
||||
Array<{
|
||||
name: string
|
||||
value: string | number | boolean
|
||||
type: 'string' | 'number' | 'boolean'
|
||||
}>
|
||||
>(assistant?.settings?.customParameters ?? [])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onTemperatureChange = (value) => {
|
||||
@@ -69,7 +68,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
const onUpdateCustomParameter = (
|
||||
index: number,
|
||||
field: 'name' | 'value' | 'type',
|
||||
value: string | number | boolean | object
|
||||
value: string | number | boolean
|
||||
) => {
|
||||
const newParams = [...customParameters]
|
||||
if (field === 'type') {
|
||||
@@ -81,9 +80,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
case 'boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
case 'json':
|
||||
defaultValue = ''
|
||||
break
|
||||
default:
|
||||
defaultValue = ''
|
||||
}
|
||||
@@ -96,6 +92,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
newParams[index] = { ...newParams[index], [field]: value }
|
||||
}
|
||||
setCustomParameters(newParams)
|
||||
updateAssistantSettings({ customParameters: newParams })
|
||||
}
|
||||
|
||||
const renderParameterValueInput = (param: (typeof customParameters)[0], index: number) => {
|
||||
@@ -116,20 +113,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
onChange={(checked) => onUpdateCustomParameter(index, 'value', checked)}
|
||||
/>
|
||||
)
|
||||
case 'json':
|
||||
return (
|
||||
<Input
|
||||
value={typeof param.value === 'string' ? param.value : JSON.stringify(param.value, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const jsonValue = JSON.parse(e.target.value)
|
||||
onUpdateCustomParameter(index, 'value', jsonValue)
|
||||
} catch {
|
||||
onUpdateCustomParameter(index, 'value', e.target.value)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
@@ -176,10 +159,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => updateAssistantSettings({ customParameters: customParametersRef.current })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row align="middle" style={{ marginBottom: 10 }}>
|
||||
@@ -358,7 +337,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
</Button>
|
||||
</SettingRow>
|
||||
{customParameters.map((param, index) => (
|
||||
<Row key={index} align="stretch" gutter={10} style={{ marginTop: 10 }}>
|
||||
<Row key={index} align="middle" gutter={10} style={{ marginTop: 10 }}>
|
||||
<Col span={6}>
|
||||
<Input
|
||||
placeholder={t('models.parameter_name')}
|
||||
@@ -374,11 +353,10 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
|
||||
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
|
||||
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
|
||||
<Select.Option value="json">{t('models.parameter_type.json')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
<Col span={12}>{renderParameterValueInput(param, index)}</Col>
|
||||
<Col span={2} style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Col span={11}>{renderParameterValueInput(param, index)}</Col>
|
||||
<Col span={3}>
|
||||
<Button icon={<DeleteOutlined />} onClick={() => onDeleteCustomParameter(index)} danger />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -43,7 +43,6 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant,
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
onBlur={onUpdate}
|
||||
spellCheck={false}
|
||||
style={{ minHeight: 'calc(80vh - 200px)', maxHeight: 'calc(80vh - 150px)' }}
|
||||
/>
|
||||
<HStack width="100%" justifyContent="flex-end" mt="10px">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { backup, reset, restore } from '@renderer/services/BackupService'
|
||||
import { AppInfo } from '@renderer/types'
|
||||
import { Button, Modal, Typography } from 'antd'
|
||||
import { Button, message, Modal, Typography } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -42,9 +42,9 @@ const DataSettings: FC = () => {
|
||||
onOk: async () => {
|
||||
try {
|
||||
await window.api.clearCache()
|
||||
window.message.success(t('settings.data.clear_cache.success'))
|
||||
message.success(t('settings.data.clear_cache.success'))
|
||||
} catch (error) {
|
||||
window.message.error(t('settings.data.clear_cache.error'))
|
||||
message.error(t('settings.data.clear_cache.error'))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { FolderOpenOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
@@ -12,8 +11,7 @@ import {
|
||||
setWebdavSyncInterval as _setWebdavSyncInterval,
|
||||
setWebdavUser as _setWebdavUser
|
||||
} from '@renderer/store/settings'
|
||||
import { Button, Input, Select } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Button, Input, Select, Switch } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -25,6 +23,7 @@ const WebDavSettings: FC = () => {
|
||||
webdavUser: webDAVUser,
|
||||
webdavPass: webDAVPass,
|
||||
webdavPath: webDAVPath,
|
||||
webdavAutoSync: webDAVAutoSync,
|
||||
webdavSyncInterval: webDAVSyncInterval
|
||||
} = useSettings()
|
||||
|
||||
@@ -33,6 +32,7 @@ const WebDavSettings: FC = () => {
|
||||
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
|
||||
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
|
||||
|
||||
const [autoSync, setAutoSync] = useState<boolean>(webDAVAutoSync)
|
||||
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
|
||||
|
||||
const [backuping, setBackuping] = useState(false)
|
||||
@@ -42,8 +42,6 @@ const WebDavSettings: FC = () => {
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { webdavSync } = useRuntime()
|
||||
|
||||
// 把之前备份的文件定时上传到 webdav,首先先配置 webdav 的 host, port, user, pass, path
|
||||
|
||||
const onBackup = async () => {
|
||||
@@ -66,40 +64,18 @@ const WebDavSettings: FC = () => {
|
||||
setRestoring(false)
|
||||
}
|
||||
|
||||
const onSyncIntervalChange = (value: number) => {
|
||||
setSyncInterval(value)
|
||||
dispatch(_setWebdavSyncInterval(value))
|
||||
if (value === 0) {
|
||||
dispatch(setWebdavAutoSync(false))
|
||||
stopAutoSync()
|
||||
} else {
|
||||
dispatch(setWebdavAutoSync(true))
|
||||
const onToggleAutoSync = (checked: boolean) => {
|
||||
dispatch(setWebdavAutoSync(checked))
|
||||
if (checked) {
|
||||
startAutoSync()
|
||||
} else {
|
||||
stopAutoSync()
|
||||
}
|
||||
}
|
||||
|
||||
const renderSyncStatus = () => {
|
||||
if (!webdavHost) return null
|
||||
|
||||
if (!webdavSync.lastSyncTime && !webdavSync.syncing && !webdavSync.lastSyncError) {
|
||||
return <span style={{ color: 'var(--text-secondary)' }}>{t('settings.data.webdav.noSync')}</span>
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack gap="5px" alignItems="center">
|
||||
{webdavSync.syncing && <SyncOutlined spin />}
|
||||
{webdavSync.lastSyncTime && (
|
||||
<span style={{ color: 'var(--text-secondary)' }}>
|
||||
{t('settings.data.webdav.lastSync')}: {dayjs(webdavSync.lastSyncTime).format('HH:mm:ss')}
|
||||
</span>
|
||||
)}
|
||||
{webdavSync.lastSyncError && (
|
||||
<span style={{ color: 'var(--error-color)' }}>
|
||||
{t('settings.data.webdav.syncError')}: {webdavSync.lastSyncError}
|
||||
</span>
|
||||
)}
|
||||
</HStack>
|
||||
)
|
||||
const onSyncIntervalChange = (value: number) => {
|
||||
setSyncInterval(value)
|
||||
dispatch(_setWebdavSyncInterval(value))
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -151,6 +127,32 @@ const WebDavSettings: FC = () => {
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
|
||||
<HStack gap="10px" alignItems="center">
|
||||
<Switch
|
||||
checked={autoSync}
|
||||
onChange={(checked) => {
|
||||
setAutoSync(checked)
|
||||
onToggleAutoSync(checked)
|
||||
}}
|
||||
disabled={!webdavHost}
|
||||
/>
|
||||
<Select
|
||||
value={syncInterval || 5}
|
||||
onChange={onSyncIntervalChange}
|
||||
disabled={!webdavHost || !autoSync}
|
||||
style={{ width: 120 }}>
|
||||
<Select.Option value={1}>1 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={5}>5 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={15}>15 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={30}>30 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={60}>60 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={120}>120 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
</Select>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
@@ -163,28 +165,6 @@ const WebDavSettings: FC = () => {
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.autoSync')}</SettingRowTitle>
|
||||
<Select value={syncInterval} onChange={onSyncIntervalChange} disabled={!webdavHost} style={{ width: 120 }}>
|
||||
<Select.Option value={0}>{t('settings.data.webdav.autoSync.off')}</Select.Option>
|
||||
<Select.Option value={1}>1 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={5}>5 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={15}>15 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={30}>30 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={60}>60 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
<Select.Option value={120}>120 {t('settings.data.webdav.minutes')}</Select.Option>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
{webdavSync && syncInterval > 0 && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.syncStatus')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,20 +3,18 @@ import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import {
|
||||
DEFAULT_SIDEBAR_ICONS,
|
||||
setClickAssistantToShowTopic,
|
||||
setCustomCss,
|
||||
setShowTopicTime,
|
||||
setSidebarIcons
|
||||
setShowFilesIcon,
|
||||
setShowMinappIcon,
|
||||
setShowTopicTime
|
||||
} from '@renderer/store/settings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { Button, Input, Select, Switch } from 'antd'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { Input, Select, Switch } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import SidebarIconsManager from './SidebarIconsManager'
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
||||
|
||||
const DisplaySettings: FC = () => {
|
||||
const {
|
||||
@@ -24,33 +22,22 @@ const DisplaySettings: FC = () => {
|
||||
theme,
|
||||
windowStyle,
|
||||
setWindowStyle,
|
||||
showMinappIcon,
|
||||
showFilesIcon,
|
||||
topicPosition,
|
||||
setTopicPosition,
|
||||
clickAssistantToShowTopic,
|
||||
showTopicTime,
|
||||
customCss,
|
||||
sidebarIcons
|
||||
customCss
|
||||
} = useSettings()
|
||||
const { theme: themeMode } = useTheme()
|
||||
|
||||
const { t } = useTranslation()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
|
||||
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])
|
||||
const handleWindowStyleChange = (checked: boolean) => {
|
||||
setWindowStyle(checked ? 'transparent' : 'opaque')
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer theme={themeMode}>
|
||||
@@ -60,7 +47,7 @@ const DisplaySettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
||||
<Select
|
||||
value={theme}
|
||||
defaultValue={theme}
|
||||
style={{ width: 120 }}
|
||||
onChange={setTheme}
|
||||
options={[
|
||||
@@ -86,7 +73,7 @@ const DisplaySettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
||||
<Select
|
||||
value={topicPosition || 'right'}
|
||||
defaultValue={topicPosition || 'right'}
|
||||
style={{ width: 120 }}
|
||||
onChange={setTopicPosition}
|
||||
options={[
|
||||
@@ -114,27 +101,24 @@ const DisplaySettings: FC = () => {
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<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>
|
||||
<SettingTitle>{t('settings.display.sidebar.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SidebarIconsManager
|
||||
visibleIcons={visibleIcons}
|
||||
disabledIcons={disabledIcons}
|
||||
setVisibleIcons={setVisibleIcons}
|
||||
setDisabledIcons={setDisabledIcons}
|
||||
/>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.display.sidebar.minapp.icon')}</SettingRowTitle>
|
||||
<Switch checked={showMinappIcon} onChange={(value) => dispatch(setShowMinappIcon(value))} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.display.sidebar.files.icon')}</SettingRowTitle>
|
||||
<Switch checked={showFilesIcon} onChange={(value) => dispatch(setShowFilesIcon(value))} />
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.display.custom.css')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<Input.TextArea
|
||||
value={customCss}
|
||||
onChange={(e) => dispatch(setCustomCss(e.target.value))}
|
||||
defaultValue={customCss}
|
||||
onBlur={(e) => dispatch(setCustomCss(e.target.value))}
|
||||
placeholder={t('settings.display.custom.css.placeholder')}
|
||||
style={{
|
||||
minHeight: 200,
|
||||
@@ -146,10 +130,4 @@ const DisplaySettings: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const ResetButtonWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
export default DisplaySettings
|
||||
@@ -1,272 +0,0 @@
|
||||
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
|
||||
@@ -89,7 +89,6 @@ const AssistantSettings: FC = () => {
|
||||
value={defaultAssistant.prompt}
|
||||
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
|
||||
style={{ margin: '10px 0' }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
|
||||
@@ -41,39 +41,22 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const onAddModel = (values: FieldType) => {
|
||||
const id = values.id.trim()
|
||||
|
||||
if (find(models, { id })) {
|
||||
window.message.error('Model ID already exists')
|
||||
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
|
||||
if (find(models, { id: values.id })) {
|
||||
Modal.error({ title: 'Error', content: 'Model ID already exists' })
|
||||
return
|
||||
}
|
||||
|
||||
const model: Model = {
|
||||
id,
|
||||
provider: provider.id,
|
||||
name: values.name ? values.name : id.toUpperCase(),
|
||||
group: getDefaultGroupName(values.group || id)
|
||||
id: values.id,
|
||||
name: values.name ? values.name : values.id.toUpperCase(),
|
||||
group: getDefaultGroupName(values.group || values.id)
|
||||
}
|
||||
|
||||
addModel(model)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const onFinish: FormProps<FieldType>['onFinish'] = (values) => {
|
||||
const id = values.id.trim().replaceAll(',', ',')
|
||||
|
||||
if (id.includes(',')) {
|
||||
const ids = id.split(',')
|
||||
ids.forEach((id) => onAddModel({ id, name: id } as FieldType))
|
||||
resolve({})
|
||||
return
|
||||
}
|
||||
|
||||
if (onAddModel(values)) {
|
||||
resolve({})
|
||||
}
|
||||
resolve(model)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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 ModelTags from '@renderer/components/ModelTags'
|
||||
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel, SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { fetchModels } from '@renderer/services/ApiService'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
|
||||
import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tooltip } from 'antd'
|
||||
import { Avatar, Button, Empty, Flex, Modal, Popover, Radio, Tag, Tooltip } from 'antd'
|
||||
import Search from 'antd/es/input/Search'
|
||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
@@ -81,18 +82,16 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
setLoading(true)
|
||||
const models = await fetchModels(_provider)
|
||||
setListModels(
|
||||
models
|
||||
.map((model) => ({
|
||||
id: model.id,
|
||||
// @ts-ignore name
|
||||
name: model.name || model.id,
|
||||
provider: _provider.id,
|
||||
group: getDefaultGroupName(model.id),
|
||||
// @ts-ignore name
|
||||
description: model?.description,
|
||||
owned_by: model?.owned_by
|
||||
}))
|
||||
.filter((model) => !isEmpty(model.id))
|
||||
models.map((model) => ({
|
||||
id: model.id,
|
||||
// @ts-ignore name
|
||||
name: model.name || model.id,
|
||||
provider: _provider.id,
|
||||
group: getDefaultGroupName(model.id),
|
||||
// @ts-ignore name
|
||||
description: model?.description,
|
||||
owned_by: model?.owned_by
|
||||
}))
|
||||
)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
@@ -157,7 +156,18 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
<Tooltip title={model.id} placement="top">
|
||||
<span style={{ cursor: 'help' }}>{model.name}</span>
|
||||
</Tooltip>
|
||||
<ModelTags model={model} />
|
||||
{isVisionModel(model) && <VisionIcon />}
|
||||
{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) && (
|
||||
<Popover
|
||||
trigger="click"
|
||||
|
||||
@@ -7,8 +7,16 @@ import {
|
||||
PlusOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import ModelTags from '@renderer/components/ModelTags'
|
||||
import { EMBEDDING_REGEX, getModelLogo, VISION_REGEX } from '@renderer/config/models'
|
||||
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||
import WebSearchIcon from '@renderer/components/Icons/WebSearchIcon'
|
||||
import {
|
||||
EMBEDDING_REGEX,
|
||||
getModelLogo,
|
||||
isEmbeddingModel,
|
||||
isVisionModel,
|
||||
isWebSearchModel,
|
||||
VISION_REGEX
|
||||
} from '@renderer/config/models'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/providers'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAssistants, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
@@ -19,7 +27,7 @@ import { checkApi } from '@renderer/services/ApiService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setModel } from '@renderer/store/assistants'
|
||||
import { Model, ModelType, Provider } from '@renderer/types'
|
||||
import { Avatar, Button, Card, Checkbox, Divider, Flex, Input, Popover, Space, Switch } from 'antd'
|
||||
import { Avatar, Button, Card, Checkbox, Divider, Flex, Input, Popover, Space, Switch, Tag } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import { groupBy, isEmpty } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
@@ -270,8 +278,13 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
|
||||
{model.name[0].toUpperCase()}
|
||||
</Avatar>
|
||||
{model.name}
|
||||
<ModelTags model={model} />
|
||||
{model.name} {isVisionModel(model) && <VisionIcon />}
|
||||
{isWebSearchModel(model) && <WebSearchIcon />}
|
||||
{isEmbeddingModel(model) && (
|
||||
<Tag style={{ marginLeft: 10 }} color="orange">
|
||||
{t('models.embedding')}
|
||||
</Tag>
|
||||
)}
|
||||
<Popover content={modelTypeContent(model)} title={t('models.type.select')} trigger="click">
|
||||
<SettingIcon />
|
||||
</Popover>
|
||||
|
||||
@@ -124,7 +124,7 @@ const ProvidersList: FC = () => {
|
||||
{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}
|
||||
</ProviderItemName>
|
||||
{provider.enabled && (
|
||||
<Tag color="green" style={{ marginLeft: 'auto', marginRight: 0, borderRadius: 16 }}>
|
||||
<Tag color="green" style={{ marginLeft: 'auto', borderRadius: 16 }}>
|
||||
ON
|
||||
</Tag>
|
||||
)}
|
||||
@@ -163,7 +163,7 @@ const Container = styled.div`
|
||||
const ProviderListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: calc(var(--settings-width) + 10px);
|
||||
width: var(--assistants-width);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
`
|
||||
@@ -173,17 +173,16 @@ const ProviderList = styled.div`
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
padding-right: 5px;
|
||||
`
|
||||
|
||||
const ProviderListItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
padding: 5px 8px;
|
||||
width: 100%;
|
||||
cursor: grab;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 0.5px solid transparent;
|
||||
|
||||
@@ -15,7 +15,7 @@ import styled from 'styled-components'
|
||||
|
||||
import AboutSettings from './AboutSettings'
|
||||
import DataSettings from './DataSettings/DataSettings'
|
||||
import DisplaySettings from './DisplaySettings/DisplaySettings'
|
||||
import DisplaySettings from './DisplaySettings'
|
||||
import GeneralSettings from './GeneralSettings'
|
||||
import ModelSettings from './ModalSettings/ModelSettings'
|
||||
import ProvidersList from './ProviderSettings'
|
||||
@@ -132,7 +132,7 @@ const MenuItem = styled.li`
|
||||
padding: 6px 10px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease-in-out;
|
||||
border: 0.5px solid transparent;
|
||||
|
||||
@@ -211,7 +211,6 @@ const TranslatePage: FC = () => {
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={loading}
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
<TranslateButton
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import BaseProvider from '@renderer/providers/BaseProvider'
|
||||
import ProviderFactory from '@renderer/providers/ProviderFactory'
|
||||
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { CompletionsParams } from '.'
|
||||
@@ -48,7 +48,16 @@ export default class AiProvider {
|
||||
return this.sdk.getApiKey()
|
||||
}
|
||||
|
||||
public async generateImage(params: GenerateImageParams): Promise<string[]> {
|
||||
public async generateImage(params: {
|
||||
prompt: string
|
||||
negativePrompt: string
|
||||
imageSize: string
|
||||
batchSize: number
|
||||
seed?: string
|
||||
numInferenceSteps: number
|
||||
guidanceScale: number
|
||||
signal?: AbortSignal
|
||||
}): Promise<string[]> {
|
||||
return this.sdk.generateImage(params)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@rende
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { filterContextMessages } from '@renderer/services/MessagesService'
|
||||
import { Assistant, FileTypes, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeSpecialCharacters } from '@renderer/utils'
|
||||
import { first, flatten, last, sum, takeRight } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
@@ -207,9 +206,7 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
max_tokens: 4096
|
||||
})
|
||||
|
||||
const content = message.content[0].type === 'text' ? message.content[0].text : ''
|
||||
|
||||
return removeSpecialCharacters(content)
|
||||
return message.content[0].type === 'text' ? message.content[0].text : ''
|
||||
}
|
||||
|
||||
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { REFERENCE_PROMPT } from '@renderer/config/prompts'
|
||||
import { getOllamaKeepAliveTime } from '@renderer/hooks/useOllama'
|
||||
import { getKnowledgeReferences } from '@renderer/services/KnowledgeService'
|
||||
import store from '@renderer/store'
|
||||
import { Assistant, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { delay, isJSON } from '@renderer/utils'
|
||||
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { delay } from '@renderer/utils'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { CompletionsParams } from '.'
|
||||
@@ -26,7 +26,16 @@ export default abstract class BaseProvider {
|
||||
abstract generateText({ prompt, content }: { prompt: string; content: string }): Promise<string>
|
||||
abstract check(): Promise<{ valid: boolean; error: Error | null }>
|
||||
abstract models(): Promise<OpenAI.Models.Model[]>
|
||||
abstract generateImage(params: GenerateImageParams): Promise<string[]>
|
||||
abstract generateImage(_params: {
|
||||
prompt: string
|
||||
negativePrompt: string
|
||||
imageSize: string
|
||||
batchSize: number
|
||||
seed?: string
|
||||
numInferenceSteps: number
|
||||
guidanceScale: number
|
||||
signal?: AbortSignal
|
||||
}): Promise<string[]>
|
||||
abstract getEmbeddingDimensions(model: Model): Promise<number>
|
||||
|
||||
public getBaseURL(): string {
|
||||
@@ -92,16 +101,13 @@ export default abstract class BaseProvider {
|
||||
|
||||
protected getCustomParameters(assistant: Assistant) {
|
||||
return (
|
||||
assistant?.settings?.customParameters?.reduce((acc, param) => {
|
||||
if (!param.name?.trim()) {
|
||||
return acc
|
||||
}
|
||||
if (param.type === 'json') {
|
||||
const value = param.value as string
|
||||
return { ...acc, [param.name]: isJSON(value) ? JSON.parse(value) : value }
|
||||
}
|
||||
return { ...acc, [param.name]: param.value }
|
||||
}, {}) || {}
|
||||
assistant?.settings?.customParameters?.reduce(
|
||||
(acc, param) => ({
|
||||
...acc,
|
||||
[param.name]: param.value
|
||||
}),
|
||||
{}
|
||||
) || {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Content,
|
||||
FileDataPart,
|
||||
GoogleGenerativeAI,
|
||||
HarmBlockThreshold,
|
||||
HarmCategory,
|
||||
@@ -15,8 +14,7 @@ import i18n from '@renderer/i18n'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { filterContextMessages } from '@renderer/services/MessagesService'
|
||||
import { Assistant, FileType, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeSpecialCharacters } from '@renderer/utils'
|
||||
import { Assistant, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import axios from 'axios'
|
||||
import { first, isEmpty, last, takeRight } from 'lodash'
|
||||
import OpenAI from 'openai'
|
||||
@@ -40,43 +38,6 @@ export default class GeminiProvider extends BaseProvider {
|
||||
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> {
|
||||
const role = message.role === 'user' ? 'user' : 'model'
|
||||
|
||||
@@ -92,12 +53,6 @@ export default class GeminiProvider extends BaseProvider {
|
||||
}
|
||||
} as InlineDataPart)
|
||||
}
|
||||
|
||||
if (file.ext === '.pdf') {
|
||||
parts.push(await this.handlePdfFile(file))
|
||||
continue
|
||||
}
|
||||
|
||||
if ([FileTypes.TEXT, FileTypes.DOCUMENT].includes(file.type)) {
|
||||
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
|
||||
parts.push({
|
||||
@@ -137,7 +92,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
model: model.id,
|
||||
systemInstruction: assistant.prompt,
|
||||
// @ts-ignore googleSearch is not a valid tool for Gemini
|
||||
tools: assistant.enableWebSearch && isWebSearchModel(model) ? [{ googleSearch: {} }] : undefined,
|
||||
tools: assistant.enableWebSearch && isWebSearchModel(model) ? [{ googleSearch: {} }] : [],
|
||||
generationConfig: {
|
||||
maxOutputTokens: maxTokens,
|
||||
temperature: assistant?.settings?.temperature,
|
||||
@@ -268,7 +223,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
|
||||
const { response } = await chat.sendMessage(userMessage.content)
|
||||
|
||||
return removeSpecialCharacters(response.text())
|
||||
return response.text()
|
||||
}
|
||||
|
||||
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { getWebSearchParams, isEmbeddingModel, isSupportedModel, isVisionModel } from '@renderer/config/models'
|
||||
import { isEmbeddingModel, isSupportedModel, isVisionModel } from '@renderer/config/models'
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { filterContextMessages } from '@renderer/services/MessagesService'
|
||||
import { Assistant, FileTypes, GenerateImageParams, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeSpecialCharacters } from '@renderer/utils'
|
||||
import DuckDuckGoLiteSearch from '@renderer/tools/DuckDuckGoLiteSearch/function.json'
|
||||
import DuckDuckGoLiteSearchCode from '@renderer/tools/DuckDuckGoLiteSearch/index.js?raw'
|
||||
import { Assistant, FileTypes, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { removeQuotes } from '@renderer/utils'
|
||||
import { last, takeRight } from 'lodash'
|
||||
import OpenAI, { AzureOpenAI } from 'openai'
|
||||
import {
|
||||
ChatCompletionContentPart,
|
||||
ChatCompletionCreateParamsNonStreaming,
|
||||
ChatCompletionMessageParam
|
||||
ChatCompletionMessageParam,
|
||||
ChatCompletionTool
|
||||
} from 'openai/resources'
|
||||
|
||||
import { CompletionsParams } from '.'
|
||||
@@ -134,13 +137,6 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
|
||||
const isOpenAIo1 = model.id.includes('o1-')
|
||||
|
||||
const isSupportStreamOutput = () => {
|
||||
if (this.provider.id === 'github' && isOpenAIo1) {
|
||||
return false
|
||||
}
|
||||
return streamOutput
|
||||
}
|
||||
|
||||
let time_first_token_millsec = 0
|
||||
const start_time_millsec = new Date().getTime()
|
||||
|
||||
@@ -154,13 +150,28 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
top_p: assistant?.settings?.topP,
|
||||
max_tokens: maxTokens,
|
||||
keep_alive: this.keepAliveTime,
|
||||
stream: isSupportStreamOutput(),
|
||||
...(assistant.enableWebSearch ? getWebSearchParams(model) : {}),
|
||||
stream: streamOutput,
|
||||
tools: [DuckDuckGoLiteSearch as ChatCompletionTool],
|
||||
...this.getCustomParameters(assistant)
|
||||
})
|
||||
|
||||
if (!isSupportStreamOutput()) {
|
||||
if (!streamOutput) {
|
||||
const time_completion_millsec = new Date().getTime() - start_time_millsec
|
||||
|
||||
stream.choices[0].message?.tool_calls?.forEach(async (toolCall) => {
|
||||
const functionName = toolCall.function.name
|
||||
const params = toolCall.function.arguments
|
||||
|
||||
console.log(functionName, DuckDuckGoLiteSearchCode)
|
||||
|
||||
const result = await window.api.vm.run(`
|
||||
var params = ${params};
|
||||
${DuckDuckGoLiteSearchCode}
|
||||
`)
|
||||
|
||||
console.log(result)
|
||||
})
|
||||
|
||||
return onChunk({
|
||||
text: stream.choices[0].message?.content || '',
|
||||
usage: stream.usage,
|
||||
@@ -205,8 +216,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
model: model.id,
|
||||
messages: messages as ChatCompletionMessageParam[],
|
||||
stream: false,
|
||||
keep_alive: this.keepAliveTime,
|
||||
temperature: assistant?.settings?.temperature
|
||||
keep_alive: this.keepAliveTime
|
||||
})
|
||||
|
||||
return response.choices[0].message?.content || ''
|
||||
@@ -246,7 +256,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
max_tokens: 1000
|
||||
})
|
||||
|
||||
return removeSpecialCharacters(response.choices[0].message?.content?.substring(0, 50) || '')
|
||||
return removeQuotes(response.choices[0].message?.content?.substring(0, 50) || '')
|
||||
}
|
||||
|
||||
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
||||
@@ -352,7 +362,6 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
public async generateImage({
|
||||
model,
|
||||
prompt,
|
||||
negativePrompt,
|
||||
imageSize,
|
||||
@@ -360,23 +369,30 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
seed,
|
||||
numInferenceSteps,
|
||||
guidanceScale,
|
||||
signal,
|
||||
promptEnhancement
|
||||
}: GenerateImageParams): Promise<string[]> {
|
||||
signal
|
||||
}: {
|
||||
prompt: string
|
||||
negativePrompt?: string
|
||||
imageSize: string
|
||||
batchSize: number
|
||||
seed?: string
|
||||
numInferenceSteps: number
|
||||
guidanceScale: number
|
||||
signal?: AbortSignal
|
||||
}): Promise<string[]> {
|
||||
const response = (await this.sdk.request({
|
||||
method: 'post',
|
||||
path: '/images/generations',
|
||||
signal,
|
||||
body: {
|
||||
model,
|
||||
model: 'stabilityai/stable-diffusion-3-5-large',
|
||||
prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
image_size: imageSize,
|
||||
batch_size: batchSize,
|
||||
seed: seed ? parseInt(seed) : undefined,
|
||||
num_inference_steps: numInferenceSteps,
|
||||
guidance_scale: guidanceScale,
|
||||
prompt_enhancement: promptEnhancement
|
||||
guidance_scale: guidanceScale
|
||||
}
|
||||
})) as { data: Array<{ url: string }> }
|
||||
|
||||
|
||||
2
src/renderer/src/providers/index.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import type { Assistant, Metrics } from '@renderer/types'
|
||||
import { Metrics } from "@renderer/types"
|
||||
|
||||
interface ChunkCallbackData {
|
||||
text?: string
|
||||
|
||||
@@ -230,8 +230,12 @@ export async function fetchModels(provider: Provider) {
|
||||
|
||||
function formatErrorMessage(error: any): string {
|
||||
try {
|
||||
return '```json\n' + JSON.stringify(error, null, 2) + '\n```'
|
||||
return (
|
||||
'```json\n' +
|
||||
JSON.stringify(error?.response?.data || error?.response || error?.request || error, null, 2) +
|
||||
'\n```'
|
||||
)
|
||||
} catch (e) {
|
||||
return 'Error: ' + error?.message
|
||||
return 'Error: ' + error.message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,6 @@ export function getDefaultTranslateAssistant(targetLanguage: string, text: strin
|
||||
const translateModel = getTranslateModel()
|
||||
const assistant: Assistant = getDefaultAssistant()
|
||||
assistant.model = translateModel
|
||||
|
||||
assistant.settings = {
|
||||
temperature: 0.7
|
||||
}
|
||||
|
||||
assistant.prompt = store
|
||||
.getState()
|
||||
.settings.translateModelPrompt.replace('{{target_language}}', targetLanguage)
|
||||
@@ -105,7 +100,6 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
|
||||
maxTokens: getAssistantMaxTokens(),
|
||||
streamOutput: assistant?.settings?.streamOutput ?? true,
|
||||
hideMessages: assistant?.settings?.hideMessages ?? false,
|
||||
defaultModel: assistant?.defaultModel ?? undefined,
|
||||
autoResetModel: assistant?.settings?.autoResetModel ?? false,
|
||||
customParameters: assistant?.settings?.customParameters ?? []
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { setWebDAVSyncState } from '@renderer/store/runtime'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export async function backup() {
|
||||
@@ -64,9 +63,6 @@ export async function backupToWebdav({ showMessage = true }: { showMessage?: boo
|
||||
console.log('[Backup] Manual backup already in progress')
|
||||
return
|
||||
}
|
||||
|
||||
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
|
||||
|
||||
const { webdavHost, webdavUser, webdavPass, webdavPath } = store.getState().settings
|
||||
|
||||
const backupData = await getBackupData()
|
||||
@@ -80,19 +76,11 @@ export async function backupToWebdav({ showMessage = true }: { showMessage?: boo
|
||||
webdavPath
|
||||
})
|
||||
if (success) {
|
||||
store.dispatch(
|
||||
setWebDAVSyncState({
|
||||
lastSyncTime: Date.now(),
|
||||
lastSyncError: null
|
||||
})
|
||||
)
|
||||
showMessage && window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
|
||||
} else {
|
||||
store.dispatch(setWebDAVSyncState({ lastSyncError: 'Backup failed' }))
|
||||
showMessage && window.message.error({ content: i18n.t('message.backup.failed'), key: 'backup' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
store.dispatch(setWebDAVSyncState({ lastSyncError: error.message }))
|
||||
console.error('[backup] backupToWebdav: Error uploading file to WebDAV:', error)
|
||||
showMessage &&
|
||||
window.modal.error({
|
||||
@@ -100,7 +88,6 @@ export async function backupToWebdav({ showMessage = true }: { showMessage?: boo
|
||||
content: error.message
|
||||
})
|
||||
} finally {
|
||||
store.dispatch(setWebDAVSyncState({ syncing: false }))
|
||||
isManualBackupRunning = false
|
||||
}
|
||||
}
|
||||
@@ -138,9 +125,9 @@ export function startAutoSync() {
|
||||
return
|
||||
}
|
||||
|
||||
const { webdavAutoSync, webdavHost } = store.getState().settings
|
||||
const { webdavAutoSync, webdavHost, webdavSyncInterval } = store.getState().settings
|
||||
|
||||
if (!webdavAutoSync || !webdavHost) {
|
||||
if (!webdavAutoSync || !webdavHost || webdavSyncInterval <= 0) {
|
||||
console.log('[AutoSync] Invalid sync settings, auto sync disabled')
|
||||
return
|
||||
}
|
||||
@@ -157,16 +144,7 @@ export function startAutoSync() {
|
||||
syncTimeout = null
|
||||
}
|
||||
|
||||
const { webdavSyncInterval } = store.getState().settings
|
||||
|
||||
if (webdavSyncInterval <= 0) {
|
||||
console.log('[AutoSync] Invalid sync interval, auto sync disabled')
|
||||
stopAutoSync()
|
||||
return
|
||||
}
|
||||
|
||||
syncTimeout = setTimeout(performAutoBackup, webdavSyncInterval * 60 * 1000)
|
||||
|
||||
console.log(`[AutoSync] Next sync scheduled in ${webdavSyncInterval} minutes`)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,29 +57,20 @@ class FileManager {
|
||||
return file
|
||||
}
|
||||
|
||||
static async deleteFile(id: string, force: boolean = false): Promise<void> {
|
||||
static async deleteFile(id: string): Promise<void> {
|
||||
const file = await this.getFile(id)
|
||||
|
||||
console.debug('[FileManager] Deleting file:', file)
|
||||
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!force) {
|
||||
if (file.count > 1) {
|
||||
await db.files.update(id, { ...file, count: file.count - 1 })
|
||||
return
|
||||
}
|
||||
if (file.count > 1) {
|
||||
await db.files.update(id, { ...file, count: file.count - 1 })
|
||||
return
|
||||
}
|
||||
|
||||
await db.files.delete(id)
|
||||
|
||||
try {
|
||||
await window.api.file.delete(id + file.ext)
|
||||
} catch (error) {
|
||||
console.error('[FileManager] Failed to delete file:', error)
|
||||
}
|
||||
await window.api.file.delete(id + file.ext)
|
||||
}
|
||||
|
||||
static async deleteFiles(files: FileType[]): Promise<void> {
|
||||
@@ -102,14 +93,6 @@ class FileManager {
|
||||
const filesPath = store.getState().runtime.filesPath
|
||||
return 'file://' + filesPath + '/' + file.name
|
||||
}
|
||||
|
||||
static async updateFile(file: FileType) {
|
||||
if (!file.origin_name.includes(file.ext)) {
|
||||
file.origin_name = file.origin_name + file.ext
|
||||
}
|
||||
|
||||
await db.files.update(file.id, file)
|
||||
}
|
||||
}
|
||||
|
||||
export default FileManager
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import AiProvider from '@renderer/providers/AiProvider'
|
||||
import { FileType, KnowledgeBase, KnowledgeBaseParams, Message } from '@renderer/types'
|
||||
import { take } from 'lodash'
|
||||
import { isEmpty, take } from 'lodash'
|
||||
|
||||
import { getProviderByModel } from './AssistantService'
|
||||
import FileManager from './FileManager'
|
||||
@@ -10,6 +10,10 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
||||
const provider = getProviderByModel(base.model)
|
||||
const aiProvider = new AiProvider(provider)
|
||||
|
||||
if (provider.id === 'ollama' && isEmpty(provider.apiKey)) {
|
||||
provider.apiKey = 'empty'
|
||||
}
|
||||
|
||||
let host = aiProvider.getBaseURL()
|
||||
|
||||
if (provider.type === 'gemini') {
|
||||
@@ -20,7 +24,7 @@ export const getKnowledgeBaseParams = (base: KnowledgeBase): KnowledgeBaseParams
|
||||
id: base.id,
|
||||
model: base.model.id,
|
||||
dimensions: base.dimensions,
|
||||
apiKey: aiProvider.getApiKey() || 'secret',
|
||||
apiKey: aiProvider.getApiKey(),
|
||||
apiVersion: provider.apiVersion,
|
||||
baseURL: host
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
|
||||
export function getProviderName(id: string) {
|
||||
const provider = store.getState().llm.providers.find((p) => p.id === id)
|
||||
if (!provider) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (provider.isSystem) {
|
||||
return i18n.t(`provider.${provider.id}`, { defaultValue: provider.name })
|
||||
}
|
||||
|
||||
return provider?.name
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 54,
|
||||
version: 51,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -33,16 +33,6 @@ const initialState: LlmState = {
|
||||
isSystem: true,
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'aihubmix',
|
||||
name: 'AiHubMix',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aihubmix.com',
|
||||
models: SYSTEM_MODELS.aihubmix,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'ollama',
|
||||
name: 'Ollama',
|
||||
@@ -323,6 +313,16 @@ const initialState: LlmState = {
|
||||
models: SYSTEM_MODELS.jina,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
},
|
||||
{
|
||||
id: 'aihubmix',
|
||||
name: 'AiHubMix',
|
||||
type: 'openai',
|
||||
apiKey: '',
|
||||
apiHost: 'https://aihubmix.com',
|
||||
models: SYSTEM_MODELS.aihubmix,
|
||||
isSystem: true,
|
||||
enabled: false
|
||||
}
|
||||
],
|
||||
settings: {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { isEmpty } from 'lodash'
|
||||
import { createMigrate } from 'redux-persist'
|
||||
|
||||
import { RootState } from '.'
|
||||
import { DEFAULT_SIDEBAR_ICONS } from './settings'
|
||||
|
||||
const migrateConfig = {
|
||||
'2': (state: RootState) => {
|
||||
@@ -743,6 +742,8 @@ const migrateConfig = {
|
||||
return state
|
||||
},
|
||||
'49': (state: RootState) => {
|
||||
state.settings.showMinappIcon = true
|
||||
state.settings.showFilesIcon = true
|
||||
state.settings.pasteLongTextThreshold = 1500
|
||||
if (state.shortcuts) {
|
||||
state.shortcuts.shortcuts = [
|
||||
@@ -774,22 +775,6 @@ const migrateConfig = {
|
||||
'51': (state: RootState) => {
|
||||
state.settings.topicNamingPrompt = ''
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||