Compare commits

..

1 Commits

Author SHA1 Message Date
kangfenmao
ebf61b1ce9 feat: plugins 2024-12-30 23:45:47 +08:00
132 changed files with 3111 additions and 5587 deletions

View File

@@ -78,9 +78,19 @@ jobs:
run: node scripts/replace-spaces.js
- name: Release
uses: ncipollo/release-action@v1
uses: softprops/action-gh-release@v2
with:
draft: true
allowUpdates: true
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/*.blockmap'
token: ${{ secrets.GH_TOKEN }}
files: |
dist/*.exe
dist/*.zip
dist/*.dmg
dist/*.AppImage
dist/*.snap
dist/*.deb
dist/*.rpm
dist/*.tar.gz
dist/latest*.yml
dist/*.blockmap
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}

View File

@@ -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,

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -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,9 +78,4 @@ afterPack: scripts/after-pack.js
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
支持将小程序固定到侧边栏 @hxp0618
增加 Grok 和 QwenLM 小程序 @ruiwarn
支持下载模型生成的 CSV 文件
知识库增加刷新按钮
Gemini 搜索增加引用来源
修复模型设置参数无法保存的问题
增加 Genspark 小程序

View File

@@ -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']
}
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "0.9.8",
"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",

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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()
}
})
}

View File

@@ -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>
}
}
}

View File

@@ -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)
}
}

View File

@@ -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';
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -42,9 +42,6 @@
--color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333;
--color-group-background: var(--color-background-soft);
--color-reference: #404040;
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
--navbar-background-mac: rgba(30, 30, 30, 0.6);
@@ -63,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'] {
@@ -105,9 +100,6 @@ body[theme-mode='light'] {
--color-active: var(--color-white-soft);
--color-frame-border: #ddd;
--color-group-background: var(--color-white);
--color-reference: #cfe1ff;
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
--navbar-background-mac: rgba(255, 255, 255, 0.6);
@@ -177,9 +169,12 @@ body,
#content-container {
background-color: var(--color-background);
border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px;
}
#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 {
@@ -221,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 {

View File

@@ -208,14 +208,6 @@
sup {
top: -0.5em;
border-radius: 50%;
background-color: var(--color-reference);
color: var(--color-reference-text);
padding: 2px 5px;
zoom: 0.8;
& > span.link {
color: var(--color-reference-text);
}
}
sub {
@@ -234,55 +226,51 @@
text-decoration: underline;
}
}
}
.footnotes {
margin-top: 1em;
margin-bottom: 1em;
padding-top: 1em;
.footnotes {
margin-top: 1em;
margin-bottom: 1em;
padding-top: 1em;
background-color: var(--color-reference-background);
border-radius: 8px;
padding: 8px 12px;
background-color: var(--color-reference-background);
border-radius: 8px;
padding: 8px 12px;
h4 {
margin-bottom: 5px;
font-size: 12px;
}
a {
color: var(--color-link);
}
ol {
padding-left: 1em;
margin: 0;
li:last-child {
margin-bottom: 0;
h4 {
margin-bottom: 5px;
font-size: 12px;
}
}
li {
font-size: 0.9em;
margin-bottom: 0.5em;
color: var(--color-text-light);
p {
display: inline;
ol {
padding-left: 1em;
margin: 0;
li:last-child {
margin-bottom: 0;
}
}
}
.footnote-backref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
li {
font-size: 0.9em;
margin-bottom: 0.5em;
color: var(--color-text-light);
&:hover {
text-decoration: underline;
p {
display: inline;
margin: 0;
}
}
.footnote-backref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}

View File

@@ -47,22 +47,19 @@ const DragableList: FC<Props<any>> = ({
<Droppable droppableId="droppable" {...droppableProps}>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
{list.map((item, index) => {
const id = item.id || item
return (
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
{children(item, index)}
</div>
)}
</Draggable>
)
})}
{list.map((item, index) => (
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
{children(item, index)}
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>

View File

@@ -1,39 +0,0 @@
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { MinAppType } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
app: MinAppType
size?: number
style?: React.CSSProperties
}
const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
if (!_app) {
return null
}
return (
<Container
src={_app.logo}
style={{
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`,
backgroundColor: _app.background,
...style
}}
/>
)
}
const Container = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
export default MinAppIcon

View File

@@ -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;

View File

@@ -5,7 +5,6 @@ import { useBridge } from '@renderer/hooks/useBridge'
import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer } from 'antd'
import { WebviewTag } from 'electron'
import { useEffect, useRef, useState } from 'react'
@@ -29,10 +28,9 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
const onClose = async (_delay = 0.3) => {
const onClose = () => {
setOpen(false)
await delay(_delay)
resolve({})
setTimeout(() => resolve({}), 300)
}
MinApp.onClose = onClose
@@ -60,7 +58,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<ExportOutlined />
</Button>
)}
<Button onClick={() => onClose()}>
<Button onClick={onClose}>
<CloseOutlined />
</Button>
</ButtonsGroup>
@@ -101,7 +99,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<Drawer
title={<Title />}
placement="bottom"
onClose={() => onClose()}
onClose={onClose}
open={open}
mask={true}
rootClassName="minapp-drawer"
@@ -204,22 +202,12 @@ const EmptyView = styled.div`
export default class MinApp {
static topviewId = 0
static onClose = () => {}
static app: MinAppType | null = null
static async start(app: MinAppType) {
if (MinApp.app?.id === app.id) {
return
}
if (MinApp.app) {
// @ts-ignore delay params
await MinApp.onClose(0)
await delay(0)
}
MinApp.app = app
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
}
static start(app: MinAppType) {
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
@@ -233,10 +221,4 @@ export default class MinApp {
)
})
}
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
MinApp.app = null
}
}

View File

@@ -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

View File

@@ -1,5 +1,5 @@
import { Center } from '@renderer/components/Layout'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { getAllMinApps } from '@renderer/config/minapps'
import App from '@renderer/pages/apps/App'
import { Popover } from 'antd'
import { Empty } from 'antd'
@@ -14,9 +14,9 @@ interface Props {
children: React.ReactNode
}
const MinAppsPopover: FC<Props> = ({ children }) => {
const AppStorePopover: FC<Props> = ({ children }) => {
const [open, setOpen] = useState(false)
const { minapps } = useMinapps()
const apps = getAllMinApps()
useHotkeys('esc', () => {
setOpen(false)
@@ -29,10 +29,10 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
const content = (
<PopoverContent>
<AppsContainer>
{minapps.map((app) => (
{apps.map((app) => (
<App key={app.id} app={app} onClick={handleClose} size={50} />
))}
{isEmpty(minapps) && (
{isEmpty(apps) && (
<Center>
<Empty />
</Center>
@@ -48,7 +48,7 @@ const MinAppsPopover: 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 MinAppsPopover
export default AppStorePopover

View File

@@ -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) => {

View File

@@ -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}

View File

@@ -55,7 +55,7 @@ const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 ${isMac ? '20px' : 0};
padding: 0 ${isMac ? '20px' : '15px'};
font-weight: bold;
color: var(--color-text-1);
`

View File

@@ -3,20 +3,15 @@ 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 { useMinapps } from '@renderer/hooks/useMinapps'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import type { MenuProps } from 'antd'
import { Tooltip } from 'antd'
import { Avatar } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon'
import MinApp from '../MinApp'
import UserPopup from '../Popups/UserPopup'
@@ -24,21 +19,25 @@ 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 { pinned } = useMinapps()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const onEditUser = () => UserPopup.show()
const macTransparentWindow = isMac && windowStyle === 'transparent'
const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)'
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
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)
}
@@ -50,19 +49,63 @@ const Sidebar: FC = () => {
zIndex: minappShow ? 10000 : 'initial'
}}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenusContainer>
<MainMenus>
<Menus onClick={MinApp.onClose}>
<MainMenus />
<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>
{showPinnedApps && (
<AppsContainer>
<Divider />
<Menus>
<PinnedApps />
</Menus>
</AppsContainer>
)}
</MainMenusContainer>
</MainMenus>
<Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={() => toggleTheme()}>
@@ -85,82 +128,6 @@ const Sidebar: FC = () => {
)
}
const MainMenus: FC = () => {
const { t } = useTranslation()
const { pathname } = useLocation()
const { sidebarIcons } = useSettings()
const navigate = useNavigate()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
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'
}
return sidebarIcons.visible.map((icon) => {
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={() => navigate(path)}>
<Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink>
</Tooltip>
)
})
}
const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
return (
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
{(app) => {
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: t('minapp.sidebar.remove.title'),
onClick: () => {
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
}
}
]
return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Icon onClick={() => MinApp.start(app)}>
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
}}
</DragableList>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
@@ -182,18 +149,15 @@ const AvatarImg = styled(Avatar)`
border: none;
cursor: pointer;
`
const MainMenusContainer = styled.div`
const MainMenus = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
`
const Menus = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
`
const Icon = styled.div`
@@ -203,6 +167,7 @@ const Icon = styled.div`
justify-content: center;
align-items: center;
border-radius: 50%;
margin-bottom: 5px;
-webkit-app-region: none;
border: 0.5px solid transparent;
.iconfont,
@@ -240,24 +205,4 @@ const StyledLink = styled.div`
}
`
const AppsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
overflow-y: auto;
overflow-x: hidden;
margin-bottom: 10px;
-webkit-app-region: none;
&::-webkit-scrollbar {
display: none;
}
`
const Divider = styled.div`
width: 50%;
margin: 8px 0;
border-bottom: 0.5px solid var(--color-border);
`
export default Sidebar

View File

@@ -8,8 +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 GrokAppLogo from '@renderer/assets/images/apps/grok.png'
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'
@@ -17,7 +15,6 @@ import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp'
@@ -36,7 +33,7 @@ import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.p
import MinApp from '@renderer/components/MinApp'
import { MinAppType } from '@renderer/types'
export const DEFAULT_MIN_APPS: MinAppType[] = [
const _apps: MinAppType[] = [
{
id: 'openai',
name: 'ChatGPT',
@@ -229,13 +226,6 @@ export const DEFAULT_MIN_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',
@@ -247,23 +237,14 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
name: 'Genspark',
logo: GensparkLogo,
url: 'https://www.genspark.ai/'
},
{
id: 'grok',
name: 'Grok',
logo: GrokAppLogo,
url: 'https://x.com/i/grok',
bodered: true
},
{
id: 'qwenlm',
name: 'QwenLM',
logo: QwenlmAppLogo,
url: 'https://qwenlm.ai/'
}
]
export function getAllMinApps() {
return _apps as MinAppType[]
}
export function startMinAppById(id: string) {
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
const app = getAllMinApps().find((app) => app?.id === id)
app && MinApp.start(app)
}

View File

@@ -125,8 +125,6 @@ import { getProviderByModel } from '@renderer/services/AssistantService'
import { Model } from '@renderer/types'
import OpenAI from 'openai'
import { getWebSearchTools } from './tools'
const visionAllowedModels = [
'llava',
'moondream',
@@ -264,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: [
{
@@ -314,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',
@@ -561,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'
},
{
@@ -602,12 +550,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
name: 'GLM-4-Flash',
group: 'GLM-4'
},
{
id: 'glm-4-flashx',
provider: 'zhipu',
name: 'GLM-4-FlashX',
group: 'GLM-4'
},
{
id: 'glm-4v',
provider: 'zhipu',
@@ -625,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: [
@@ -814,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',
@@ -1011,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)
}
@@ -1055,47 +1000,9 @@ export function isWebSearchModel(model: Model): boolean {
const provider = getProviderByModel(model)
if (provider.type === 'openai') {
if (model?.id?.includes('gemini-2.0-flash-exp')) {
return true
}
}
if (!provider) {
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 getOpenAIWebSearchParams(model: Model): Record<string, any> {
if (isWebSearchModel(model)) {
const webSearchTools = getWebSearchTools(model)
if (model.provider === 'hunyuan') {
return { enable_enhancement: true }
}
return {
tools: webSearchTools
}
}
return {}
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp'
}

View File

@@ -1,32 +0,0 @@
import { Model } from '@renderer/types'
import { ChatCompletionTool } from 'openai/resources'
export function getWebSearchTools(model: Model): ChatCompletionTool[] {
if (model?.provider === 'zhipu') {
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 [
{
type: 'function',
function: {
name: 'googleSearch'
}
}
]
}

View File

@@ -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) => ({ '<': '&lt;', '>': '&gt;' })[char]!)
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '&lt;', '>': '&gt;' })[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>`
}

View File

@@ -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')
}, [])

View File

@@ -15,7 +15,6 @@ import {
renameBase,
updateBase,
updateBases,
updateItem as updateItemAction,
updateItemProcessingStatus,
updateNotes
} from '@renderer/store/knowledge'
@@ -118,8 +117,7 @@ export const useKnowledge = (baseId: string) => {
await db.knowledge_notes.put(updatedNote)
dispatch(updateNotes({ baseId, item: updatedNote }))
}
const noteItem = base?.items.find((item) => item.id === noteId)
noteItem && refreshItem(noteItem)
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
// 获取笔记内容
@@ -127,10 +125,6 @@ export const useKnowledge = (baseId: string) => {
return await db.knowledge_notes.get(noteId)
}
const updateItem = (item: KnowledgeItem) => {
dispatch(updateItemAction({ baseId, item }))
}
// 移除项目
const removeItem = async (item: KnowledgeItem) => {
dispatch(removeItemAction({ baseId, item }))
@@ -144,27 +138,6 @@ export const useKnowledge = (baseId: string) => {
}
}
// 刷新项目
const refreshItem = async (item: KnowledgeItem) => {
const status = getProcessingStatus(item.id)
if (status === 'pending' || status === 'processing') {
return
}
if (base && item.uniqueId) {
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, base: getKnowledgeBaseParams(base) })
updateItem({
...item,
processingStatus: 'pending',
processingProgress: 0,
processingError: '',
uniqueId: undefined
})
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
}
}
// 更新处理状态
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
dispatch(
@@ -265,9 +238,7 @@ export const useKnowledge = (baseId: string) => {
addNote,
updateNoteContent,
getNoteContent,
updateItem,
updateItemStatus,
refreshItem,
getProcessingStatus,
getProcessingItemsByType,
clearCompleted,

View File

@@ -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)
}, [])
}

View File

@@ -1,23 +0,0 @@
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
import { MinAppType } from '@renderer/types'
export const useMinapps = () => {
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
const dispatch = useAppDispatch()
return {
minapps: enabled,
disabled,
pinned,
updateMinapps: (minapps: MinAppType[]) => {
dispatch(setMinApps(minapps))
},
updateDisabledMinapps: (minapps: MinAppType[]) => {
dispatch(setDisabledMinApps(minapps))
},
updatePinnedMinapps: (minapps: MinAppType[]) => {
dispatch(setPinnedMinApps(minapps))
}
}
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -2,14 +2,13 @@ import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import {
SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut,
setSidebarIcons,
setTheme,
SettingsState,
setTopicPosition,
setTray,
setWindowStyle
} from '@renderer/store/settings'
import { SidebarIcon, ThemeMode } from '@renderer/types'
import { ThemeMode } from '@renderer/types'
export function useSettings() {
const settings = useAppSelector((state) => state.settings)
@@ -31,15 +30,6 @@ export function useSettings() {
},
setTopicPosition(topicPosition: 'left' | 'right') {
dispatch(setTopicPosition(topicPosition))
},
updateSidebarIcons(icons: { visible: SidebarIcon[]; disabled: SidebarIcon[] }) {
dispatch(setSidebarIcons(icons))
},
updateSidebarVisibleIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ visible: icons }))
},
updateSidebarDisabledIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ disabled: icons }))
}
}
}

View File

@@ -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"
@@ -231,7 +221,6 @@
"message": {
"api.connection.failed": "Connection failed",
"api.connection.success": "Connection successful",
"api.check.model.title": "Select the model to use for detection",
"assistant.added.content": "Assistant added successfully",
"backup.failed": "Backup failed",
"backup.success": "Backup successful",
@@ -255,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",
@@ -265,9 +254,7 @@
"error.get_embedding_dimensions": "Failed to get embedding dimensions"
},
"minapp": {
"title": "MinApp",
"sidebar.add.title": "Add to sidebar",
"sidebar.remove.title": "Remove from sidebar"
"title": "MinApp"
},
"ollama": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
@@ -292,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",
@@ -373,16 +358,11 @@
"webdav.password": "WebDAV Password",
"webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "Auto Backup",
"webdav.autoSync": "Auto Sync",
"webdav.minutes": "Minutes",
"webdav.restore.button": "Restore from WebDAV",
"webdav.title": "WebDAV",
"webdav.user": "WebDAV User",
"webdav.syncStatus": "Backup Status",
"webdav.autoSync.off": "Off",
"webdav.noSync": "Waiting for next backup",
"webdav.syncError": "Backup Error",
"webdav.lastSync": "Last Backup"
"webdav.user": "WebDAV User"
},
"display.title": "Display Settings",
"font_size.title": "Message font size",
@@ -398,21 +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 icons",
"display.sidebar.disabled": "Hide 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.minApp.title": "MinApp Settings",
"display.minApp.visible": "Visible MinApp",
"display.minApp.disabled": "Hidden MinApp",
"display.minApp.empty": "Drag minApp from the left to hide them here",
"": "MinApp that have been added to the sidebar do not support hiding. If you want to hide them, please remove them from the sidebar first.",
"display.topic.title": "Topic Settings",
"display.custom.css": "Custom CSS",
"display.custom.css.placeholder": "/* Put custom CSS here */",
@@ -423,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",
@@ -517,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",
@@ -559,7 +526,7 @@
"show_window": "Show Window",
"quit": "Quit"
},
"knowledge": {
"knowledge_base": {
"title": "Knowledge Base",
"search": "Search knowledge base",
"empty": "No knowledge base found",
@@ -601,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": {
@@ -628,8 +594,7 @@
"parameter_type": {
"string": "Text",
"number": "Number",
"boolean": "Boolean",
"json": "JSON"
"boolean": "Boolean"
}
},
"prompts": {

View File

@@ -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": "ソース"
@@ -231,7 +221,6 @@
"message": {
"api.connection.failed": "接続に失敗しました",
"api.connection.success": "接続に成功しました",
"api.check.model.title": "検出に使用するモデルを選択してください",
"assistant.added.content": "アシスタントが追加されました",
"backup.failed": "バックアップに失敗しました",
"backup.success": "バックアップに成功しました",
@@ -254,7 +243,7 @@
"reset.double.confirm.title": "データが失われます!!!",
"restore.success": "復元に成功しました",
"save.success.title": "保存に成功しました",
"switch.disabled": "現在の応答が完了するまで切り替え無効にします",
"switch.disabled": "アシスタントが生成中は切り替え無効す",
"topic.added": "新しいトピックが追加されました",
"upgrade.success.button": "再起動",
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
@@ -263,9 +252,7 @@
"copy.success": "コピーしました!"
},
"minapp": {
"title": "ミニアプリ",
"sidebar.add.title": "サイドバーに追加",
"sidebar.remove.title": "サイドバーから削除"
"title": "ミニアプリ"
},
"ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",
@@ -290,9 +277,7 @@
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
"seed": "シード",
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
"title": "画像",
"prompt_enhancement": "プロンプト強化",
"prompt_enhancement_tip": "オンにすると、プロンプトを詳細でモデルに適したバージョンに書き直します"
"title": "画像"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -371,16 +356,11 @@
"webdav.password": "WebDAVパスワード",
"webdav.path": "WebDAVパス",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動バックアップ",
"webdav.autoSync": "自動同期",
"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,23 +376,12 @@
"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を入力 */",
"display.minApp.title": "ミニプログラム表示設定",
"display.minApp.visible": "表示中ミニプログラム",
"display.minApp.disabled": "非表示ミニプログラム",
"display.minApp.empty": "非表示にしたいアプレットを左からここまでドラッグします",
"input.auto_translate_with_space": "スペースを3回押して翻訳",
"messages.divider": "メッセージ間に区切り線を表示",
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
@@ -502,8 +471,7 @@
"clear_shortcut": "ショートカットをクリア",
"toggle_show_assistants": "アシスタントの表示を切り替え",
"toggle_show_topics": "トピックの表示を切り替え",
"copy_last_message": "最後のメッセージをコピー",
"search_message": "メッセージを検索"
"copy_last_message": "最後のメッセージをコピー"
},
"theme.auto": "自動",
"theme.dark": "ダークテーマ",
@@ -544,7 +512,7 @@
"show_window": "ウィンドウを表示",
"quit": "終了"
},
"knowledge": {
"knowledge_base": {
"title": "ナレッジベース",
"search": "ナレッジベースを検索",
"empty": "ナレッジベースが見つかりません",
@@ -583,11 +551,7 @@
"sitemap_placeholder": "サイトマップURLを入力",
"directories": "ディレクトリ",
"add_directory": "ディレクトリを追加",
"directory_placeholder": "ディレクトリパスを入力",
"model_info": "モデル情報",
"not_support": "ナレッジベースデータベースエンジンが更新されました。このナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"no_provider": "ナレッジベースモデルプロバイダーが設定されていません。ナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
"source": "ソース"
"directory_placeholder": "ディレクトリパスを入力"
},
"models": {
"pinned": "固定済み",
@@ -613,8 +577,7 @@
"parameter_type": {
"string": "テキスト",
"number": "数値",
"boolean": "真偽値",
"json": "JSON"
"boolean": "真偽値"
}
},
"prompts": {

View File

@@ -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": "Исходный код"
@@ -231,7 +221,6 @@
"message": {
"api.connection.failed": "Соединение не удалось",
"api.connection.success": "Соединение успешно",
"api.check.model.title": "Выберите модель для проверки",
"assistant.added.content": "Ассистент успешно добавлен",
"backup.failed": "Создание резервной копии не удалось",
"backup.success": "Резервная копия успешно создана",
@@ -255,7 +244,7 @@
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
"restore.success": "Успешно восстановлено",
"save.success.title": "Успешно сохранено",
"switch.disabled": ожалуйста, дождитесь завершения текущего ответа",
"switch.disabled": ереключение отключено, пока ассистент генерирует",
"topic.added": "Новый топик добавлен",
"upgrade.success.button": "Перезапустить",
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
@@ -265,9 +254,7 @@
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания"
},
"minapp": {
"title": "Встроенные приложения",
"sidebar.add.title": "Добавить в боковую панель",
"sidebar.remove.title": "Удалить из боковой панели"
"title": "Встроенные приложения"
},
"ollama": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
@@ -292,9 +279,7 @@
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
"seed": "Ключ генерации",
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
"title": "Изображения",
"prompt_enhancement": "Улучшение промпта",
"prompt_enhancement_tip": "При включении переписывает промпт в более детальную, модель-ориентированную версию"
"title": "Изображения"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -373,16 +358,11 @@
"webdav.password": "Пароль WebDAV",
"webdav.path": "Путь WebDAV",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "Автоматическое резервное копирование",
"webdav.autoSync": "Автоматическая синхронизация",
"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": "Размер шрифта сообщений",
@@ -398,20 +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.minApp.title": "Настройки отображения мини программы",
"display.minApp.visible": "Отображаемый апплет",
"display.minApp.disabled": "скрытый апплет",
"display.minApp.empty": "Перетащите апплет, который хотите скрыть, слева сюда",
"display.topic.title": "Настройки топиков",
"display.custom.css": "Пользовательский CSS",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
@@ -516,8 +485,7 @@
"clear_shortcut": "Очистить сочетание клавиш",
"toggle_show_assistants": "Переключить отображение ассистентов",
"toggle_show_topics": "Переключить отображение топиков",
"copy_last_message": "Копировать последнее сообщение",
"search_message": "Поиск сообщения"
"copy_last_message": "Копировать последнее сообщение"
},
"theme.auto": "Автоматически",
"theme.dark": "Темная",
@@ -558,7 +526,7 @@
"show_window": "Показать окно",
"quit": "Выйти"
},
"knowledge": {
"knowledge_base": {
"title": "База знаний",
"search": "Поиск в базе знаний",
"empty": "База знаний не найдена",
@@ -600,7 +568,6 @@
"directory_placeholder": "Введите путь к директории",
"model_info": "Модель информации",
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"no_provider": "База знаний модель поставщика не настроена, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
"source": "Источник"
},
"models": {
@@ -627,8 +594,7 @@
"parameter_type": {
"string": "Текст",
"number": "Число",
"boolean": "Логическое",
"json": "JSON"
"boolean": "Логическое"
}
},
"prompts": {

View File

@@ -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": "源码"
@@ -232,7 +222,6 @@
"message": {
"api.connection.failed": "连接失败",
"api.connection.success": "连接成功",
"api.check.model.title": "请选择要检测的模型",
"assistant.added.content": "智能体添加成功",
"backup.failed": "备份失败",
"backup.success": "备份成功",
@@ -256,7 +245,7 @@
"reset.double.confirm.title": "数据丢失!!!",
"restore.success": "恢复成功",
"save.success.title": "保存成功",
"switch.disabled": "请等待当前回复完成后操作",
"switch.disabled": "模型回复完成后才能切换",
"topic.added": "话题添加成功",
"upgrade.success.button": "重启",
"upgrade.success.content": "重启用以完成升级",
@@ -266,9 +255,7 @@
"error.get_embedding_dimensions": "获取嵌入维度失败"
},
"minapp": {
"title": "小程序",
"sidebar.add.title": "添加到侧边栏",
"sidebar.remove.title": "从侧边栏移除"
"title": "小程序"
},
"ollama": {
"keep_alive_time.description": "对话后模型在内存中保持的时间默认5分钟",
@@ -293,9 +280,7 @@
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
"seed": "随机种子",
"seed_tip": "相同的种子和提示词可以生成相似的图片",
"title": "图片",
"prompt_enhancement": "提示词增强",
"prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本"
"title": "图片"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -374,16 +359,11 @@
"webdav.password": "WebDAV 密码",
"webdav.path": "WebDAV 路径",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "自动备份",
"webdav.autoSync": "自动同步",
"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": "消息字体大小",
@@ -399,20 +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.minApp.title": "小程序显示设置",
"display.minApp.visible": "显示的小程序",
"display.minApp.disabled": "隐藏的小程序",
"display.minApp.empty": "把要隐藏的小程序从左侧拖拽到这里",
"display.topic.title": "话题设置",
"display.custom.css": "自定义 CSS",
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
@@ -505,8 +474,7 @@
"clear_shortcut": "清除快捷键",
"toggle_show_assistants": "切换助手显示",
"toggle_show_topics": "切换话题显示",
"copy_last_message": "复制上一条消息",
"search_message": "搜索消息"
"copy_last_message": "复制上一条消息"
},
"theme.auto": "跟随系统",
"theme.dark": "深色主题",
@@ -547,7 +515,7 @@
"show_window": "显示窗口",
"quit": "退出"
},
"knowledge": {
"knowledge_base": {
"title": "知识库",
"search": "搜索知识库",
"empty": "暂无知识库",
@@ -589,7 +557,6 @@
"directory_placeholder": "请输入目录路径",
"model_info": "模型信息",
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
"no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库",
"source": "来源"
},
"models": {
@@ -616,8 +583,7 @@
"parameter_type": {
"string": "文本",
"number": "数字",
"boolean": "布尔值",
"json": "JSON"
"boolean": "布尔值"
}
},
"prompts": {

View File

@@ -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": "原始碼"
@@ -231,7 +221,6 @@
"message": {
"api.connection.failed": "連接失敗",
"api.connection.success": "連接成功",
"api.check.model.title": "請選擇要檢測的模型",
"assistant.added.content": "智能體添加成功",
"backup.failed": "備份失敗",
"backup.success": "備份成功",
@@ -255,7 +244,7 @@
"reset.double.confirm.title": "資料將會丟失!!!",
"restore.success": "恢復成功",
"save.success.title": "保存成功",
"switch.disabled": "請等待當前回覆完成",
"switch.disabled": "助手生成回覆時無法切換",
"topic.added": "新話題已添加",
"upgrade.success.button": "重新啟動",
"upgrade.success.content": "請重新啟動應用以完成升級",
@@ -265,9 +254,7 @@
"error.get_embedding_dimensions": "獲取嵌入維度失敗"
},
"minapp": {
"title": "小程序",
"sidebar.add.title": "添加到側邊欄",
"sidebar.remove.title": "從側邊欄移除"
"title": "小程序"
},
"ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
@@ -292,9 +279,7 @@
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
"seed": "隨機種子",
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
"title": "繪圖",
"prompt_enhancement": "提示詞增強",
"prompt_enhancement_tip": "開啟後將提示重寫為詳細的、適合模型的版本"
"title": "繪圖"
},
"provider": {
"aihubmix": "AiHubMix",
@@ -373,16 +358,11 @@
"webdav.password": "WebDAV 密碼",
"webdav.path": "WebDAV Path",
"webdav.path.placeholder": "/backup",
"webdav.autoSync": "自動備份",
"webdav.autoSync": "自動同步",
"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": "訊息字體大小",
@@ -398,21 +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.minApp.title": "小程序顯示設定",
"display.minApp.visible": "顯示的小程序",
"display.minApp.disabled": "隱藏的小程序",
"display.minApp.empty": "把要隱藏的小程序從左側拖拽到這裡",
"display.custom.css": "自定義 CSS",
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
@@ -504,8 +473,7 @@
"clear_shortcut": "清除快捷鍵",
"toggle_show_assistants": "切換助手顯示",
"toggle_show_topics": "切換話題顯示",
"copy_last_message": "複製上一条消息",
"search_message": "搜索消息"
"copy_last_message": "複製上一条消息"
},
"theme.auto": "自動",
"theme.dark": "深色主題",
@@ -546,7 +514,7 @@
"show_window": "顯示視窗",
"quit": "退出"
},
"knowledge": {
"knowledge_base": {
"title": "知識庫",
"search": "搜尋知識庫",
"empty": "暫無知識庫",
@@ -588,7 +556,6 @@
"directory_placeholder": "請輸入目錄路徑",
"model_info": "模型信息",
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
"no_provider": "知識庫模型提供商遺失,該知識庫將不再支持,請重新創建知識庫",
"source": "來源"
},
"models": {
@@ -615,8 +582,7 @@
"parameter_type": {
"string": "文字",
"number": "數字",
"boolean": "布林值",
"json": "JSON"
"boolean": "布林值"
}
},
"prompts": {

View File

@@ -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()

View File

@@ -132,7 +132,7 @@ const AgentsPage: FC = () => {
key: id,
children: (
<TabContent key={group}>
<Title level={5} key={group} style={{ marginBottom: 10 }}>
<Title level={5} key={group} style={{ marginBottom: 16 }}>
{localizedGroupName}
</Title>
<Row gutter={[20, 20]}>
@@ -272,8 +272,8 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
padding-right: 0 !important;
}
.ant-tabs-nav {
min-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
max-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
min-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
max-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
}
.ant-tabs-nav-list {
padding: 10px 8px;
@@ -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;

View File

@@ -81,7 +81,7 @@ const Container = styled.div`
text-align: center;
gap: 10px;
background-color: var(--color-background);
border-radius: 10px;
border-radius: 16px;
position: relative;
overflow: hidden;
cursor: pointer;

View File

@@ -1,11 +1,6 @@
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
import MinApp from '@renderer/components/MinApp'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
@@ -15,36 +10,23 @@ interface Props {
}
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
const { t } = useTranslation()
const { minapps, pinned, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
const handleClick = () => {
MinApp.start(app)
onClick?.()
}
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
onClick: () => {
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
updatePinnedMinapps(newPinned)
}
}
]
if (!isVisible) return null
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleClick}>
<MinAppIcon size={size} app={app} />
<AppTitle>{app.name}</AppTitle>
</Container>
</Dropdown>
<Container onClick={handleClick}>
<AppIcon
src={app.logo}
style={{
border: app.bodered ? '0.5px solid var(--color-border)' : 'none',
width: `${size}px`,
height: `${size}px`
}}
/>
<AppTitle>{app.name}</AppTitle>
</Container>
)
}
@@ -57,6 +39,12 @@ const Container = styled.div`
overflow: hidden;
`
const AppIcon = styled.img`
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
const AppTitle = styled.div`
font-size: 12px;
margin-top: 5px;

View File

@@ -1,10 +1,10 @@
import { SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { Center } from '@renderer/components/Layout'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { getAllMinApps } from '@renderer/config/minapps'
import { Empty, Input } from 'antd'
import { isEmpty } from 'lodash'
import React, { FC, useState } from 'react'
import { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -13,29 +13,16 @@ import App from './App'
const AppsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState('')
const { minapps } = useMinapps()
console.debug('minapps', minapps)
const apps = useMemo(() => getAllMinApps(), [])
const filteredApps = search
? minapps.filter(
? apps.filter(
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
)
: minapps
// Calculate the required number of lines
const itemsPerRow = Math.floor(930 / 115) // Maximum width divided by the width of each item (including spacing)
const rowCount = Math.ceil(filteredApps.length / itemsPerRow)
// Each line height is 85px (60px icon + 5px margin + 12px text + spacing)
const containerHeight = rowCount * 85 + (rowCount - 1) * 25 // 25px is the line spacing.
// Disable right-click menu in blank area
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
}
: apps
return (
<Container onContextMenu={handleContextMenu}>
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('minapp.title')}
@@ -53,7 +40,7 @@ const AppsPage: FC = () => {
</NavbarCenter>
</Navbar>
<ContentContainer id="content-container">
<AppsContainer style={{ height: containerHeight }}>
<AppsContainer>
{filteredApps.map((app) => (
<App key={app.id} app={app} />
))}
@@ -81,18 +68,18 @@ const ContentContainer = styled.div`
flex-direction: row;
justify-content: center;
height: 100%;
overflow-y: auto;
overflow-y: scroll;
padding: 50px;
`
const AppsContainer = styled.div`
display: grid;
min-width: 0;
display: flex;
min-width: 930px;
max-width: 930px;
width: 100%;
grid-template-columns: repeat(auto-fill, 90px);
gap: 25px;
justify-content: center;
max-height: 500px;
display: grid;
grid-template-columns: repeat(8, minmax(90px, 1fr));
gap: 25px 25px;
`
export default AppsPage

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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

View File

@@ -1,8 +1,8 @@
import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons'
import { Message, Topic } from '@renderer/types'
import { Input, InputRef } from 'antd'
import { Input } from 'antd'
import { last } from 'lodash'
import { FC, useEffect, useRef, useState } from 'react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -25,7 +25,6 @@ const TopicsPage: FC = () => {
const [stack, setStack] = useState<Route[]>(_stack)
const [topic, setTopic] = useState<Topic | undefined>(_topic)
const [message, setMessage] = useState<Message | undefined>(_message)
const inputRef = useRef<InputRef>(null)
_search = search
_stack = stack
@@ -59,12 +58,6 @@ const TopicsPage: FC = () => {
const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none')
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [])
return (
<Container>
<Header>
@@ -79,9 +72,7 @@ const TopicsPage: FC = () => {
placeholder={t('history.search.placeholder')}
type="search"
value={search}
autoFocus
allowClear
ref={inputRef}
onChange={(e) => setSearch(e.target.value.trimStart())}
suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />}
onPressEnter={onSearch}

View File

@@ -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 && (

View File

@@ -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

View File

@@ -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>
)
}

View File

@@ -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)' }} />

View File

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

View File

@@ -1,7 +1,7 @@
import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons'
import MinApp from '@renderer/components/MinApp'
import { AppLogo } from '@renderer/config/env'
import { extractTitle } from '@renderer/utils/formats'
import { extractTitle } from '@renderer/utils/formula'
import { Button } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'

View File

@@ -1,9 +1,7 @@
import { CheckOutlined, DownloadOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'
import { CheckOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { HStack } from '@renderer/components/Layout'
import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import dayjs from 'dayjs'
import React, { memo, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -18,6 +16,28 @@ interface CodeBlockProps {
[key: string]: any
}
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
return (
<CollapseIconWrapper onClick={onClick}>
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</CollapseIconWrapper>
)
}
const ExpandButton: React.FC<{
isExpanded: boolean
onClick: () => void
showButton: boolean
}> = ({ isExpanded, onClick, showButton }) => {
if (!showButton) return null
return (
<ExpandButtonWrapper onClick={onClick}>
<div className="button-text">{isExpanded ? '收起' : '展开'}</div>
</ExpandButtonWrapper>
)
}
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const match = /language-(\w+)/.exec(className || '')
const { codeShowLineNumbers, fontSize, codeCollapsible } = useSettings()
@@ -30,8 +50,6 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
const showFooterCopyButton = children && children.length > 500 && !codeCollapsible
const showDownloadButton = ['csv', 'json', 'txt', 'md'].includes(language)
useEffect(() => {
const loadHighlightedCode = async () => {
const highlightedHtml = await codeToHtml(children, language)
@@ -83,10 +101,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
)}
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
</div>
<HStack gap={12} alignItems="center">
{showDownloadButton && <DownloadButton language={language} data={children} />}
<CopyButton text={children} />
</HStack>
<CopyButton text={children} />
</CodeHeader>
<CodeContent
ref={codeContentRef}
@@ -122,28 +137,6 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
)
}
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
return (
<CollapseIconWrapper onClick={onClick}>
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
</CollapseIconWrapper>
)
}
const ExpandButton: React.FC<{
isExpanded: boolean
onClick: () => void
showButton: boolean
}> = ({ isExpanded, onClick, showButton }) => {
if (!showButton) return null
return (
<ExpandButtonWrapper onClick={onClick}>
<div className="button-text">{isExpanded ? '收起' : '展开'}</div>
</ExpandButtonWrapper>
)
}
const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => {
const [copied, setCopied] = useState(false)
const { t } = useTranslation()
@@ -162,19 +155,6 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
)
}
const DownloadButton = ({ language, data }: { language: string; data: string }) => {
const onDownload = () => {
const fileName = `${dayjs().format('YYYYMMDDHHmm')}.${language}`
window.api.file.save(fileName, data)
}
return (
<DownloadWrapper onClick={onDownload}>
<DownloadOutlined />
</DownloadWrapper>
)
}
const CodeBlockWrapper = styled.div``
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
@@ -284,18 +264,4 @@ const CollapseIconWrapper = styled.div`
}
`
const DownloadWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--color-text-3);
transition: color 0.3s;
font-size: 16px;
&:hover {
color: var(--color-text-1);
}
`
export default memo(CodeBlock)

View File

@@ -2,7 +2,7 @@ import 'katex/dist/katex.min.css'
import { useSettings } from '@renderer/hooks/useSettings'
import { Message } from '@renderer/types'
import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formula'
import { isEmpty } from 'lodash'
import { FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -34,9 +34,9 @@ const Markdown: FC<Props> = ({ message }) => {
const messageContent = useMemo(() => {
const empty = isEmpty(message.content)
const paused = message.status === 'paused'
const content = empty && paused ? t('message.chat.completion.paused') : withGeminiGrounding(message)
const content = empty && paused ? t('message.chat.completion.paused') : message.content
return removeSvgEmptyLines(escapeBrackets(content))
}, [message, t])
}, [message.content, message.status, t])
const rehypePlugins = useMemo(() => {
const hasElements = ALLOWED_ELEMENTS.test(messageContent)

View File

@@ -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>
</>

View File

@@ -10,7 +10,6 @@ import styled from 'styled-components'
import Markdown from '../Markdown/Markdown'
import MessageAttachments from './MessageAttachments'
import MessageError from './MessageError'
import MessageSearchResults from './MessageSearchResults'
const MessageContent: React.FC<{
message: Message
@@ -51,7 +50,6 @@ const MessageContent: React.FC<{
</>
)}
<MessageAttachments message={message} />
<MessageSearchResults message={message} />
</>
)
}

View File

@@ -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
}}

View File

@@ -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;
}
`

View File

@@ -1,96 +0,0 @@
import { InfoCircleOutlined } from '@ant-design/icons'
import { Message } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
message: Message
}
const MessageSearchResults: FC<Props> = ({ message }) => {
const { t } = useTranslation()
if (!message.metadata?.groundingMetadata) {
return null
}
const { groundingChunks, searchEntryPoint } = message.metadata.groundingMetadata
if (!groundingChunks) {
return null
}
let searchEntryContent = searchEntryPoint?.renderedContent
searchEntryContent = searchEntryContent?.replace(
/@media \(prefers-color-scheme: light\)/g,
'body[theme-mode="light"]'
)
searchEntryContent = searchEntryContent?.replace(/@media \(prefers-color-scheme: dark\)/g, 'body[theme-mode="dark"]')
return (
<>
<Container className="footnotes">
<TitleRow>
<Title>{t('common.footnotes')}</Title>
<InfoCircleOutlined />
</TitleRow>
<Sources>
{groundingChunks.map((chunk, index) => (
<SourceItem key={index}>
<Link href={chunk.web?.uri} target="_blank" rel="noopener noreferrer">
{chunk.web?.title}
</Link>
</SourceItem>
))}
</Sources>
</Container>
<SearchEntryPoint dangerouslySetInnerHTML={{ __html: searchEntryContent || '' }} />
</>
)
}
const Container = styled.div`
padding: 16px;
border-radius: 8px;
margin-bottom: 0;
`
const TitleRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
margin-bottom: 10px;
`
const Title = styled.h4`
margin: 0 !important;
`
const Sources = styled.ol`
margin-top: 10px;
`
const SourceItem = styled.li`
margin-bottom: 5px;
`
const Link = styled.a`
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
`
const SearchEntryPoint = styled.div`
margin-top: 10px;
margin-bottom: 10px;
`
export default MessageSearchResults

View File

@@ -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>
)
}

View File

@@ -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

View File

@@ -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);

View File

@@ -1,7 +1,7 @@
import { FormOutlined, SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import AppStorePopover from '@renderer/components/Popups/AppStorePopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac, isWindows } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
@@ -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') && (
<MinAppsPopover>
<NarrowIcon>
<i className="iconfont icon-appstore" />
</NarrowIcon>
</MinAppsPopover>
)}
</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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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;

View File

@@ -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}

View File

@@ -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;
`

View File

@@ -6,7 +6,6 @@ import {
GlobalOutlined,
LinkOutlined,
PlusOutlined,
RedoOutlined,
SearchOutlined
} from '@ant-design/icons'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
@@ -14,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'
@@ -31,7 +29,31 @@ 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;
flex-direction: column;
gap: 8px;
`
const FlexAlignCenter = styled.div`
display: flex;
align-items: center;
gap: 16px;
`
const ClickableSpan = styled.span`
cursor: pointer;
`
const FileIcon = styled(FileTextOutlined)`
font-size: 16px;
`
const BottomSpacer = styled.div`
min-height: 20px;
`
const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
const { t } = useTranslation()
@@ -44,7 +66,6 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
directoryItems,
addFiles,
updateNoteContent,
refreshItem,
addUrl,
addSitemap,
removeItem,
@@ -53,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
@@ -76,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,
@@ -99,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
@@ -117,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)
@@ -128,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
@@ -146,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)
@@ -157,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)
@@ -187,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
@@ -205,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>
@@ -223,10 +211,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
<ClickableSpan onClick={() => window.api.file.openPath(file.path)}>{file.origin_name}</ClickableSpan>
</ItemInfo>
<FlexAlignCenter>
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter>
</ItemContent>
@@ -237,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>
@@ -253,10 +238,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</ClickableSpan>
</ItemInfo>
<FlexAlignCenter>
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter>
</ItemContent>
@@ -267,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>
@@ -283,10 +265,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</a>
</ItemInfo>
<FlexAlignCenter>
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter>
</ItemContent>
@@ -297,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>
@@ -313,10 +292,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</a>
</ItemInfo>
<FlexAlignCenter>
{item.uniqueId && <Button type="text" icon={<RefreshIcon />} onClick={() => refreshItem(item)} />}
<StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} />
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
</FlexAlignCenter>
</ItemContent>
@@ -327,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>
@@ -341,9 +317,7 @@ const KnowledgeContent: FC<KnowledgeContentProps> = ({ selectedBase }) => {
</ItemInfo>
<FlexAlignCenter>
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
<StatusIconWrapper>
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} />
</StatusIconWrapper>
<StatusIcon sourceId={note.id} base={base} getProcessingStatus={getProcessingStatus} />
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
</FlexAlignCenter>
</ItemContent>
@@ -355,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>
@@ -470,42 +440,4 @@ const ModelInfo = styled.div`
}
`
const FlexColumn = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
`
const FlexAlignCenter = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
const ClickableSpan = styled.span`
cursor: pointer;
`
const FileIcon = styled(FileTextOutlined)`
font-size: 16px;
`
const BottomSpacer = styled.div`
min-height: 20px;
`
const StatusIconWrapper = styled.div`
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
padding-top: 2px;
`
const RefreshIcon = styled(RedoOutlined)`
font-size: 15px !important;
color: var(--color-text-2);
`
export default KnowledgeContent

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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}

View File

@@ -1,4 +1,5 @@
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
import { Center } from '@renderer/components/Layout'
import { KnowledgeBase, ProcessingStatus } from '@renderer/types'
import { Tooltip } from 'antd'
import { FC } from 'react'
@@ -20,14 +21,16 @@ 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">
<StatusDot $status="new" />
<Tooltip title={t('knowledge_base.status_new')} placement="left">
<Center style={{ width: '16px', height: '16px' }}>
<StatusDot $status="new" />
</Center>
</Tooltip>
)
}
@@ -35,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>
)
@@ -63,8 +66,8 @@ const StatusIcon: FC<StatusIconProps> = ({ sourceId, base, getProcessingStatus }
}
const StatusDot = styled.div<{ $status: 'pending' | 'processing' | 'new' }>`
width: 10px;
height: 10px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: ${(props) =>
props.$status === 'pending' ? '#faad14' : props.$status === 'new' ? '#918999' : '#1890ff'};

View File

@@ -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'

View File

@@ -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

View File

@@ -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>

View File

@@ -4,10 +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 { isNull } from 'lodash'
import { FC, useEffect, useRef, useState } from 'react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -26,18 +25,16 @@ 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) => {
console.debug('[onTemperatureChange]', value)
if (!isNaN(value as number)) {
updateAssistantSettings({ temperature: value })
}
@@ -71,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') {
@@ -83,9 +80,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
case 'boolean':
defaultValue = false
break
case 'json':
defaultValue = ''
break
default:
defaultValue = ''
}
@@ -98,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) => {
@@ -118,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
@@ -178,10 +159,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
}
}
useEffect(() => {
return () => updateAssistantSettings({ customParameters: customParametersRef.current })
}, [])
return (
<Container>
<Row align="middle" style={{ marginBottom: 10 }}>
@@ -208,7 +185,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
value={autoResetModel}
onChange={(checked) => {
setAutoResetModel(checked)
setTimeout(() => updateAssistantSettings({ autoResetModel: checked }), 500)
updateAssistantSettings({ autoResetModel: checked })
}}
/>
</SettingRow>
@@ -220,7 +197,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
</Tooltip>
</Row>
<Row align="middle" gutter={20}>
<Col span={20}>
<Col span={21}>
<Slider
min={0}
max={2}
@@ -231,19 +208,13 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
step={0.01}
/>
</Col>
<Col span={4}>
<Col span={3}>
<InputNumber
min={0}
max={2}
step={0.01}
value={temperature}
changeOnBlur
onChange={(value) => {
if (!isNull(value)) {
setTemperature(value)
setTimeout(() => updateAssistantSettings({ temperature: value }), 500)
}
}}
onChange={onTemperatureChange}
style={{ width: '100%' }}
/>
</Col>
@@ -255,7 +226,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
</Tooltip>
</Row>
<Row align="middle" gutter={20}>
<Col span={20}>
<Col span={21}>
<Slider
min={0}
max={1}
@@ -266,21 +237,8 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
step={0.01}
/>
</Col>
<Col span={4}>
<InputNumber
min={0}
max={1}
step={0.01}
value={topP}
changeOnBlur
onChange={(value) => {
if (!isNull(value)) {
setTopP(value)
setTimeout(() => updateAssistantSettings({ topP: value }), 500)
}
}}
style={{ width: '100%' }}
/>
<Col span={3}>
<InputNumber min={0} max={1} step={0.01} value={topP} onChange={onTopPChange} style={{ width: '100%' }} />
</Col>
</Row>
<Row align="middle">
@@ -292,7 +250,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
</Label>
</Row>
<Row align="middle" gutter={20}>
<Col span={20}>
<Col span={21}>
<Slider
min={0}
max={20}
@@ -303,19 +261,13 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
step={1}
/>
</Col>
<Col span={4}>
<Col span={3}>
<InputNumber
min={0}
max={20}
step={1}
value={contextCount}
changeOnBlur
onChange={(value) => {
if (!isNull(value)) {
setContextCount(value)
setTimeout(() => updateAssistantSettings({ contextCount: value }), 500)
}
}}
onChange={onContextCountChange}
style={{ width: '100%' }}
/>
</Col>
@@ -338,7 +290,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
</SettingRow>
{enableMaxTokens && (
<Row align="middle" gutter={20}>
<Col span={20}>
<Col span={21}>
<Slider
disabled={!enableMaxTokens}
min={0}
@@ -346,27 +298,21 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
onChange={setMaxTokens}
onChangeComplete={onMaxTokensChange}
value={typeof maxTokens === 'number' ? maxTokens : 0}
step={50}
step={100}
marks={{
0: '0',
32000: t('chat.settings.max')
}}
/>
</Col>
<Col span={4}>
<Col span={3}>
<InputNumber
disabled={!enableMaxTokens}
min={0}
max={32000}
step={100}
value={maxTokens}
changeOnBlur
onChange={(value) => {
if (!isNull(value)) {
setMaxTokens(value)
setTimeout(() => updateAssistantSettings({ maxTokens: value }), 1000)
}
}}
onChange={onMaxTokensChange}
style={{ width: '100%' }}
/>
</Col>
@@ -391,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')}
@@ -407,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>

View File

@@ -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">

View File

@@ -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'))
}
}
})

View File

@@ -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>
</>
)}
</>
)
}

View File

@@ -1,25 +1,20 @@
import { isMac } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinapps } from '@renderer/hooks/useMinapps'
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 MiniAppIconsManager from './MiniAppIconsManager'
import SidebarIconsManager from './SidebarIconsManager'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
const DisplaySettings: FC = () => {
const {
@@ -27,43 +22,22 @@ const DisplaySettings: FC = () => {
theme,
windowStyle,
setWindowStyle,
showMinappIcon,
showFilesIcon,
topicPosition,
setTopicPosition,
clickAssistantToShowTopic,
showTopicTime,
customCss,
sidebarIcons
customCss
} = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
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 || [])
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
const [disabledMiniApps, setDisabledMiniApps] = useState(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 handleResetMinApps = useCallback(() => {
setVisibleMiniApps(DEFAULT_MIN_APPS)
setDisabledMiniApps([])
updateMinapps(DEFAULT_MIN_APPS)
updateDisabledMinapps([])
}, [updateDisabledMinapps, updateMinapps])
const handleWindowStyleChange = (checked: boolean) => {
setWindowStyle(checked ? 'transparent' : 'opaque')
}
return (
<SettingContainer theme={themeMode}>
@@ -73,7 +47,7 @@ const DisplaySettings: FC = () => {
<SettingRow>
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
<Select
value={theme}
defaultValue={theme}
style={{ width: 120 }}
onChange={setTheme}
options={[
@@ -99,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={[
@@ -127,43 +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}
/>
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.display.minApp.title')}</span>
<ResetButtonWrapper>
<Button onClick={handleResetMinApps}>{t('common.reset')}</Button>
</ResetButtonWrapper>
</SettingTitle>
<SettingRow>
<SettingRowTitle>{t('settings.display.sidebar.minapp.icon')}</SettingRowTitle>
<Switch checked={showMinappIcon} onChange={(value) => dispatch(setShowMinappIcon(value))} />
</SettingRow>
<SettingDivider />
<MiniAppIconsManager
visibleMiniApps={visibleMiniApps}
disabledMiniApps={disabledMiniApps}
setVisibleMiniApps={setVisibleMiniApps}
setDisabledMiniApps={setDisabledMiniApps}
/>
<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,
@@ -175,10 +130,4 @@ const DisplaySettings: FC = () => {
)
}
const ResetButtonWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
`
export default DisplaySettings

View File

@@ -1,273 +0,0 @@
import { CloseOutlined } from '@ant-design/icons'
import {
DragDropContext,
Draggable,
DraggableProvided,
Droppable,
DroppableProvided,
DropResult
} from '@hello-pangea/dnd'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface MiniAppManagerProps {
visibleMiniApps: MinAppType[]
disabledMiniApps: MinAppType[]
setVisibleMiniApps: (programs: MinAppType[]) => void
setDisabledMiniApps: (programs: MinAppType[]) => void
}
type ListType = 'visible' | 'disabled'
// 添加 reorderLists 函数的接口定义
interface ReorderListsParams {
sourceList: MinAppType[]
destList: MinAppType[]
sourceIndex: number
destIndex: number
isSameList: boolean
}
interface ReorderListsResult {
sourceList: MinAppType[]
destList: MinAppType[]
}
// 添加 reorderLists 函数
const reorderLists = ({
sourceList,
destList,
sourceIndex,
destIndex,
isSameList
}: ReorderListsParams): ReorderListsResult => {
if (isSameList) {
// 在同一列表内重新排序
const newList = [...sourceList]
const [removed] = newList.splice(sourceIndex, 1)
newList.splice(destIndex, 0, removed)
return {
sourceList: newList,
destList: destList
}
} else {
// 在不同列表间移动
const newSourceList = [...sourceList]
const [removed] = newSourceList.splice(sourceIndex, 1)
const newDestList = [...destList]
newDestList.splice(destIndex, 0, removed)
return {
sourceList: newSourceList,
destList: newDestList
}
}
}
const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
visibleMiniApps,
disabledMiniApps,
setVisibleMiniApps,
setDisabledMiniApps
}) => {
const { t } = useTranslation()
const { pinned, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
const handleListUpdate = useCallback(
(newVisible: MinAppType[], newDisabled: MinAppType[]) => {
setVisibleMiniApps(newVisible)
setDisabledMiniApps(newDisabled)
updateMinapps(newVisible)
updateDisabledMinapps(newDisabled)
updatePinnedMinapps(pinned.filter((p) => !newDisabled.some((d) => d.id === p.id)))
},
[pinned, setDisabledMiniApps, setVisibleMiniApps, updateDisabledMinapps, updateMinapps, updatePinnedMinapps]
)
const onDragEnd = useCallback(
(result: DropResult) => {
if (!result.destination) return
const { source, destination } = result
const sourceList = source.droppableId as ListType
const destList = destination.droppableId as ListType
if (source.droppableId === destination.droppableId) return
const newLists = reorderLists({
sourceList: sourceList === 'visible' ? visibleMiniApps : disabledMiniApps,
destList: destList === 'visible' ? visibleMiniApps : disabledMiniApps,
sourceIndex: source.index,
destIndex: destination.index,
isSameList: sourceList === destList
})
handleListUpdate(
sourceList === 'visible' ? newLists.sourceList : newLists.destList,
sourceList === 'visible' ? newLists.destList : newLists.sourceList
)
},
[disabledMiniApps, handleListUpdate, visibleMiniApps]
)
const onMoveMiniApp = useCallback(
(program: MinAppType, fromList: ListType) => {
const isMovingToVisible = fromList === 'disabled'
const newVisible = isMovingToVisible
? [...visibleMiniApps, program]
: visibleMiniApps.filter((p) => p.id !== program.id)
const newDisabled = isMovingToVisible
? disabledMiniApps.filter((p) => p.id !== program.id)
: [...disabledMiniApps, program]
handleListUpdate(newVisible, newDisabled)
},
[visibleMiniApps, disabledMiniApps, handleListUpdate]
)
const renderProgramItem = (program: MinAppType, provided: DraggableProvided, listType: ListType) => {
const { name, logo } = DEFAULT_MIN_APPS.find((app) => app.id === program.id) || { name: program.name, logo: '' }
return (
<ProgramItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<ProgramContent>
<AppLogo src={logo} alt={name} />
<span>{name}</span>
</ProgramContent>
<CloseButton onClick={() => onMoveMiniApp(program, listType)}>
<CloseOutlined />
</CloseButton>
</ProgramItem>
)
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<ProgramSection>
{(['visible', 'disabled'] as const).map((listType) => (
<ProgramColumn key={listType}>
<h4>{t(`settings.display.minApp.${listType}`)}</h4>
<Droppable droppableId={listType}>
{(provided: DroppableProvided) => (
<ProgramList ref={provided.innerRef} {...provided.droppableProps}>
<ScrollContainer>
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
<Draggable key={program.id} draggableId={String(program.id)} index={index}>
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
</Draggable>
))}
{disabledMiniApps.length === 0 && listType === 'disabled' && (
<EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder>
)}
{provided.placeholder}
</ScrollContainer>
</ProgramList>
)}
</Droppable>
</ProgramColumn>
))}
</ProgramSection>
</DragDropContext>
)
}
const AppLogo = styled.img`
width: 16px;
height: 16px;
border-radius: 4px;
object-fit: contain;
`
const ScrollContainer = styled.div`
overflow-y: auto;
height: 100%;
padding-right: 5px;
`
const ProgramSection = styled.div`
display: flex;
gap: 20px;
padding: 10px;
background: var(--color-background);
`
const ProgramColumn = styled.div`
flex: 1;
h4 {
margin-bottom: 10px;
color: var(--color-text);
font-weight: normal;
}
`
const ProgramList = styled.div`
height: 365px;
min-height: 365px;
padding: 10px;
padding-right: 5px;
background: var(--color-background-soft);
border-radius: 8px;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
overflow-y: hidden;
`
const ProgramItem = 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 ProgramContent = 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);
}
${ProgramItem}: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 MiniAppIconsManager

View File

@@ -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

View File

@@ -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={{

View File

@@ -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 (

View File

@@ -2,16 +2,13 @@ import { CheckCircleFilled, CloseCircleFilled, LoadingOutlined } from '@ant-desi
import Scrollbar from '@renderer/components/Scrollbar'
import { TopView } from '@renderer/components/TopView'
import { checkApi } from '@renderer/services/ApiService'
import { Model } from '@renderer/types'
import { Provider } from '@renderer/types'
import { Button, List, Modal, Space, Spin, Typography } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface ShowParams {
title: string
provider: Provider
model: Model
provider: any
apiKeys: string[]
}
@@ -25,7 +22,7 @@ interface KeyStatus {
checking?: boolean
}
const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, resolve }) => {
const PopupContainer: React.FC<Props> = ({ title, provider, apiKeys, resolve }) => {
const [open, setOpen] = useState(true)
const [keyStatuses, setKeyStatuses] = useState<KeyStatus[]>(() => {
const uniqueKeys = new Set(apiKeys)
@@ -42,7 +39,7 @@ const PopupContainer: React.FC<Props> = ({ title, provider, model, apiKeys, reso
for (let i = 0; i < newStatuses.length; i++) {
setKeyStatuses((prev) => prev.map((status, idx) => (idx === i ? { ...status, checking: true } : status)))
const valid = await checkApi({ ...provider, apiKey: newStatuses[i].key }, model)
const valid = await checkApi({ ...provider, apiKey: newStatuses[i].key })
setKeyStatuses((prev) =>
prev.map((status, idx) => (idx === i ? { ...status, checking: false, isValid: valid } : status))

Some files were not shown because too many files have changed in this diff Show More