mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 12:51:26 +08:00
Merge remote-tracking branch 'origin/main' into feat/cherry-store
This commit is contained in:
commit
98f83e096b
@ -1,5 +1,5 @@
|
||||
diff --git a/es/dropdown/dropdown.js b/es/dropdown/dropdown.js
|
||||
index 986877a762b9ad0aca596a8552732cd12d2eaabb..1f18aa2ea745e68950e4cee16d4d655f5c835fd5 100644
|
||||
index 2e45574398ff68450022a0078e213cc81fe7454e..58ba7789939b7805a89f92b93d222f8fb1168bdf 100644
|
||||
--- a/es/dropdown/dropdown.js
|
||||
+++ b/es/dropdown/dropdown.js
|
||||
@@ -2,7 +2,7 @@
|
||||
@ -11,7 +11,7 @@ index 986877a762b9ad0aca596a8552732cd12d2eaabb..1f18aa2ea745e68950e4cee16d4d655f
|
||||
import classNames from 'classnames';
|
||||
import RcDropdown from 'rc-dropdown';
|
||||
import useEvent from "rc-util/es/hooks/useEvent";
|
||||
@@ -158,8 +158,10 @@ const Dropdown = props => {
|
||||
@@ -160,8 +160,10 @@ const Dropdown = props => {
|
||||
className: `${prefixCls}-menu-submenu-arrow`
|
||||
}, direction === 'rtl' ? (/*#__PURE__*/React.createElement(LeftOutlined, {
|
||||
className: `${prefixCls}-menu-submenu-arrow-icon`
|
||||
@ -24,22 +24,8 @@ index 986877a762b9ad0aca596a8552732cd12d2eaabb..1f18aa2ea745e68950e4cee16d4d655f
|
||||
}))),
|
||||
mode: "vertical",
|
||||
selectable: false,
|
||||
diff --git a/es/dropdown/style/index.js b/es/dropdown/style/index.js
|
||||
index 768c01783002c6901c85a73061ff6b3e776a60ce..39b1b95a56cdc9fb586a193c3adad5141f5cf213 100644
|
||||
--- a/es/dropdown/style/index.js
|
||||
+++ b/es/dropdown/style/index.js
|
||||
@@ -240,7 +240,8 @@ const genBaseStyle = token => {
|
||||
marginInlineEnd: '0 !important',
|
||||
color: token.colorTextDescription,
|
||||
fontSize: fontSizeIcon,
|
||||
- fontStyle: 'normal'
|
||||
+ fontStyle: 'normal',
|
||||
+ marginTop: 3,
|
||||
}
|
||||
}
|
||||
}),
|
||||
diff --git a/es/select/useIcons.js b/es/select/useIcons.js
|
||||
index 959115be936ef8901548af2658c5dcfdc5852723..c812edd52123eb0faf4638b1154fcfa1b05b513b 100644
|
||||
index 572aaaa0899f429cbf8a7181f2eeada545f76dcb..4e175c8d7713dd6422f8bcdc74ee671a835de6ce 100644
|
||||
--- a/es/select/useIcons.js
|
||||
+++ b/es/select/useIcons.js
|
||||
@@ -4,10 +4,10 @@ import * as React from 'react';
|
||||
@ -51,10 +37,10 @@ index 959115be936ef8901548af2658c5dcfdc5852723..c812edd52123eb0faf4638b1154fcfa1
|
||||
import SearchOutlined from "@ant-design/icons/es/icons/SearchOutlined";
|
||||
import { devUseWarning } from '../_util/warning';
|
||||
+import { ChevronDown } from 'lucide-react';
|
||||
export default function useIcons(_ref) {
|
||||
let {
|
||||
suffixIcon,
|
||||
@@ -56,8 +56,10 @@ export default function useIcons(_ref) {
|
||||
export default function useIcons({
|
||||
suffixIcon,
|
||||
clearIcon,
|
||||
@@ -54,8 +54,10 @@ export default function useIcons({
|
||||
className: iconCls
|
||||
}));
|
||||
}
|
||||
14
CLAUDE.md
14
CLAUDE.md
@ -5,15 +5,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Development Commands
|
||||
|
||||
### Environment Setup
|
||||
|
||||
- **Prerequisites**: Node.js v22.x.x or higher, Yarn 4.9.1
|
||||
- **Setup Yarn**: `corepack enable && corepack prepare yarn@4.9.1 --activate`
|
||||
- **Install Dependencies**: `yarn install`
|
||||
|
||||
### Development
|
||||
|
||||
- **Start Development**: `yarn dev` - Runs Electron app in development mode
|
||||
- **Debug Mode**: `yarn debug` - Starts with debugging enabled, use chrome://inspect
|
||||
|
||||
### Testing & Quality
|
||||
|
||||
- **Run Tests**: `yarn test` - Runs all tests (Vitest)
|
||||
- **Run E2E Tests**: `yarn test:e2e` - Playwright end-to-end tests
|
||||
- **Type Check**: `yarn typecheck` - Checks TypeScript for both node and web
|
||||
@ -21,6 +24,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- **Format**: `yarn format` - Prettier formatting
|
||||
|
||||
### Build & Release
|
||||
|
||||
- **Build**: `yarn build` - Builds for production (includes typecheck)
|
||||
- **Platform-specific builds**:
|
||||
- Windows: `yarn build:win`
|
||||
@ -30,6 +34,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Architecture Overview
|
||||
|
||||
### Electron Multi-Process Architecture
|
||||
|
||||
- **Main Process** (`src/main/`): Node.js backend handling system integration, file operations, and services
|
||||
- **Renderer Process** (`src/renderer/`): React-based UI running in Chromium
|
||||
- **Preload Scripts** (`src/preload/`): Secure bridge between main and renderer processes
|
||||
@ -37,6 +42,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
### Key Architectural Components
|
||||
|
||||
#### Main Process Services (`src/main/services/`)
|
||||
|
||||
- **MCPService**: Model Context Protocol server management
|
||||
- **KnowledgeService**: Document processing and knowledge base management
|
||||
- **FileStorage/S3Storage/WebDav**: Multiple storage backends
|
||||
@ -45,22 +51,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- **SearchService**: Full-text search capabilities
|
||||
|
||||
#### AI Core (`src/renderer/src/aiCore/`)
|
||||
|
||||
- **Middleware System**: Composable pipeline for AI request processing
|
||||
- **Client Factory**: Supports multiple AI providers (OpenAI, Anthropic, Gemini, etc.)
|
||||
- **Stream Processing**: Real-time response handling
|
||||
|
||||
#### State Management (`src/renderer/src/store/`)
|
||||
|
||||
- **Redux Toolkit**: Centralized state management
|
||||
- **Persistent Storage**: Redux-persist for data persistence
|
||||
- **Thunks**: Async actions for complex operations
|
||||
|
||||
#### Knowledge Management
|
||||
|
||||
- **Embeddings**: Vector search with multiple providers (OpenAI, Voyage, etc.)
|
||||
- **OCR**: Document text extraction (system OCR, Doc2x, Mineru)
|
||||
- **Preprocessing**: Document preparation pipeline
|
||||
- **Loaders**: Support for various file formats (PDF, DOCX, EPUB, etc.)
|
||||
|
||||
### Build System
|
||||
|
||||
- **Electron-Vite**: Development and build tooling (v4.0.0)
|
||||
- **Rolldown-Vite**: Using experimental rolldown-vite instead of standard vite
|
||||
- **Workspaces**: Monorepo structure with `packages/` directory
|
||||
@ -68,12 +78,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
- **Styled Components**: CSS-in-JS styling with SWC optimization
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
- **Vitest**: Unit and integration testing
|
||||
- **Playwright**: End-to-end testing
|
||||
- **Component Testing**: React Testing Library
|
||||
- **Coverage**: Available via `yarn test:coverage`
|
||||
|
||||
### Key Patterns
|
||||
|
||||
- **IPC Communication**: Secure main-renderer communication via preload scripts
|
||||
- **Service Layer**: Clear separation between UI and business logic
|
||||
- **Plugin Architecture**: Extensible via MCP servers and middleware
|
||||
@ -83,6 +95,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
## Logging Standards
|
||||
|
||||
### Usage
|
||||
|
||||
```typescript
|
||||
// Main process
|
||||
import { loggerService } from '@logger'
|
||||
@ -98,6 +111,7 @@ logger.error('message', new Error('error'), CONTEXT)
|
||||
```
|
||||
|
||||
### Log Levels (highest to lowest)
|
||||
|
||||
- `error` - Critical errors causing crash/unusable functionality
|
||||
- `warn` - Potential issues that don't affect core functionality
|
||||
- `info` - Application lifecycle and key user actions
|
||||
|
||||
@ -128,3 +128,4 @@ releaseInfo:
|
||||
内存泄漏修复:优化代码逻辑,解决内存泄漏问题,提升运行稳定性
|
||||
嵌入模型简化:降低嵌入模型配置复杂度,提高易用性
|
||||
MCP Tool 长时间运行:增强 MCP 工具的稳定性,支持长时间任务执行
|
||||
设置页面优化:优化设置页面布局,提升用户体验
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.5.4-rc.2",
|
||||
"version": "1.5.4-rc.3",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@ -170,7 +170,7 @@
|
||||
"@viz-js/lang-dot": "^1.0.5",
|
||||
"@viz-js/viz": "^3.14.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"antd": "patch:antd@npm%3A5.24.7#~/.yarn/patches/antd-npm-5.24.7-356a553ae5.patch",
|
||||
"antd": "patch:antd@npm%3A5.26.7#~/.yarn/patches/antd-npm-5.26.7-029c5c381a.patch",
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.7.3",
|
||||
|
||||
@ -206,3 +206,5 @@ export enum UpgradeChannel {
|
||||
export const defaultTimeout = 10 * 1000 * 60
|
||||
|
||||
export const occupiedDirs = ['logs', 'Network', 'Partitions/webview/Network']
|
||||
|
||||
export const defaultByPassRules = 'localhost,127.0.0.1,::1'
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -56,8 +56,14 @@ if (isLinux && process.env.XDG_SESSION_TYPE === 'wayland') {
|
||||
app.commandLine.appendSwitch('enable-features', 'GlobalShortcutsPortal')
|
||||
}
|
||||
|
||||
// Enable features for unresponsive renderer js call stacks
|
||||
app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')
|
||||
// DocumentPolicyIncludeJSCallStacksInCrashReports: Enable features for unresponsive renderer js call stacks
|
||||
// EarlyEstablishGpuChannel,EstablishGpuChannelAsync: Enable features for early establish gpu channel
|
||||
// speed up the startup time
|
||||
// https://github.com/microsoft/vscode/pull/241640/files
|
||||
app.commandLine.appendSwitch(
|
||||
'enable-features',
|
||||
'DocumentPolicyIncludeJSCallStacksInCrashReports,EarlyEstablishGpuChannel,EstablishGpuChannelAsync'
|
||||
)
|
||||
app.on('web-contents-created', (_, webContents) => {
|
||||
webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({
|
||||
|
||||
@ -90,7 +90,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
installPath: path.dirname(app.getPath('exe'))
|
||||
}))
|
||||
|
||||
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => {
|
||||
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string, bypassRules?: string) => {
|
||||
let proxyConfig: ProxyConfig
|
||||
|
||||
if (proxy === 'system') {
|
||||
@ -101,6 +101,10 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
proxyConfig = { mode: 'direct' }
|
||||
}
|
||||
|
||||
if (bypassRules) {
|
||||
proxyConfig.proxyBypassRules = bypassRules
|
||||
}
|
||||
|
||||
await proxyManager.configureProxy(proxyConfig)
|
||||
})
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { defaultByPassRules } from '@shared/config/constant'
|
||||
import axios from 'axios'
|
||||
import { app, ProxyConfig, session } from 'electron'
|
||||
import { socksDispatcher } from 'fetch-socks'
|
||||
@ -9,12 +10,60 @@ import { ProxyAgent } from 'proxy-agent'
|
||||
import { Dispatcher, EnvHttpProxyAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const logger = loggerService.withContext('ProxyManager')
|
||||
let byPassRules = defaultByPassRules.split(',')
|
||||
|
||||
const isByPass = (hostname: string) => {
|
||||
return byPassRules.includes(hostname)
|
||||
}
|
||||
|
||||
class SelectiveDispatcher extends Dispatcher {
|
||||
private proxyDispatcher: Dispatcher
|
||||
private directDispatcher: Dispatcher
|
||||
|
||||
constructor(proxyDispatcher: Dispatcher, directDispatcher: Dispatcher) {
|
||||
super()
|
||||
this.proxyDispatcher = proxyDispatcher
|
||||
this.directDispatcher = directDispatcher
|
||||
}
|
||||
|
||||
dispatch(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers) {
|
||||
if (opts.origin) {
|
||||
const url = new URL(opts.origin)
|
||||
// 检查是否为 localhost 或本地地址
|
||||
if (isByPass(url.hostname)) {
|
||||
return this.directDispatcher.dispatch(opts, handler)
|
||||
}
|
||||
}
|
||||
|
||||
return this.proxyDispatcher.dispatch(opts, handler)
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
try {
|
||||
await this.proxyDispatcher.close()
|
||||
} catch (error) {
|
||||
logger.error('Failed to close dispatcher:', error as Error)
|
||||
this.proxyDispatcher.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
async destroy(): Promise<void> {
|
||||
try {
|
||||
await this.proxyDispatcher.destroy()
|
||||
} catch (error) {
|
||||
logger.error('Failed to destroy dispatcher:', error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ProxyManager {
|
||||
private config: ProxyConfig = { mode: 'direct' }
|
||||
private systemProxyInterval: NodeJS.Timeout | null = null
|
||||
private isSettingProxy = false
|
||||
|
||||
private proxyDispatcher: Dispatcher | null = null
|
||||
private proxyAgent: ProxyAgent | null = null
|
||||
|
||||
private originalGlobalDispatcher: Dispatcher
|
||||
private originalSocksDispatcher: Dispatcher
|
||||
// for http and https
|
||||
@ -44,7 +93,8 @@ export class ProxyManager {
|
||||
|
||||
await this.configureProxy({
|
||||
mode: 'system',
|
||||
proxyRules: currentProxy?.proxyUrl.toLowerCase()
|
||||
proxyRules: currentProxy?.proxyUrl.toLowerCase(),
|
||||
proxyBypassRules: this.config.proxyBypassRules
|
||||
})
|
||||
}, 1000 * 60)
|
||||
}
|
||||
@ -57,7 +107,8 @@ export class ProxyManager {
|
||||
}
|
||||
|
||||
async configureProxy(config: ProxyConfig): Promise<void> {
|
||||
logger.debug(`configureProxy: ${config?.mode} ${config?.proxyRules}`)
|
||||
logger.info(`configureProxy: ${config?.mode} ${config?.proxyRules} ${config?.proxyBypassRules}`)
|
||||
|
||||
if (this.isSettingProxy) {
|
||||
return
|
||||
}
|
||||
@ -65,11 +116,6 @@ export class ProxyManager {
|
||||
this.isSettingProxy = true
|
||||
|
||||
try {
|
||||
if (config?.mode === this.config?.mode && config?.proxyRules === this.config?.proxyRules) {
|
||||
logger.debug('proxy config is the same, skip configure')
|
||||
return
|
||||
}
|
||||
|
||||
this.config = config
|
||||
this.clearSystemProxyMonitor()
|
||||
if (config.mode === 'system') {
|
||||
@ -81,7 +127,8 @@ export class ProxyManager {
|
||||
this.monitorSystemProxy()
|
||||
}
|
||||
|
||||
this.setGlobalProxy()
|
||||
byPassRules = config.proxyBypassRules?.split(',') || defaultByPassRules.split(',')
|
||||
this.setGlobalProxy(this.config)
|
||||
} catch (error) {
|
||||
logger.error('Failed to config proxy:', error as Error)
|
||||
throw error
|
||||
@ -115,12 +162,12 @@ export class ProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
private setGlobalProxy() {
|
||||
this.setEnvironment(this.config.proxyRules || '')
|
||||
this.setGlobalFetchProxy(this.config)
|
||||
this.setSessionsProxy(this.config)
|
||||
private setGlobalProxy(config: ProxyConfig) {
|
||||
this.setEnvironment(config.proxyRules || '')
|
||||
this.setGlobalFetchProxy(config)
|
||||
this.setSessionsProxy(config)
|
||||
|
||||
this.setGlobalHttpProxy(this.config)
|
||||
this.setGlobalHttpProxy(config)
|
||||
}
|
||||
|
||||
private setGlobalHttpProxy(config: ProxyConfig) {
|
||||
@ -129,21 +176,18 @@ export class ProxyManager {
|
||||
http.request = this.originalHttpRequest
|
||||
https.get = this.originalHttpsGet
|
||||
https.request = this.originalHttpsRequest
|
||||
|
||||
axios.defaults.proxy = undefined
|
||||
axios.defaults.httpAgent = undefined
|
||||
axios.defaults.httpsAgent = undefined
|
||||
try {
|
||||
this.proxyAgent?.destroy()
|
||||
} catch (error) {
|
||||
logger.error('Failed to destroy proxy agent:', error as Error)
|
||||
}
|
||||
this.proxyAgent = null
|
||||
return
|
||||
}
|
||||
|
||||
// ProxyAgent 从环境变量读取代理配置
|
||||
const agent = new ProxyAgent()
|
||||
|
||||
// axios 使用代理
|
||||
axios.defaults.proxy = false
|
||||
axios.defaults.httpAgent = agent
|
||||
axios.defaults.httpsAgent = agent
|
||||
|
||||
this.proxyAgent = agent
|
||||
http.get = this.bindHttpMethod(this.originalHttpGet, agent)
|
||||
http.request = this.bindHttpMethod(this.originalHttpRequest, agent)
|
||||
|
||||
@ -176,16 +220,19 @@ export class ProxyManager {
|
||||
callback = args[1]
|
||||
}
|
||||
|
||||
// filter localhost
|
||||
if (url) {
|
||||
const hostname = typeof url === 'string' ? new URL(url).hostname : url.hostname
|
||||
if (isByPass(hostname)) {
|
||||
return originalMethod(url, options, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// for webdav https self-signed certificate
|
||||
if (options.agent instanceof https.Agent) {
|
||||
;(agent as https.Agent).options.rejectUnauthorized = options.agent.options.rejectUnauthorized
|
||||
}
|
||||
|
||||
// 确保只设置 agent,不修改其他网络选项
|
||||
if (!options.agent) {
|
||||
options.agent = agent
|
||||
}
|
||||
|
||||
options.agent = agent
|
||||
if (url) {
|
||||
return originalMethod(url, options, callback)
|
||||
}
|
||||
@ -198,22 +245,33 @@ export class ProxyManager {
|
||||
if (config.mode === 'direct' || !proxyUrl) {
|
||||
setGlobalDispatcher(this.originalGlobalDispatcher)
|
||||
global[Symbol.for('undici.globalDispatcher.1')] = this.originalSocksDispatcher
|
||||
axios.defaults.adapter = 'http'
|
||||
this.proxyDispatcher?.close()
|
||||
this.proxyDispatcher = null
|
||||
return
|
||||
}
|
||||
|
||||
// axios 使用 fetch 代理
|
||||
axios.defaults.adapter = 'fetch'
|
||||
|
||||
const url = new URL(proxyUrl)
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent())
|
||||
this.proxyDispatcher = new SelectiveDispatcher(new EnvHttpProxyAgent(), this.originalGlobalDispatcher)
|
||||
setGlobalDispatcher(this.proxyDispatcher)
|
||||
return
|
||||
}
|
||||
|
||||
global[Symbol.for('undici.globalDispatcher.1')] = socksDispatcher({
|
||||
port: parseInt(url.port),
|
||||
type: url.protocol === 'socks4:' ? 4 : 5,
|
||||
host: url.hostname,
|
||||
userId: url.username || undefined,
|
||||
password: url.password || undefined
|
||||
})
|
||||
this.proxyDispatcher = new SelectiveDispatcher(
|
||||
socksDispatcher({
|
||||
port: parseInt(url.port),
|
||||
type: url.protocol === 'socks4:' ? 4 : 5,
|
||||
host: url.hostname,
|
||||
userId: url.username || undefined,
|
||||
password: url.password || undefined
|
||||
}),
|
||||
this.originalSocksDispatcher
|
||||
)
|
||||
global[Symbol.for('undici.globalDispatcher.1')] = this.proxyDispatcher
|
||||
}
|
||||
|
||||
private async setSessionsProxy(config: ProxyConfig): Promise<void> {
|
||||
|
||||
@ -26,7 +26,7 @@ function streamToBuffer(stream: Readable): Promise<Buffer> {
|
||||
}
|
||||
|
||||
// 需要使用 Virtual Host-Style 的服务商域名后缀白名单
|
||||
const VIRTUAL_HOST_SUFFIXES = ['aliyuncs.com', 'myqcloud.com']
|
||||
const VIRTUAL_HOST_SUFFIXES = ['aliyuncs.com', 'myqcloud.com', 'volces.com']
|
||||
|
||||
/**
|
||||
* 使用 AWS SDK v3 的简单 S3 封装,兼容之前 RemoteStorage 的最常用接口。
|
||||
|
||||
@ -41,7 +41,8 @@ export function tracedInvoke(channel: string, spanContext: SpanContext | undefin
|
||||
const api = {
|
||||
getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info),
|
||||
reload: () => ipcRenderer.invoke(IpcChannel.App_Reload),
|
||||
setProxy: (proxy: string | undefined) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy),
|
||||
setProxy: (proxy: string | undefined, bypassRules?: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules),
|
||||
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
|
||||
showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog),
|
||||
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
|
||||
|
||||
@ -53,3 +53,18 @@
|
||||
animation-fill-mode: both;
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
|
||||
// 旋转动画
|
||||
@keyframes animation-rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animation-rotate {
|
||||
transform-origin: center;
|
||||
animation: animation-rotate 0.75s linear infinite;
|
||||
}
|
||||
|
||||
@ -12,6 +12,13 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Align lucide icon in Button
|
||||
.ant-btn .ant-btn-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ant-tabs-tabpane:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
@ -84,6 +91,14 @@
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
border: 0.5px solid var(--color-border);
|
||||
|
||||
// Align lucide icon in dropdown menu item extra
|
||||
.ant-dropdown-menu-submenu-expand-icon,
|
||||
.ant-dropdown-menu-item-extra {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.ant-dropdown-arrow + .ant-dropdown-menu {
|
||||
border: none;
|
||||
@ -96,6 +111,10 @@
|
||||
background-color: var(--ant-color-bg-elevated);
|
||||
overflow: hidden;
|
||||
border-radius: var(--ant-border-radius-lg);
|
||||
|
||||
.ant-dropdown-menu-submenu-title {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-popover {
|
||||
|
||||
@ -32,7 +32,7 @@
|
||||
--color-border: #ffffff19;
|
||||
--color-border-soft: #ffffff10;
|
||||
--color-border-mute: #ffffff05;
|
||||
--color-error: #f44336;
|
||||
--color-error: #ff4d50;
|
||||
--color-link: #338cff;
|
||||
--color-code-background: #323232;
|
||||
--color-hover: rgba(40, 40, 40, 1);
|
||||
@ -73,8 +73,8 @@
|
||||
|
||||
--list-item-border-radius: 10px;
|
||||
|
||||
--color-status-success: #52c41a;
|
||||
--color-status-error: #ff4d4f;
|
||||
--color-status-success: green;
|
||||
--color-status-error: var(--color-error);
|
||||
--color-status-warning: #faad14;
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
--color-border: #00000019;
|
||||
--color-border-soft: #00000010;
|
||||
--color-border-mute: #00000005;
|
||||
--color-error: #f44336;
|
||||
--color-error: #ff4d50;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #e3e3e3;
|
||||
--color-hover: var(--color-white-mute);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { AsyncInitializer } from '@renderer/utils/asyncInitializer'
|
||||
import { Flex, Spin } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
@ -86,7 +86,7 @@ const GraphvizPreview: React.FC<BasicPreviewProps> = ({ children, setTools }) =>
|
||||
}, [children, debouncedRender])
|
||||
|
||||
return (
|
||||
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
|
||||
<Spin spinning={isLoading} indicator={<LoadingIcon color="var(--color-text-2)" />}>
|
||||
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
|
||||
{error && <PreviewError>{error}</PreviewError>}
|
||||
<StyledGraphviz ref={graphvizRef} className="graphviz special-preview" />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { usePreviewToolHandlers, usePreviewTools } from '@renderer/components/CodeToolbar'
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { useMermaid } from '@renderer/hooks/useMermaid'
|
||||
import { Flex, Spin } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
@ -139,7 +139,7 @@ const MermaidPreview: React.FC<BasicPreviewProps> = ({ children, setTools }) =>
|
||||
const isLoading = isLoadingMermaid || isRendering
|
||||
|
||||
return (
|
||||
<Spin spinning={isLoading} indicator={<SvgSpinners180Ring color="var(--color-text-2)" />}>
|
||||
<Spin spinning={isLoading} indicator={<LoadingIcon color="var(--color-text-2)" />}>
|
||||
<Flex vertical style={{ minHeight: isLoading ? '2rem' : 'auto' }}>
|
||||
{(mermaidError || error) && <PreviewError>{mermaidError || error}</PreviewError>}
|
||||
<StyledMermaid ref={mermaidRef} className="mermaid special-preview" />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { CodeTool, CodeToolbar, TOOL_SPECS, useCodeTool } from '@renderer/components/CodeToolbar'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { pyodideService } from '@renderer/services/PyodideService'
|
||||
import { extractTitle } from '@renderer/utils/formats'
|
||||
@ -173,7 +173,7 @@ export const CodeBlockView: React.FC<Props> = memo(({ children, language, onSave
|
||||
|
||||
registerTool({
|
||||
...TOOL_SPECS.run,
|
||||
icon: isRunning ? <LoadingOutlined /> : <CirclePlay className="icon" />,
|
||||
icon: isRunning ? <LoadingIcon /> : <CirclePlay className="icon" />,
|
||||
tooltip: t('code_block.run'),
|
||||
onClick: () => !isRunning && handleRunScript()
|
||||
})
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { Copy } from 'lucide-react'
|
||||
|
||||
const CopyIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||
return <i {...props} className={`iconfont icon-copy ${props.className}`} />
|
||||
}
|
||||
const CopyIcon = (props: React.ComponentProps<typeof Copy>) => <Copy size="1rem" {...props} />
|
||||
|
||||
export default CopyIcon
|
||||
|
||||
5
src/renderer/src/components/Icons/DeleteIcon.tsx
Normal file
5
src/renderer/src/components/Icons/DeleteIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { Trash } from 'lucide-react'
|
||||
|
||||
const DeleteIcon = (props: React.ComponentProps<typeof Trash>) => <Trash size="1rem" {...props} />
|
||||
|
||||
export default DeleteIcon
|
||||
5
src/renderer/src/components/Icons/EditIcon.tsx
Normal file
5
src/renderer/src/components/Icons/EditIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { Pencil } from 'lucide-react'
|
||||
|
||||
const EditIcon = (props: React.ComponentProps<typeof Pencil>) => <Pencil size="1rem" {...props} />
|
||||
|
||||
export default EditIcon
|
||||
5
src/renderer/src/components/Icons/RefreshIcon.tsx
Normal file
5
src/renderer/src/components/Icons/RefreshIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
|
||||
const RefreshIcon = (props: React.ComponentProps<typeof RefreshCw>) => <RefreshCw size="1rem" {...props} />
|
||||
|
||||
export default RefreshIcon
|
||||
5
src/renderer/src/components/Icons/ResetIcon.tsx
Normal file
5
src/renderer/src/components/Icons/ResetIcon.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { RotateCcw } from 'lucide-react'
|
||||
|
||||
const ResetIcon = (props: React.ComponentProps<typeof RotateCcw>) => <RotateCcw size="1rem" {...props} />
|
||||
|
||||
export default ResetIcon
|
||||
@ -1,41 +1,21 @@
|
||||
import { SVGProps } from 'react'
|
||||
|
||||
export function SvgSpinners180Ring(props: SVGProps<SVGSVGElement>) {
|
||||
// 避免与全局样式冲突
|
||||
const animationClassName = 'svg-spinner-anim-180-ring'
|
||||
export function SvgSpinners180Ring(props: SVGProps<SVGSVGElement> & { size?: number | string }) {
|
||||
const { size = '1em', ...svgProps } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* CSS transform 硬件加速 */}
|
||||
<style>
|
||||
{`
|
||||
@keyframes svg-spinner-rotate-180-ring {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.${animationClassName} {
|
||||
transform-origin: center;
|
||||
animation: svg-spinner-rotate-180-ring 0.75s linear infinite;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
className={`${animationClassName} ${props.className || ''}`.trim()}>
|
||||
{/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"></path>
|
||||
</svg>
|
||||
</>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
{...svgProps}
|
||||
className={`animation-rotate ${svgProps.className || ''}`.trim()}>
|
||||
{/* Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE */}
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
export default SvgSpinners180Ring
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import CopyIcon from '../CopyIcon'
|
||||
|
||||
describe('CopyIcon', () => {
|
||||
it('should match snapshot with props and className', () => {
|
||||
const onClick = vi.fn()
|
||||
const { container } = render(
|
||||
<CopyIcon className="custom-class" onClick={onClick} title="Copy to clipboard" data-testid="copy-icon" />
|
||||
)
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@ -1,9 +0,0 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`CopyIcon > should match snapshot with props and className 1`] = `
|
||||
<i
|
||||
class="iconfont icon-copy custom-class"
|
||||
data-testid="copy-icon"
|
||||
title="Copy to clipboard"
|
||||
/>
|
||||
`;
|
||||
19
src/renderer/src/components/Icons/index.ts
Normal file
19
src/renderer/src/components/Icons/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
export { default as CopyIcon } from './CopyIcon'
|
||||
export { default as DeleteIcon } from './DeleteIcon'
|
||||
export * from './DownloadIcons'
|
||||
export { default as EditIcon } from './EditIcon'
|
||||
export { default as FallbackFavicon } from './FallbackFavicon'
|
||||
export { default as MinAppIcon } from './MinAppIcon'
|
||||
export * from './NutstoreIcons'
|
||||
export { default as OcrIcon } from './OcrIcon'
|
||||
export { default as ReasoningIcon } from './ReasoningIcon'
|
||||
export { default as RefreshIcon } from './RefreshIcon'
|
||||
export { default as ResetIcon } from './ResetIcon'
|
||||
export * from './SVGIcon'
|
||||
export { default as LoadingIcon } from './SvgSpinners180Ring'
|
||||
export { default as ToolIcon } from './ToolIcon'
|
||||
export { default as ToolsCallingIcon } from './ToolsCallingIcon'
|
||||
export { default as UnWrapIcon } from './UnWrapIcon'
|
||||
export { default as VisionIcon } from './VisionIcon'
|
||||
export { default as WebSearchIcon } from './WebSearchIcon'
|
||||
export { default as WrapIcon } from './WrapIcon'
|
||||
@ -1,17 +1,18 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Tooltip, TooltipProps } from 'antd'
|
||||
import { Info } from 'lucide-react'
|
||||
|
||||
type InheritedTooltipProps = Omit<TooltipProps, 'children'>
|
||||
|
||||
interface InfoTooltipProps extends InheritedTooltipProps {
|
||||
iconColor?: string
|
||||
iconSize?: string | number
|
||||
iconStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
const InfoTooltip = ({ iconColor = 'var(--color-text-3)', iconStyle, ...rest }: InfoTooltipProps) => {
|
||||
const InfoTooltip = ({ iconColor = 'var(--color-text-3)', iconSize = 14, iconStyle, ...rest }: InfoTooltipProps) => {
|
||||
return (
|
||||
<Tooltip {...rest}>
|
||||
<InfoCircleOutlined style={{ color: iconColor, ...iconStyle }} role="img" aria-label="Information" />
|
||||
<Info size={iconSize} color={iconColor} style={{ ...iconStyle }} role="img" aria-label="Information" />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { loggerService } from '@logger'
|
||||
import AiProvider from '@renderer/aiCore'
|
||||
import { RefreshIcon } from '@renderer/components/Icons'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { Model } from '@renderer/types'
|
||||
import { getErrorMessage } from '@renderer/utils'
|
||||
import { Button, InputNumber, Space, Tooltip } from 'antd'
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
import { memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@ -77,10 +77,9 @@ const InputEmbeddingDimension = ({
|
||||
<Button
|
||||
role="button"
|
||||
aria-label="Get embedding dimension"
|
||||
icon={<RefreshCw size={16} />}
|
||||
loading={loading}
|
||||
disabled={disabled}
|
||||
disabled={disabled || loading}
|
||||
onClick={handleFetchDimension}
|
||||
icon={<RefreshIcon size={16} className={loading ? 'animation-rotate' : ''} />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import ModelEditContent from '@renderer/components/ModelList/ModelEditContent'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import React from 'react'
|
||||
|
||||
interface ShowParams {
|
||||
provider: Provider
|
||||
model: Model
|
||||
}
|
||||
|
||||
interface Props extends ShowParams {
|
||||
resolve: (data?: Model) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ provider, model, resolve }) => {
|
||||
const handleUpdateModel = (updatedModel: Model) => {
|
||||
resolve(updatedModel)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
resolve(undefined) // Resolve with no data on close
|
||||
}
|
||||
|
||||
return (
|
||||
<ModelEditContent
|
||||
provider={provider}
|
||||
model={model}
|
||||
onUpdateModel={handleUpdateModel}
|
||||
open={true} // Always open when rendered by TopView
|
||||
onClose={handleClose}
|
||||
key={model.id} // Ensure re-mount when model changes
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const TopViewKey = 'EditModelPopup'
|
||||
|
||||
export default class EditModelPopup {
|
||||
static hide() {
|
||||
TopView.hide(TopViewKey)
|
||||
}
|
||||
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<Model | undefined>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>,
|
||||
TopViewKey
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,451 +0,0 @@
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { endpointTypeOptions } from '@renderer/config/endpointTypes'
|
||||
import {
|
||||
isEmbeddingModel,
|
||||
isFunctionCallingModel,
|
||||
isReasoningModel,
|
||||
isRerankModel,
|
||||
isVisionModel,
|
||||
isWebSearchModel
|
||||
} from '@renderer/config/models'
|
||||
import { useDynamicLabelWidth } from '@renderer/hooks/useDynamicLabelWidth'
|
||||
import { Model, ModelCapability, ModelType, Provider } from '@renderer/types'
|
||||
import { getDefaultGroupName, getDifference, getUnion, uniqueObjectArray } from '@renderer/utils'
|
||||
import { Button, Checkbox, Divider, Flex, Form, Input, InputNumber, message, Modal, Select, Switch } from 'antd'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ModelEditContentProps {
|
||||
provider: Provider
|
||||
model: Model
|
||||
onUpdateModel: (model: Model) => void
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const symbols = ['$', '¥', '€', '£']
|
||||
const ModelEditContent: FC<ModelEditContentProps> = ({ provider, model, onUpdateModel, open, onClose }) => {
|
||||
const [form] = Form.useForm()
|
||||
const { t } = useTranslation()
|
||||
const [showMoreSettings, setShowMoreSettings] = useState(false)
|
||||
const [currencySymbol, setCurrencySymbol] = useState(model.pricing?.currencySymbol || '$')
|
||||
const [isCustomCurrency, setIsCustomCurrency] = useState(!symbols.includes(model.pricing?.currencySymbol || '$'))
|
||||
const [modelCapabilities, setModelCapabilities] = useState(model.capabilities || [])
|
||||
const originalModelCapabilities = cloneDeep(model.capabilities || [])
|
||||
const [supportedTextDelta, setSupportedTextDelta] = useState(model.supported_text_delta)
|
||||
const [hasUserModified, setHasUserModified] = useState(false)
|
||||
|
||||
const labelWidth = useDynamicLabelWidth([t('settings.models.add.endpoint_type.label')])
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
const finalCurrencySymbol = isCustomCurrency ? values.customCurrencySymbol : values.currencySymbol
|
||||
const updatedModel: Model = {
|
||||
...model,
|
||||
id: values.id || model.id,
|
||||
name: values.name || model.name,
|
||||
group: values.group || model.group,
|
||||
endpoint_type: provider.id === 'new-api' ? values.endpointType : model.endpoint_type,
|
||||
capabilities: modelCapabilities,
|
||||
supported_text_delta: supportedTextDelta,
|
||||
pricing: {
|
||||
input_per_million_tokens: Number(values.input_per_million_tokens) || 0,
|
||||
output_per_million_tokens: Number(values.output_per_million_tokens) || 0,
|
||||
currencySymbol: finalCurrencySymbol || '$'
|
||||
}
|
||||
}
|
||||
onUpdateModel(updatedModel)
|
||||
setShowMoreSettings(false)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setShowMoreSettings(false)
|
||||
setModelCapabilities(model.capabilities || [])
|
||||
onClose()
|
||||
}
|
||||
|
||||
const currencyOptions = [
|
||||
...symbols.map((symbol) => ({ label: symbol, value: symbol })),
|
||||
{ label: t('models.price.custom'), value: 'custom' }
|
||||
]
|
||||
|
||||
const defaultTypes = [
|
||||
...(isVisionModel(model) ? ['vision'] : []),
|
||||
...(isReasoningModel(model) ? ['reasoning'] : []),
|
||||
...(isFunctionCallingModel(model) ? ['function_calling'] : []),
|
||||
...(isWebSearchModel(model) ? ['web_search'] : []),
|
||||
...(isEmbeddingModel(model) ? ['embedding'] : []),
|
||||
...(isRerankModel(model) ? ['rerank'] : [])
|
||||
]
|
||||
|
||||
const selectedTypes: string[] = getUnion(
|
||||
modelCapabilities?.filter((t) => t.isUserSelected).map((t) => t.type) || [],
|
||||
getDifference(defaultTypes, modelCapabilities?.filter((t) => t.isUserSelected === false).map((t) => t.type) || [])
|
||||
)
|
||||
|
||||
// 被rerank/embedding改变的类型
|
||||
const changedTypesRef = useRef<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (showMoreSettings) {
|
||||
const newModelCapabilities = getUnion(
|
||||
selectedTypes.map((type) => {
|
||||
const existingCapability = modelCapabilities?.find((m) => m.type === type)
|
||||
return {
|
||||
type: type as ModelType,
|
||||
isUserSelected: existingCapability?.isUserSelected ?? undefined
|
||||
}
|
||||
}),
|
||||
modelCapabilities?.filter((t) => t.isUserSelected === false),
|
||||
(item) => item.type
|
||||
)
|
||||
setModelCapabilities(newModelCapabilities)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showMoreSettings])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('models.edit')}
|
||||
open={open}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
transitionName="animation-move-down"
|
||||
centered
|
||||
afterOpenChange={(visible) => {
|
||||
if (visible) {
|
||||
form.getFieldInstance('id')?.focus()
|
||||
} else {
|
||||
setShowMoreSettings(false)
|
||||
}
|
||||
}}>
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ flex: provider.id === 'new-api' ? labelWidth : '110px' }}
|
||||
labelAlign="left"
|
||||
colon={false}
|
||||
style={{ marginTop: 15 }}
|
||||
initialValues={{
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
group: model.group,
|
||||
endpointType: model.endpoint_type,
|
||||
input_per_million_tokens: model.pricing?.input_per_million_tokens ?? 0,
|
||||
output_per_million_tokens: model.pricing?.output_per_million_tokens ?? 0,
|
||||
currencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
||||
? model.pricing?.currencySymbol || '$'
|
||||
: 'custom',
|
||||
customCurrencySymbol: symbols.includes(model.pricing?.currencySymbol || '$')
|
||||
? ''
|
||||
: model.pricing?.currencySymbol || ''
|
||||
}}
|
||||
onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="id"
|
||||
label={t('settings.models.add.model_id.label')}
|
||||
tooltip={t('settings.models.add.model_id.tooltip')}
|
||||
rules={[{ required: true }]}>
|
||||
<Flex justify="space-between" gap={5}>
|
||||
<Input
|
||||
placeholder={t('settings.models.add.model_id.placeholder')}
|
||||
spellCheck={false}
|
||||
maxLength={200}
|
||||
disabled={true}
|
||||
value={model.id}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
form.setFieldValue('name', value)
|
||||
form.setFieldValue('group', getDefaultGroupName(value))
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => {
|
||||
//copy model id
|
||||
const val = form.getFieldValue('name')
|
||||
navigator.clipboard.writeText((val.id || model.id) as string)
|
||||
message.success(t('message.copied'))
|
||||
}}>
|
||||
<CopyIcon /> {t('chat.topics.copy.title')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('settings.models.add.model_name.label')}
|
||||
tooltip={t('settings.models.add.model_name.tooltip')}>
|
||||
<Input placeholder={t('settings.models.add.model_name.placeholder')} spellCheck={false} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="group"
|
||||
label={t('settings.models.add.group_name.label')}
|
||||
tooltip={t('settings.models.add.group_name.tooltip')}>
|
||||
<Input placeholder={t('settings.models.add.group_name.placeholder')} spellCheck={false} />
|
||||
</Form.Item>
|
||||
{provider.id === 'new-api' && (
|
||||
<Form.Item
|
||||
name="endpointType"
|
||||
label={t('settings.models.add.endpoint_type.label')}
|
||||
tooltip={t('settings.models.add.endpoint_type.tooltip')}
|
||||
rules={[{ required: true, message: t('settings.models.add.endpoint_type.required') }]}>
|
||||
<Select placeholder={t('settings.models.add.endpoint_type.placeholder')}>
|
||||
{endpointTypeOptions.map((opt) => (
|
||||
<Select.Option key={opt.value} value={opt.value}>
|
||||
{t(opt.label)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item style={{ marginBottom: 8, textAlign: 'center' }}>
|
||||
<Flex justify="space-between" align="center" style={{ position: 'relative' }}>
|
||||
<Button
|
||||
color="default"
|
||||
variant="filled"
|
||||
icon={showMoreSettings ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
||||
iconPosition="end"
|
||||
onClick={() => setShowMoreSettings(!showMoreSettings)}
|
||||
style={{ color: 'var(--color-text-3)' }}>
|
||||
{t('settings.moresetting.label')}
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit" size="middle">
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
{showMoreSettings && (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Divider style={{ margin: '16px 0 16px 0' }} />
|
||||
<TypeTitle>{t('models.type.select')}:</TypeTitle>
|
||||
{(() => {
|
||||
const isDisabled = selectedTypes.includes('rerank') || selectedTypes.includes('embedding')
|
||||
|
||||
const isRerankDisabled = selectedTypes.includes('embedding')
|
||||
const isEmbeddingDisabled = selectedTypes.includes('rerank')
|
||||
const showTypeConfirmModal = (newCapability: ModelCapability) => {
|
||||
const onUpdateType = selectedTypes?.find((t) => t === newCapability.type)
|
||||
window.modal.confirm({
|
||||
title: t('settings.moresetting.warn'),
|
||||
content: t('settings.moresetting.check.warn'),
|
||||
okText: t('settings.moresetting.check.confirm'),
|
||||
cancelText: t('common.cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
cancelButtonProps: { type: 'primary' },
|
||||
onOk: () => {
|
||||
if (onUpdateType) {
|
||||
const updatedModelCapabilities = modelCapabilities?.map((t) => {
|
||||
if (t.type === newCapability.type) {
|
||||
return { ...t, isUserSelected: true }
|
||||
}
|
||||
if (
|
||||
((onUpdateType !== t.type && onUpdateType === 'rerank') ||
|
||||
(onUpdateType === 'embedding' && onUpdateType !== t.type)) &&
|
||||
t.isUserSelected !== false
|
||||
) {
|
||||
changedTypesRef.current.push(t.type)
|
||||
return { ...t, isUserSelected: false }
|
||||
}
|
||||
return t
|
||||
})
|
||||
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
|
||||
} else {
|
||||
const updatedModelCapabilities = modelCapabilities?.map((t) => {
|
||||
if (
|
||||
((newCapability.type !== t.type && newCapability.type === 'rerank') ||
|
||||
(newCapability.type === 'embedding' && newCapability.type !== t.type)) &&
|
||||
t.isUserSelected !== false
|
||||
) {
|
||||
changedTypesRef.current.push(t.type)
|
||||
return { ...t, isUserSelected: false }
|
||||
}
|
||||
if (newCapability.type === t.type) {
|
||||
return { ...t, isUserSelected: true }
|
||||
}
|
||||
return t
|
||||
})
|
||||
updatedModelCapabilities.push(newCapability as any)
|
||||
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
|
||||
}
|
||||
},
|
||||
onCancel: () => {},
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
|
||||
const handleTypeChange = (types: string[]) => {
|
||||
setHasUserModified(true) // 标记用户已进行修改
|
||||
const diff = types.length > selectedTypes.length
|
||||
if (diff) {
|
||||
const newCapability = getDifference(types, selectedTypes) // checkbox的特性,确保了newCapability只有一个元素
|
||||
showTypeConfirmModal({
|
||||
type: newCapability[0] as ModelType,
|
||||
isUserSelected: true
|
||||
})
|
||||
} else {
|
||||
const disabledTypes = getDifference(selectedTypes, types)
|
||||
const onUpdateType = modelCapabilities?.find((t) => t.type === disabledTypes[0])
|
||||
if (onUpdateType) {
|
||||
const updatedTypes = modelCapabilities?.map((t) => {
|
||||
if (t.type === disabledTypes[0]) {
|
||||
return { ...t, isUserSelected: false }
|
||||
}
|
||||
if (
|
||||
((onUpdateType !== t && onUpdateType.type === 'rerank') ||
|
||||
(onUpdateType.type === 'embedding' && onUpdateType !== t)) &&
|
||||
t.isUserSelected === false
|
||||
) {
|
||||
if (changedTypesRef.current.includes(t.type)) {
|
||||
return { ...t, isUserSelected: true }
|
||||
}
|
||||
}
|
||||
return t
|
||||
})
|
||||
setModelCapabilities(uniqueObjectArray(updatedTypes as ModelCapability[]))
|
||||
} else {
|
||||
const updatedModelCapabilities = modelCapabilities?.map((t) => {
|
||||
if (
|
||||
(disabledTypes[0] === 'rerank' && t.type !== 'rerank') ||
|
||||
(disabledTypes[0] === 'embedding' && t.type !== 'embedding' && t.isUserSelected === false)
|
||||
) {
|
||||
return { ...t, isUserSelected: true }
|
||||
}
|
||||
return t
|
||||
})
|
||||
updatedModelCapabilities.push({ type: disabledTypes[0] as ModelType, isUserSelected: false })
|
||||
setModelCapabilities(uniqueObjectArray(updatedModelCapabilities as ModelCapability[]))
|
||||
}
|
||||
changedTypesRef.current.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetTypes = () => {
|
||||
setModelCapabilities(originalModelCapabilities)
|
||||
setHasUserModified(false) // 重置后清除修改标志
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Flex justify="space-between" align="center" style={{ marginBottom: 8 }}>
|
||||
<Checkbox.Group
|
||||
value={selectedTypes}
|
||||
onChange={handleTypeChange}
|
||||
options={[
|
||||
{
|
||||
label: t('models.type.vision'),
|
||||
value: 'vision',
|
||||
disabled: isDisabled
|
||||
},
|
||||
{
|
||||
label: t('models.type.websearch'),
|
||||
value: 'web_search',
|
||||
disabled: isDisabled
|
||||
},
|
||||
{
|
||||
label: t('models.type.rerank'),
|
||||
value: 'rerank',
|
||||
disabled: isRerankDisabled
|
||||
},
|
||||
{
|
||||
label: t('models.type.embedding'),
|
||||
value: 'embedding',
|
||||
disabled: isEmbeddingDisabled
|
||||
},
|
||||
{
|
||||
label: t('models.type.reasoning'),
|
||||
value: 'reasoning',
|
||||
disabled: isDisabled
|
||||
},
|
||||
{
|
||||
label: t('models.type.function_calling'),
|
||||
value: 'function_calling',
|
||||
disabled: isDisabled
|
||||
}
|
||||
]}
|
||||
/>
|
||||
{hasUserModified && (
|
||||
<Button size="small" onClick={handleResetTypes}>
|
||||
{t('common.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<Form.Item
|
||||
name="supported_text_delta"
|
||||
label={t('settings.models.add.supported_text_delta.label')}
|
||||
tooltip={t('settings.models.add.supported_text_delta.tooltip')}>
|
||||
<Switch checked={supportedTextDelta} onChange={(checked) => setSupportedTextDelta(checked)} />
|
||||
</Form.Item>
|
||||
<TypeTitle>{t('models.price.price')}</TypeTitle>
|
||||
<Form.Item name="currencySymbol" label={t('models.price.currency')} style={{ marginBottom: 10 }}>
|
||||
<Select
|
||||
style={{ width: '100px' }}
|
||||
options={currencyOptions}
|
||||
onChange={(value) => {
|
||||
if (value === 'custom') {
|
||||
setIsCustomCurrency(true)
|
||||
setCurrencySymbol(form.getFieldValue('customCurrencySymbol') || '')
|
||||
} else {
|
||||
setIsCustomCurrency(false)
|
||||
setCurrencySymbol(value)
|
||||
}
|
||||
}}
|
||||
dropdownMatchSelectWidth={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{isCustomCurrency && (
|
||||
<Form.Item
|
||||
name="customCurrencySymbol"
|
||||
label={t('models.price.custom_currency')}
|
||||
style={{ marginBottom: 10 }}
|
||||
rules={[{ required: isCustomCurrency }]}>
|
||||
<Input
|
||||
style={{ width: '100px' }}
|
||||
placeholder={t('models.price.custom_currency_placeholder')}
|
||||
defaultValue={model.pricing?.currencySymbol}
|
||||
maxLength={5}
|
||||
onChange={(e) => setCurrencySymbol(e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item label={t('models.price.input')} name="input_per_million_tokens">
|
||||
<InputNumber
|
||||
placeholder="0.00"
|
||||
defaultValue={model.pricing?.input_per_million_tokens}
|
||||
min={0}
|
||||
step={0.01}
|
||||
precision={2}
|
||||
style={{ width: '240px' }}
|
||||
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t('models.price.output')} name="output_per_million_tokens">
|
||||
<InputNumber
|
||||
placeholder="0.00"
|
||||
defaultValue={model.pricing?.output_per_million_tokens}
|
||||
min={0}
|
||||
step={0.01}
|
||||
precision={2}
|
||||
style={{ width: '240px' }}
|
||||
addonAfter={`${currencySymbol} / ${t('models.price.million_tokens')}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const TypeTitle = styled.div`
|
||||
margin: 12px 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
`
|
||||
|
||||
export default ModelEditContent
|
||||
@ -38,7 +38,11 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
||||
const list = [defaultAssistant, ...allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))]
|
||||
const filtered = searchText
|
||||
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
|
||||
? list.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(searchText.trim().toLocaleLowerCase())
|
||||
)
|
||||
: list
|
||||
|
||||
if (searchText.trim()) {
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { type HealthResult, HealthStatusIndicator } from '@renderer/components/HealthStatusIndicator'
|
||||
import { EditIcon } from '@renderer/components/Icons'
|
||||
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||
import { ApiKeyWithStatus } from '@renderer/types/healthCheck'
|
||||
import { maskApiKey } from '@renderer/utils/api'
|
||||
import { Button, Flex, Input, InputRef, List, Popconfirm, Tooltip, Typography } from 'antd'
|
||||
import { Check, Minus, Pen, X } from 'lucide-react'
|
||||
import { Check, Minus, X } from 'lucide-react'
|
||||
import { FC, memo, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -148,7 +149,7 @@ const ApiKeyItem: FC<ApiKeyItemProps> = ({
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('common.edit')} mouseLeaveDelay={0}>
|
||||
<Button type="text" icon={<Pen size={16} />} onClick={handleEdit} disabled={disabled} />
|
||||
<Button type="text" icon={<EditIcon size={16} />} onClick={handleEdit} disabled={disabled} />
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title={t('common.delete_confirm')}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import { StreamlineGoodHealthAndWellBeing } from '@renderer/components/Icons/SVGIcon'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { usePreprocessProvider } from '@renderer/hooks/usePreprocess'
|
||||
@ -8,7 +8,7 @@ import { SettingHelpText } from '@renderer/pages/settings'
|
||||
import { isProviderSupportAuth } from '@renderer/services/ProviderService'
|
||||
import { ApiKeyWithStatus, HealthStatus } from '@renderer/types/healthCheck'
|
||||
import { Button, Card, Flex, List, Popconfirm, Space, Tooltip, Typography } from 'antd'
|
||||
import { Trash } from 'lucide-react'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -140,7 +140,12 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
|
||||
cancelText={t('common.cancel')}
|
||||
okButtonProps={{ danger: true }}>
|
||||
<Tooltip title={t('settings.provider.remove_invalid_keys')} placement="top" mouseLeaveDelay={0}>
|
||||
<Button type="text" icon={<Trash size={16} />} disabled={isChecking || !!pendingNewKey} danger />
|
||||
<Button
|
||||
type="text"
|
||||
icon={<DeleteIcon size={16} className="lucide-custom" />}
|
||||
disabled={isChecking || !!pendingNewKey}
|
||||
danger
|
||||
/>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
|
||||
@ -161,7 +166,7 @@ export const ApiKeyList: FC<ApiKeyListProps> = ({ provider, updateProvider, prov
|
||||
key="add"
|
||||
type="primary"
|
||||
onClick={handleAddNew}
|
||||
icon={<PlusOutlined />}
|
||||
icon={<Plus size={16} />}
|
||||
autoFocus={shouldAutoFocus()}
|
||||
disabled={isChecking || !!pendingNewKey}>
|
||||
{t('common.add')}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { CopyIcon, DeleteIcon } from '@renderer/components/Icons'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { Topic } from '@renderer/types'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { Copy, Save, Trash, X } from 'lucide-react'
|
||||
import { Save, X } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -49,7 +50,7 @@ const MultiSelectActionPopup: FC<Props> = ({ topic }) => {
|
||||
shape="circle"
|
||||
color="default"
|
||||
variant="text"
|
||||
icon={<Copy size={16} />}
|
||||
icon={<CopyIcon size={16} />}
|
||||
disabled={isActionDisabled}
|
||||
onClick={() => handleAction('copy')}
|
||||
/>
|
||||
@ -60,7 +61,7 @@ const MultiSelectActionPopup: FC<Props> = ({ topic }) => {
|
||||
color="danger"
|
||||
variant="text"
|
||||
danger
|
||||
icon={<Trash size={16} />}
|
||||
icon={<DeleteIcon size={16} className="lucide-custom" />}
|
||||
onClick={() => handleAction('delete')}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@ -1,9 +1,25 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import InfoTooltip from '../InfoTooltip'
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
Tooltip: ({ children, title }: { children: React.ReactNode; title: string }) => (
|
||||
<div>
|
||||
{children}
|
||||
{title && <div>{title}</div>}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('lucide-react', () => ({
|
||||
Info: ({ ref, ...props }) => (
|
||||
<div {...props} ref={ref} role="img" aria-label="Information">
|
||||
Info
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
||||
describe('InfoTooltip', () => {
|
||||
it('should match snapshot', () => {
|
||||
const { container } = render(
|
||||
@ -12,13 +28,11 @@ describe('InfoTooltip', () => {
|
||||
expect(container.firstChild).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should show tooltip on hover', async () => {
|
||||
it('should pass title prop to the underlying Tooltip component', () => {
|
||||
const tooltipText = 'This is helpful information'
|
||||
render(<InfoTooltip title={tooltipText} />)
|
||||
|
||||
const icon = screen.getByRole('img', { name: 'Information' })
|
||||
await userEvent.hover(icon)
|
||||
|
||||
expect(await screen.findByText(tooltipText)).toBeInTheDocument()
|
||||
expect(screen.getByRole('img', { name: 'Information' })).toBeInTheDocument()
|
||||
expect(screen.getByText(tooltipText)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,27 +1,71 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import InputEmbeddingDimension from '../InputEmbeddingDimension'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
aiCore: {
|
||||
getEmbeddingDimensions: vi.fn()
|
||||
},
|
||||
i18n: {
|
||||
t: (k: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'knowledge.embedding_model_required': '请选择嵌入模型',
|
||||
'knowledge.provider_not_found': '找不到提供商',
|
||||
'message.error.get_embedding_dimensions': '获取嵌入维度失败',
|
||||
'knowledge.dimensions_size_placeholder': '请输入维度大小',
|
||||
'knowledge.dimensions_auto_set': '自动设置维度'
|
||||
}
|
||||
return translations[k] || k
|
||||
const mocks = vi.hoisted(() => ({
|
||||
aiCore: {
|
||||
getEmbeddingDimensions: vi.fn()
|
||||
},
|
||||
i18n: {
|
||||
t: (k: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
'knowledge.embedding_model_required': '请选择嵌入模型',
|
||||
'knowledge.provider_not_found': '找不到提供商',
|
||||
'message.error.get_embedding_dimensions': '获取嵌入维度失败',
|
||||
'knowledge.dimensions_size_placeholder': '请输入维度大小',
|
||||
'knowledge.dimensions_auto_set': '自动设置维度'
|
||||
}
|
||||
return translations[k] || k
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock antd components to prevent flaky snapshot tests
|
||||
vi.mock('antd', () => {
|
||||
const MockSpaceCompact: React.FC<React.PropsWithChildren<{ style?: React.CSSProperties }>> = ({
|
||||
children,
|
||||
style
|
||||
}) => (
|
||||
<div data-testid="space-compact" style={style}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
const MockInputNumber = ({ ref, value, onChange, placeholder, disabled, style }: any) => (
|
||||
<input
|
||||
ref={ref}
|
||||
type="number"
|
||||
data-testid="input-number"
|
||||
placeholder={placeholder}
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.valueAsNumber)}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
|
||||
const MockButton: React.FC<any> = ({ children, onClick, disabled, icon, className, ...rest }) => (
|
||||
<button type="button" onClick={onClick} disabled={disabled} {...rest} className={className}>
|
||||
{icon}
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
const MockTooltip: React.FC<React.PropsWithChildren<{ title: string }>> = ({ children, title }) => (
|
||||
<div data-testid="tooltip" data-title={title}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
return {
|
||||
Button: MockButton,
|
||||
InputNumber: MockInputNumber,
|
||||
Space: { Compact: MockSpaceCompact },
|
||||
Tooltip: MockTooltip
|
||||
}
|
||||
})
|
||||
|
||||
// Mock dependencies
|
||||
@ -46,20 +90,10 @@ vi.mock('react-i18next', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@logger', () => ({
|
||||
loggerService: {
|
||||
withContext: () => ({
|
||||
warn: vi.fn(),
|
||||
error: vi.fn()
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('lucide-react', () => ({
|
||||
RefreshCw: (props: React.SVGProps<SVGSVGElement>) => (
|
||||
vi.mock('@renderer/components/Icons', () => ({
|
||||
RefreshIcon: (props: React.SVGProps<SVGSVGElement>) => (
|
||||
<svg data-testid="refresh-icon" aria-label="refresh" role="img" {...props}>
|
||||
RefreshCw
|
||||
RefreshIcon
|
||||
</svg>
|
||||
)
|
||||
}))
|
||||
@ -119,13 +153,11 @@ describe('InputEmbeddingDimension', () => {
|
||||
describe('functionality', () => {
|
||||
it('should call onChange when input value changes', async () => {
|
||||
const handleChange = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<InputEmbeddingDimension model={mockModel} onChange={handleChange} />)
|
||||
|
||||
const input = screen.getByPlaceholderText('请输入维度大小')
|
||||
await user.clear(input)
|
||||
await user.type(input, '2048')
|
||||
fireEvent.change(input, { target: { value: '2048' } })
|
||||
|
||||
expect(handleChange).toHaveBeenCalledWith(2048)
|
||||
})
|
||||
@ -182,7 +214,6 @@ describe('InputEmbeddingDimension', () => {
|
||||
|
||||
it('should handle null value correctly', async () => {
|
||||
const handleChange = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<InputEmbeddingDimension model={mockModel} value={null} onChange={handleChange} />)
|
||||
|
||||
@ -190,7 +221,7 @@ describe('InputEmbeddingDimension', () => {
|
||||
expect(input.value).toBe('')
|
||||
|
||||
// Should allow typing new value
|
||||
await user.type(input, '1024')
|
||||
fireEvent.change(input, { target: { value: '1024' } })
|
||||
expect(handleChange).toHaveBeenCalledWith(1024)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,28 +1,18 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`InfoTooltip > should match snapshot 1`] = `
|
||||
<span
|
||||
aria-describedby="test-id"
|
||||
aria-label="Information"
|
||||
class="anticon anticon-info-circle"
|
||||
role="img"
|
||||
style="color: rgb(24, 144, 255); font-size: 16px;"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="info-circle"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
<div>
|
||||
<div
|
||||
aria-label="Information"
|
||||
color="#1890ff"
|
||||
role="img"
|
||||
size="14"
|
||||
style="font-size: 16px;"
|
||||
>
|
||||
<path
|
||||
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z"
|
||||
/>
|
||||
<path
|
||||
d="M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
Info
|
||||
</div>
|
||||
<div>
|
||||
Test tooltip
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -2,220 +2,71 @@
|
||||
|
||||
exports[`InputEmbeddingDimension > basic rendering > should match snapshot with all props 1`] = `
|
||||
<div
|
||||
class="ant-space-compact css-dev-only-do-not-override-1261szd"
|
||||
data-testid="space-compact"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<input
|
||||
data-testid="input-number"
|
||||
placeholder="请输入维度大小"
|
||||
style="flex: 1;"
|
||||
type="number"
|
||||
value="1536"
|
||||
/>
|
||||
<div
|
||||
class="ant-input-number css-dev-only-do-not-override-1261szd ant-input-number-outlined ant-input-number-compact-item ant-input-number-compact-first-item"
|
||||
style="flex: 1 1 0%;"
|
||||
data-testid="tooltip"
|
||||
data-title="自动设置维度"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-valuemin="1"
|
||||
aria-valuenow="1536"
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
placeholder="请输入维度大小"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value="1536"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-describedby="test-id"
|
||||
aria-label="Get embedding dimension"
|
||||
class="ant-btn css-dev-only-do-not-override-1261szd ant-btn-default ant-btn-color-default ant-btn-variant-outlined ant-btn-icon-only ant-btn-compact-item ant-btn-compact-last-item"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon"
|
||||
<button
|
||||
aria-label="Get embedding dimension"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-label="refresh"
|
||||
class=""
|
||||
data-testid="refresh-icon"
|
||||
role="img"
|
||||
size="16"
|
||||
>
|
||||
RefreshCw
|
||||
RefreshIcon
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`InputEmbeddingDimension > basic rendering > should match snapshot with loading state 1`] = `
|
||||
<div
|
||||
class="ant-space-compact css-dev-only-do-not-override-1261szd"
|
||||
data-testid="space-compact"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<input
|
||||
data-testid="input-number"
|
||||
placeholder="请输入维度大小"
|
||||
style="flex: 1;"
|
||||
type="number"
|
||||
value=""
|
||||
/>
|
||||
<div
|
||||
class="ant-input-number css-dev-only-do-not-override-1261szd ant-input-number-outlined ant-input-number-compact-item ant-input-number-compact-first-item"
|
||||
style="flex: 1 1 0%;"
|
||||
data-testid="tooltip"
|
||||
data-title="自动设置维度"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
<button
|
||||
aria-label="Get embedding dimension"
|
||||
disabled=""
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-valuemin="1"
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
placeholder="请输入维度大小"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
aria-describedby="test-id"
|
||||
aria-label="Get embedding dimension"
|
||||
class="ant-btn css-dev-only-do-not-override-1261szd ant-btn-default ant-btn-color-default ant-btn-variant-outlined ant-btn-icon-only ant-btn-loading ant-btn-compact-item ant-btn-compact-last-item"
|
||||
role="button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="ant-btn-icon ant-btn-loading-icon"
|
||||
>
|
||||
<span
|
||||
aria-label="loading"
|
||||
class="anticon anticon-loading anticon-spin"
|
||||
<svg
|
||||
aria-label="refresh"
|
||||
class="animation-rotate"
|
||||
data-testid="refresh-icon"
|
||||
role="img"
|
||||
size="16"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="loading"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="0 0 1024 1024"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 00-94.3-139.9 437.71 437.71 0 00-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
RefreshIcon
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -27,7 +27,16 @@ const logger = loggerService.withContext('useAppInit')
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings()
|
||||
const {
|
||||
proxyUrl,
|
||||
proxyBypassRules,
|
||||
language,
|
||||
windowStyle,
|
||||
autoCheckUpdate,
|
||||
proxyMode,
|
||||
customCss,
|
||||
enableDataCollection
|
||||
} = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
@ -77,13 +86,13 @@ export function useAppInit() {
|
||||
|
||||
useEffect(() => {
|
||||
if (proxyMode === 'system') {
|
||||
window.api.setProxy('system')
|
||||
window.api.setProxy('system', proxyBypassRules)
|
||||
} else if (proxyMode === 'custom') {
|
||||
proxyUrl && window.api.setProxy(proxyUrl)
|
||||
proxyUrl && window.api.setProxy(proxyUrl, proxyBypassRules)
|
||||
} else {
|
||||
window.api.setProxy('')
|
||||
}
|
||||
}, [proxyUrl, proxyMode])
|
||||
}, [proxyUrl, proxyMode, proxyBypassRules])
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language || navigator.language || defaultLanguage)
|
||||
|
||||
106
src/renderer/src/hooks/useInPlaceEdit.ts
Normal file
106
src/renderer/src/hooks/useInPlaceEdit.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export interface UseInPlaceEditOptions {
|
||||
onSave: (value: string) => void
|
||||
onCancel?: () => void
|
||||
autoSelectOnStart?: boolean
|
||||
trimOnSave?: boolean
|
||||
}
|
||||
|
||||
export interface UseInPlaceEditReturn {
|
||||
isEditing: boolean
|
||||
editValue: string
|
||||
inputRef: React.RefObject<HTMLInputElement | null>
|
||||
startEdit: (initialValue: string) => void
|
||||
saveEdit: () => void
|
||||
cancelEdit: () => void
|
||||
handleKeyDown: (e: React.KeyboardEvent) => void
|
||||
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||
}
|
||||
|
||||
export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditReturn {
|
||||
const { onSave, onCancel, autoSelectOnStart = true, trimOnSave = true } = options
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editValue, setEditValue] = useState('')
|
||||
const [originalValue, setOriginalValue] = useState('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const startEdit = useCallback(
|
||||
(initialValue: string) => {
|
||||
setIsEditing(true)
|
||||
setEditValue(initialValue)
|
||||
setOriginalValue(initialValue)
|
||||
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus()
|
||||
if (autoSelectOnStart) {
|
||||
inputRef.current?.select()
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
[autoSelectOnStart]
|
||||
)
|
||||
|
||||
const saveEdit = useCallback(() => {
|
||||
const finalValue = trimOnSave ? editValue.trim() : editValue
|
||||
if (finalValue !== originalValue) {
|
||||
onSave(finalValue)
|
||||
}
|
||||
setIsEditing(false)
|
||||
setEditValue('')
|
||||
setOriginalValue('')
|
||||
}, [editValue, originalValue, onSave, trimOnSave])
|
||||
|
||||
const cancelEdit = useCallback(() => {
|
||||
setIsEditing(false)
|
||||
setEditValue('')
|
||||
setOriginalValue('')
|
||||
onCancel?.()
|
||||
}, [onCancel])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
saveEdit()
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
cancelEdit()
|
||||
}
|
||||
},
|
||||
[saveEdit, cancelEdit]
|
||||
)
|
||||
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEditValue(e.target.value)
|
||||
}, [])
|
||||
|
||||
// Handle clicks outside the input to save
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (isEditing && inputRef.current && !inputRef.current.contains(event.target as Node)) {
|
||||
saveEdit()
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}
|
||||
return
|
||||
}, [isEditing, saveEdit])
|
||||
|
||||
return {
|
||||
isEditing,
|
||||
editValue,
|
||||
inputRef,
|
||||
startEdit,
|
||||
saveEdit,
|
||||
cancelEdit,
|
||||
handleKeyDown,
|
||||
handleInputChange
|
||||
}
|
||||
}
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "Web Search Settings"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "Unable to convert to a valid tool call format: {{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "Multiple matching MCP tools exist, {{tool}} has been selected",
|
||||
"no_tool": "No matching MCP tool found for {{tool}}"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "Topic List",
|
||||
"move_to": "Move to",
|
||||
"new": "New Topic",
|
||||
"pinned": "Pinned Topics",
|
||||
"pin": "Pin Topic",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "Edit Topic Prompts"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "Topic Prompts: Additional supplementary prompts provided for the current topic"
|
||||
},
|
||||
"title": "Topics",
|
||||
"unpinned": "Unpinned Topics"
|
||||
"unpin": "Unpin Topic"
|
||||
},
|
||||
"translate": "Translate"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "Tool",
|
||||
"reasoning": "Reasoning",
|
||||
"rerank": "Reranker",
|
||||
"select": "Select Model Types",
|
||||
"select": "Model Types",
|
||||
"text": "Text",
|
||||
"vision": "Vision",
|
||||
"websearch": "WebSearch"
|
||||
@ -1559,6 +1568,7 @@
|
||||
"mode": {
|
||||
"edit": "Edit",
|
||||
"generate": "Draw",
|
||||
"merge": "Merge",
|
||||
"remix": "Remix",
|
||||
"upscale": "Upscale"
|
||||
},
|
||||
@ -2713,6 +2723,8 @@
|
||||
"jsonSaveError": "Failed to save JSON configuration.",
|
||||
"jsonSaveSuccess": "JSON configuration has been saved.",
|
||||
"logoUrl": "Logo URL",
|
||||
"longRunning": "Long Running Mode",
|
||||
"longRunningTooltip": "When enabled, the server supports long-running tasks. When receiving progress notifications, the timeout will be reset and the maximum execution time will be extended to 10 minutes.",
|
||||
"missingDependencies": "is Missing, please install it to continue.",
|
||||
"more": {
|
||||
"awesome": "Curated MCP Server List",
|
||||
@ -2959,8 +2971,8 @@
|
||||
"tooltip": "Optional e.g. GPT-4"
|
||||
},
|
||||
"supported_text_delta": {
|
||||
"label": "Incremental text output",
|
||||
"tooltip": "When the model is not supported, close the button"
|
||||
"label": "Support incremental text output",
|
||||
"tooltip": "The model returns text incrementally, rather than all at once. Enabled by default, if the model does not support it, please disable this option"
|
||||
}
|
||||
},
|
||||
"api_key": "API Key",
|
||||
@ -3016,7 +3028,6 @@
|
||||
"provider_name": "Provider Name",
|
||||
"quick_assistant_default_tag": "Default",
|
||||
"quick_assistant_model": "Quick Assistant Model",
|
||||
"quick_assistant_model_description": "Default model used by Quick Assistant",
|
||||
"quick_assistant_selection": "Select Assistant",
|
||||
"topic_naming_model": "Topic Naming Model",
|
||||
"topic_naming_model_description": "Model used when automatically naming a new topic",
|
||||
@ -3244,6 +3255,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "Proxy Address",
|
||||
"bypass": "Bypass Rules",
|
||||
"mode": {
|
||||
"custom": "Custom Proxy",
|
||||
"none": "No Proxy",
|
||||
@ -3333,7 +3345,7 @@
|
||||
"title": "Pre Process",
|
||||
"tooltip": "In Settings -> Tools, set a document preprocessing service provider. Document preprocessing can effectively improve the retrieval performance of complex format documents and scanned documents."
|
||||
},
|
||||
"title": "Tools Settings",
|
||||
"title": "Other Settings",
|
||||
"websearch": {
|
||||
"apikey": "API key",
|
||||
"blacklist": "Blacklist",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "ウェブ検索設定"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "有効なツール呼び出し形式に変換できません:{{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "複数の一致するMCPツールが存在するため、{{tool}} が選択されました",
|
||||
"no_tool": "必要なMCPツール {{tool}} が見つかりません"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "トピックリスト",
|
||||
"move_to": "移動先",
|
||||
"new": "新しいトピック",
|
||||
"pinned": "トピックを固定",
|
||||
"pin": "トピックを固定",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "トピック提示語を編集する"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供"
|
||||
},
|
||||
"title": "トピック",
|
||||
"unpinned": "固定解除"
|
||||
"unpin": "固定解除"
|
||||
},
|
||||
"translate": "翻訳"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "ツール",
|
||||
"reasoning": "推論",
|
||||
"rerank": "再順序付け",
|
||||
"select": "モデルタイプを選択",
|
||||
"select": "モデルタイプ",
|
||||
"text": "テキスト",
|
||||
"vision": "画像",
|
||||
"websearch": "ウェブ検索"
|
||||
@ -1559,6 +1568,7 @@
|
||||
"mode": {
|
||||
"edit": "部分編集",
|
||||
"generate": "画像生成",
|
||||
"merge": "マージ",
|
||||
"remix": "混合",
|
||||
"upscale": "拡大"
|
||||
},
|
||||
@ -2713,6 +2723,8 @@
|
||||
"jsonSaveError": "JSON設定の保存に失敗しました",
|
||||
"jsonSaveSuccess": "JSON設定が保存されました。",
|
||||
"logoUrl": "ロゴURL",
|
||||
"longRunning": "長時間運行モード",
|
||||
"longRunningTooltip": "このオプションを有効にすると、サーバーは長時間のタスクをサポートします。進行状況通知を受信すると、タイムアウトがリセットされ、最大実行時間が10分に延長されます。",
|
||||
"missingDependencies": "が不足しています。続行するにはインストールしてください。",
|
||||
"more": {
|
||||
"awesome": "厳選された MCP サーバーリスト",
|
||||
@ -2959,8 +2971,8 @@
|
||||
"tooltip": "例:GPT-4"
|
||||
},
|
||||
"supported_text_delta": {
|
||||
"label": "インクリメンタルテキスト出力",
|
||||
"tooltip": "モデルがサポートされていない場合は、ボタンを閉じます"
|
||||
"label": "インクリメンタルテキスト出力のサポート",
|
||||
"tooltip": "モデルがテキストをチャンクで返す場合、デフォルトで有効になっています。モデルがサポートしていない場合は、このオプションを無効にしてください"
|
||||
}
|
||||
},
|
||||
"api_key": "API キー",
|
||||
@ -3016,7 +3028,6 @@
|
||||
"provider_name": "プロバイダー名",
|
||||
"quick_assistant_default_tag": "デフォルト",
|
||||
"quick_assistant_model": "クイックアシスタントモデル",
|
||||
"quick_assistant_model_description": "クイックアシスタントで使用されるデフォルトモデル",
|
||||
"quick_assistant_selection": "アシスタントを選択します",
|
||||
"topic_naming_model": "トピック命名モデル",
|
||||
"topic_naming_model_description": "新しいトピックを自動的に命名する際に使用されるモデル",
|
||||
@ -3244,6 +3255,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "プロキシアドレス",
|
||||
"bypass": "バイパスルール",
|
||||
"mode": {
|
||||
"custom": "カスタムプロキシ",
|
||||
"none": "プロキシを使用しない",
|
||||
@ -3333,7 +3345,7 @@
|
||||
"title": "前処理",
|
||||
"tooltip": "設定 → ツールで、ドキュメント前処理サービスプロバイダーを設定します。ドキュメント前処理は、複雑な形式のドキュメントやスキャンされたドキュメントの検索性能を効果的に向上させます。"
|
||||
},
|
||||
"title": "ツール設定",
|
||||
"title": "その他の設定",
|
||||
"websearch": {
|
||||
"apikey": "APIキー",
|
||||
"blacklist": "ブラックリスト",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "Настройки веб-поиска"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "Не удалось преобразовать в действительный формат вызова инструмента: {{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "Существует несколько совпадающих инструментов MCP, выбран {{tool}}",
|
||||
"no_tool": "Не удалось сопоставить требуемый инструмент MCP {{tool}}"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "Список топиков",
|
||||
"move_to": "Переместить в",
|
||||
"new": "Новый топик",
|
||||
"pinned": "Закрепленные темы",
|
||||
"pin": "Закрепленные темы",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "Редактировать подсказки темы"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы"
|
||||
},
|
||||
"title": "Топики",
|
||||
"unpinned": "Открепленные темы"
|
||||
"unpin": "Открепленные темы"
|
||||
},
|
||||
"translate": "Перевести"
|
||||
},
|
||||
@ -1559,6 +1568,7 @@
|
||||
"mode": {
|
||||
"edit": "Редактирование",
|
||||
"generate": "Рисование",
|
||||
"merge": "Слияние",
|
||||
"remix": "Смешивание",
|
||||
"upscale": "Увеличение"
|
||||
},
|
||||
@ -2713,6 +2723,8 @@
|
||||
"jsonSaveError": "Не удалось сохранить конфигурацию JSON",
|
||||
"jsonSaveSuccess": "JSON конфигурация сохранена",
|
||||
"logoUrl": "URL логотипа",
|
||||
"longRunning": "Длительный режим работы",
|
||||
"longRunningTooltip": "Включив эту опцию, сервер будет поддерживать длительные задачи. При получении уведомлений о ходе выполнения будет сброшен тайм-аут и максимальное время выполнения будет увеличено до 10 минут.",
|
||||
"missingDependencies": "отсутствует, пожалуйста, установите для продолжения.",
|
||||
"more": {
|
||||
"awesome": "Кураторский список серверов MCP",
|
||||
@ -2959,8 +2971,8 @@
|
||||
"tooltip": "Необязательно, например, GPT-4"
|
||||
},
|
||||
"supported_text_delta": {
|
||||
"label": "Инкрементный текст вывод",
|
||||
"tooltip": "Когда модель не поддерживается, закройте кнопку"
|
||||
"label": "Поддержка инкрементного текстового вывода",
|
||||
"tooltip": "Модель возвращает текст по частям, а не одним блоком, по умолчанию включено, если модель не поддерживает, закройте эту опцию"
|
||||
}
|
||||
},
|
||||
"api_key": "API ключ",
|
||||
@ -3016,7 +3028,6 @@
|
||||
"provider_name": "Имя провайдера",
|
||||
"quick_assistant_default_tag": "умолчанию",
|
||||
"quick_assistant_model": "Модель быстрого помощника",
|
||||
"quick_assistant_model_description": "Модель по умолчанию, используемая быстрым помощником",
|
||||
"quick_assistant_selection": "Выберите помощника",
|
||||
"topic_naming_model": "Модель именования топика",
|
||||
"topic_naming_model_description": "Модель, используемая при автоматическом именовании нового топика",
|
||||
@ -3244,6 +3255,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "Адрес прокси",
|
||||
"bypass": "Правила обхода",
|
||||
"mode": {
|
||||
"custom": "Пользовательский прокси",
|
||||
"none": "Не использовать прокси",
|
||||
@ -3333,7 +3345,7 @@
|
||||
"title": "Предварительная обработка",
|
||||
"tooltip": "В настройках (Настройки -> Инструменты) укажите поставщика услуги предварительной обработки документов. Предварительная обработка документов может значительно повысить эффективность поиска для документов сложных форматов и отсканированных документов."
|
||||
},
|
||||
"title": "Настройки инструментов",
|
||||
"title": "Другие настройки",
|
||||
"websearch": {
|
||||
"apikey": "API ключ",
|
||||
"blacklist": "Черный список",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "网络搜索设置"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "无法转换为有效的工具调用格式:{{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "存在多个匹配的MCP工具,已选择 {{tool}}",
|
||||
"no_tool": "未匹配到所需的MCP工具 {{tool}}"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "话题列表",
|
||||
"move_to": "移动到",
|
||||
"new": "开始新对话",
|
||||
"pinned": "固定话题",
|
||||
"pin": "固定话题",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "编辑话题提示词"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "话题提示词:针对当前话题提供额外的补充提示词"
|
||||
},
|
||||
"title": "话题",
|
||||
"unpinned": "取消固定"
|
||||
"unpin": "取消固定"
|
||||
},
|
||||
"translate": "翻译"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "工具",
|
||||
"reasoning": "推理",
|
||||
"rerank": "重排",
|
||||
"select": "选择模型类型",
|
||||
"select": "模型类型",
|
||||
"text": "文本",
|
||||
"vision": "视觉",
|
||||
"websearch": "联网"
|
||||
@ -1559,6 +1568,7 @@
|
||||
"mode": {
|
||||
"edit": "编辑",
|
||||
"generate": "绘图",
|
||||
"merge": "合并",
|
||||
"remix": "混合",
|
||||
"upscale": "高清增强"
|
||||
},
|
||||
@ -2713,6 +2723,8 @@
|
||||
"jsonSaveError": "保存 JSON 配置失败",
|
||||
"jsonSaveSuccess": "JSON 配置已保存",
|
||||
"logoUrl": "标志网址",
|
||||
"longRunning": "长时间运行模式",
|
||||
"longRunningTooltip": "启用后,服务器支持长时间任务,接收到进度通知时会重置超时计时器,并延长最大超时时间至10分钟",
|
||||
"missingDependencies": "缺失,请安装它以继续",
|
||||
"more": {
|
||||
"awesome": "精选的 MCP 服务器列表",
|
||||
@ -2959,8 +2971,8 @@
|
||||
"tooltip": "例如 GPT-4"
|
||||
},
|
||||
"supported_text_delta": {
|
||||
"label": "增量文本输出",
|
||||
"tooltip": "当模型不支持的时候,将该按钮关闭"
|
||||
"label": "支持增量文本输出",
|
||||
"tooltip": "模型每次返回文本增量,而不是一次性返回所有文本,默认开启,如果模型不支持,请关闭"
|
||||
}
|
||||
},
|
||||
"api_key": "API 密钥",
|
||||
@ -3016,7 +3028,6 @@
|
||||
"provider_name": "服务商名称",
|
||||
"quick_assistant_default_tag": "默认",
|
||||
"quick_assistant_model": "快捷助手模型",
|
||||
"quick_assistant_model_description": "快捷助手使用的默认模型",
|
||||
"quick_assistant_selection": "选择助手",
|
||||
"topic_naming_model": "话题命名模型",
|
||||
"topic_naming_model_description": "自动命名新话题时使用的模型",
|
||||
@ -3244,6 +3255,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "代理地址",
|
||||
"bypass": "代理绕过规则",
|
||||
"mode": {
|
||||
"custom": "自定义代理",
|
||||
"none": "不使用代理",
|
||||
@ -3333,7 +3345,7 @@
|
||||
"title": "文档预处理",
|
||||
"tooltip": "在设置 -> 工具中设置文档预处理服务商,文档预处理可以有效提升复杂格式文档与扫描版文档的检索效果"
|
||||
},
|
||||
"title": "工具设置",
|
||||
"title": "其他设置",
|
||||
"websearch": {
|
||||
"apikey": "API 密钥",
|
||||
"blacklist": "黑名单",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "網路搜尋設定"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "無法轉換為有效的工具呼叫格式:{{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "存在多個匹配的MCP工具,已選擇 {{tool}}",
|
||||
"no_tool": "未匹配到所需的MCP工具 {{tool}}"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "話題列表",
|
||||
"move_to": "移動到",
|
||||
"new": "開始新對話",
|
||||
"pinned": "固定話題",
|
||||
"pin": "固定話題",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "編輯話題提示詞"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "話題提示詞:針對目前話題提供額外的補充提示詞"
|
||||
},
|
||||
"title": "話題",
|
||||
"unpinned": "取消固定"
|
||||
"unpin": "取消固定"
|
||||
},
|
||||
"translate": "翻譯"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "工具",
|
||||
"reasoning": "推理",
|
||||
"rerank": "重排",
|
||||
"select": "選擇模型類型",
|
||||
"select": "模型類型",
|
||||
"text": "文字",
|
||||
"vision": "視覺",
|
||||
"websearch": "網路搜尋"
|
||||
@ -1559,6 +1568,7 @@
|
||||
"mode": {
|
||||
"edit": "編輯",
|
||||
"generate": "繪圖",
|
||||
"merge": "合併",
|
||||
"remix": "混合",
|
||||
"upscale": "放大"
|
||||
},
|
||||
@ -2713,6 +2723,8 @@
|
||||
"jsonSaveError": "保存 JSON 配置失敗",
|
||||
"jsonSaveSuccess": "JSON 配置已儲存",
|
||||
"logoUrl": "標誌網址",
|
||||
"longRunning": "長時間運行模式",
|
||||
"longRunningTooltip": "啟用後,伺服器支援長時間任務,接收到進度通知時會重置超時計時器,並延長最大超時時間至10分鐘",
|
||||
"missingDependencies": "缺失,請安裝它以繼續",
|
||||
"more": {
|
||||
"awesome": "精選的 MCP 伺服器清單",
|
||||
@ -2959,8 +2971,8 @@
|
||||
"tooltip": "例如 GPT-4"
|
||||
},
|
||||
"supported_text_delta": {
|
||||
"label": "增量文本輸出",
|
||||
"tooltip": "當模型不支持的時候,將該按鈕關閉"
|
||||
"label": "支持增量文本輸出",
|
||||
"tooltip": "模型每次返回文本增量,而不是一次性返回所有文本,預設開啟,如果模型不支持,請關閉"
|
||||
}
|
||||
},
|
||||
"api_key": "API 密鑰",
|
||||
@ -3016,7 +3028,6 @@
|
||||
"provider_name": "提供者名稱",
|
||||
"quick_assistant_default_tag": "預設",
|
||||
"quick_assistant_model": "快捷助手模型",
|
||||
"quick_assistant_model_description": "快捷助手使用的預設模型",
|
||||
"quick_assistant_selection": "選擇助手",
|
||||
"topic_naming_model": "話題命名模型",
|
||||
"topic_naming_model_description": "自動命名新話題時使用的模型",
|
||||
@ -3244,6 +3255,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "代理伺服器位址",
|
||||
"bypass": "代理略過規則",
|
||||
"mode": {
|
||||
"custom": "自訂代理伺服器",
|
||||
"none": "不使用代理伺服器",
|
||||
@ -3333,7 +3345,7 @@
|
||||
"title": "前置處理",
|
||||
"tooltip": "在「設定」->「工具」中設定文件預處理服務供應商。文件預處理可有效提升複雜格式文件及掃描文件的檢索效能"
|
||||
},
|
||||
"title": "工具設定",
|
||||
"title": "其他設定",
|
||||
"websearch": {
|
||||
"apikey": "API 金鑰",
|
||||
"blacklist": "黑名單",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "Ρυθμίσεις αναζήτησης στο διαδίκτυο"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "Δεν είναι δυνατή η μετατροπή σε έγκυρη μορφή κλήσης εργαλείου: {{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "Υπάρχουν πολλαπλά εργαλεία MCP που ταιριάζουν, επιλέχθηκε το {{tool}}",
|
||||
"no_tool": "Δεν βρέθηκε το απαιτούμενο εργαλείο MCP {{tool}}"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "Λίστα θεμάτων",
|
||||
"move_to": "Μετακίνηση στο",
|
||||
"new": "Ξεκινήστε νέα συζήτηση",
|
||||
"pinned": "Σταθερά θέματα",
|
||||
"pin": "Σταθερά θέματα",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "Επεξεργασία προσδοκώμενων όριων"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "Προσδοκώμενα όρια: προσθέτει επιπλέον επιστημονικές προσθήκες για το παρόν θέμα"
|
||||
},
|
||||
"title": "Θέματα",
|
||||
"unpinned": "Αποστέλλω"
|
||||
"unpin": "Ξεκαρφίτσωμα"
|
||||
},
|
||||
"translate": "Μετάφραση"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "κλήση συνάρτησης",
|
||||
"reasoning": "λογική",
|
||||
"rerank": "Τακτοποιώ",
|
||||
"select": "Επιλέξτε τύπο μοντέλου",
|
||||
"select": "Τύποι μοντέλου",
|
||||
"text": "κείμενο",
|
||||
"vision": "εικόνα",
|
||||
"websearch": "δικτύωση"
|
||||
@ -2998,6 +3007,7 @@
|
||||
"label": "Προσθήκη μοντέλων από τη λίστα"
|
||||
},
|
||||
"add_whole_group": "Προσθήκη ολόκληρης ομάδας",
|
||||
"refetch_list": "Επαναλήψτε τη λήψη της λίστας μοντέλων",
|
||||
"remove_listed": "Αφαίρεση μοντέλων από τη λίστα",
|
||||
"remove_model": "Αφαίρεση Μοντέλου",
|
||||
"remove_whole_group": "Αφαίρεση ολόκληρης ομάδας"
|
||||
@ -3015,7 +3025,6 @@
|
||||
"provider_name": "Όνομα Παρόχου",
|
||||
"quick_assistant_default_tag": "Προεπιλογή",
|
||||
"quick_assistant_model": "Μοντέλο Γρήγορου Βοηθού",
|
||||
"quick_assistant_model_description": "Προεπιλεγμένο μοντέλο που χρησιμοποιείται από το Γρήγορο Βοηθό",
|
||||
"quick_assistant_selection": "Επιλογή Βοηθού",
|
||||
"topic_naming_model": "Μοντέλο αναδόμησης θεμάτων",
|
||||
"topic_naming_model_description": "Το μοντέλο που χρησιμοποιείται όταν αυτόματα ονομάζεται ένα νέο θέμα",
|
||||
@ -3243,6 +3252,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "Διεύθυνση διαμεσολάβησης",
|
||||
"bypass": "Κανόνες Παράκαμψης",
|
||||
"mode": {
|
||||
"custom": "προσαρμοσμένη προξενική",
|
||||
"none": "χωρίς πρόξενο",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "Configuración de búsqueda en red"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "No se puede convertir al formato de llamada de herramienta válido: {{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "Existen múltiples herramientas MCP coincidentes, se ha seleccionado {{tool}}",
|
||||
"no_tool": "No se encontró la herramienta MCP requerida {{tool}}"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "Lista de temas",
|
||||
"move_to": "Mover a",
|
||||
"new": "Iniciar nueva conversación",
|
||||
"pinned": "Fijar tema",
|
||||
"pin": "Fijar tema",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "Editar palabras clave del tema"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "Palabras clave del tema: proporcionar indicaciones adicionales para el tema actual"
|
||||
},
|
||||
"title": "Tema",
|
||||
"unpinned": "Quitar fijación"
|
||||
"unpin": "Quitar fijación"
|
||||
},
|
||||
"translate": "Traducir"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "Llamada a función",
|
||||
"reasoning": "Razonamiento",
|
||||
"rerank": "Reclasificar",
|
||||
"select": "Seleccionar tipo de modelo",
|
||||
"select": "Tipos de modelo",
|
||||
"text": "Texto",
|
||||
"vision": "Imagen",
|
||||
"websearch": "Búsqueda en línea"
|
||||
@ -2998,6 +3007,7 @@
|
||||
"label": "Agregar modelo en la lista"
|
||||
},
|
||||
"add_whole_group": "Agregar todo el grupo",
|
||||
"refetch_list": "Volver a obtener la lista de modelos",
|
||||
"remove_listed": "Eliminar modelo de la lista",
|
||||
"remove_model": "Eliminar modelo",
|
||||
"remove_whole_group": "Eliminar todo el grupo"
|
||||
@ -3015,7 +3025,6 @@
|
||||
"provider_name": "Nombre del proveedor",
|
||||
"quick_assistant_default_tag": "Predeterminado",
|
||||
"quick_assistant_model": "Modelo del asistente rápido",
|
||||
"quick_assistant_model_description": "Modelo predeterminado utilizado por el asistente rápido",
|
||||
"quick_assistant_selection": "Seleccionar asistente",
|
||||
"topic_naming_model": "Modelo de nombramiento de temas",
|
||||
"topic_naming_model_description": "Modelo utilizado para nombrar temas automáticamente",
|
||||
@ -3243,6 +3252,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "Dirección del proxy",
|
||||
"bypass": "Reglas de omisión",
|
||||
"mode": {
|
||||
"custom": "Proxy personalizado",
|
||||
"none": "No usar proxy",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "Paramètres de recherche en ligne"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "Impossible de convertir au format d'appel d'outil valide : {{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "Il existe plusieurs outils MCP correspondants, {{tool}} a été sélectionné",
|
||||
"no_tool": "Aucun outil MCP requis {{tool}} n'a été trouvé"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "Liste des sujets",
|
||||
"move_to": "Déplacer vers",
|
||||
"new": "Commencer une nouvelle conversation",
|
||||
"pinned": "Fixer le sujet",
|
||||
"pin": "Fixer le sujet",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "Modifier les indicateurs de sujet"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "Indicateurs de sujet : fournir des indications supplémentaires pour le sujet actuel"
|
||||
},
|
||||
"title": "Sujet",
|
||||
"unpinned": "Annuler le fixage"
|
||||
"unpin": "Annuler le fixage"
|
||||
},
|
||||
"translate": "Traduire"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "Appel de fonction",
|
||||
"reasoning": "Raisonnement",
|
||||
"rerank": "Reclasser",
|
||||
"select": "Sélectionnez le type de modèle",
|
||||
"select": "Types de modèle",
|
||||
"text": "Texte",
|
||||
"vision": "Image",
|
||||
"websearch": "Recherche web"
|
||||
@ -2998,6 +3007,7 @@
|
||||
"label": "Ajouter le modèle dans la liste"
|
||||
},
|
||||
"add_whole_group": "Ajouter tout le groupe",
|
||||
"refetch_list": "Récupérer à nouveau la liste des modèles",
|
||||
"remove_listed": "Supprimer un modèle de la liste",
|
||||
"remove_model": "Supprimer le modèle",
|
||||
"remove_whole_group": "Supprimer tout le groupe"
|
||||
@ -3015,7 +3025,6 @@
|
||||
"provider_name": "Nom du fournisseur",
|
||||
"quick_assistant_default_tag": "Par défaut",
|
||||
"quick_assistant_model": "Modèle de l'assistant rapide",
|
||||
"quick_assistant_model_description": "Modèle par défaut utilisé par l'assistant rapide",
|
||||
"quick_assistant_selection": "Sélectionner l'assistant",
|
||||
"topic_naming_model": "Modèle de renommage des sujets",
|
||||
"topic_naming_model_description": "Modèle utilisé pour le renommage automatique des nouveaux sujets",
|
||||
@ -3243,6 +3252,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "Adresse du proxy",
|
||||
"bypass": "Règles de contournement",
|
||||
"mode": {
|
||||
"custom": "Proxy personnalisé",
|
||||
"none": "Ne pas utiliser de proxy",
|
||||
|
||||
@ -383,6 +383,15 @@
|
||||
"settings": "Configurações de Pesquisa na Web"
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"error": {
|
||||
"parse_tool_call": "Não é possível converter para um formato de chamada de ferramenta válido: {{toolCall}}"
|
||||
},
|
||||
"warning": {
|
||||
"multiple_tools": "Existem várias ferramentas MCP correspondentes, a ferramenta {{tool}} foi selecionada",
|
||||
"no_tool": "Nenhuma ferramenta MCP necessária correspondente encontrada {{tool}}"
|
||||
}
|
||||
},
|
||||
"message": {
|
||||
"new": {
|
||||
"branch": {
|
||||
@ -596,7 +605,7 @@
|
||||
"list": "Lista de tópicos",
|
||||
"move_to": "Mover para",
|
||||
"new": "Começar nova conversa",
|
||||
"pinned": "Fixar tópico",
|
||||
"pin": "Fixar tópico",
|
||||
"prompt": {
|
||||
"edit": {
|
||||
"title": "Editar prompt do tópico"
|
||||
@ -605,7 +614,7 @@
|
||||
"tips": "Prompt do tópico: fornecer prompts adicionais para o tópico atual"
|
||||
},
|
||||
"title": "Tópicos",
|
||||
"unpinned": "Desfixar"
|
||||
"unpin": "Desfixar"
|
||||
},
|
||||
"translate": "Traduzir"
|
||||
},
|
||||
@ -1461,7 +1470,7 @@
|
||||
"function_calling": "chamada de função",
|
||||
"reasoning": "raciocínio",
|
||||
"rerank": "Reclassificar",
|
||||
"select": "selecione o tipo de modelo",
|
||||
"select": "Tipos de modelo",
|
||||
"text": "texto",
|
||||
"vision": "imagem",
|
||||
"websearch": "Procurar na web"
|
||||
@ -2998,6 +3007,7 @@
|
||||
"label": "Adicionar modelo da lista"
|
||||
},
|
||||
"add_whole_group": "Adicionar todo o grupo",
|
||||
"refetch_list": "Obter novamente a lista de modelos",
|
||||
"remove_listed": "Remover modelo da lista",
|
||||
"remove_model": "Remover Modelo",
|
||||
"remove_whole_group": "Remover todo o grupo"
|
||||
@ -3015,7 +3025,6 @@
|
||||
"provider_name": "Nome do Provedor",
|
||||
"quick_assistant_default_tag": "Padrão",
|
||||
"quick_assistant_model": "Modelo do Assistente Rápido",
|
||||
"quick_assistant_model_description": "Modelo padrão usado pelo assistente rápido",
|
||||
"quick_assistant_selection": "Selecionar Assistente",
|
||||
"topic_naming_model": "Modelo de nomenclatura de tópicos",
|
||||
"topic_naming_model_description": "Modelo usado para nomear tópicos automaticamente",
|
||||
@ -3243,6 +3252,7 @@
|
||||
},
|
||||
"proxy": {
|
||||
"address": "Endereço do proxy",
|
||||
"bypass": "Regras de Contorno",
|
||||
"mode": {
|
||||
"custom": "Proxy Personalizado",
|
||||
"none": "Não Usar Proxy",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
@ -43,27 +44,22 @@ const AgentsPage: FC = () => {
|
||||
}, [systemAgents, userAgents])
|
||||
|
||||
const filteredAgents = useMemo(() => {
|
||||
let agents: Agent[] = []
|
||||
|
||||
if (search.trim()) {
|
||||
const uniqueAgents = new Map<string, Agent>()
|
||||
|
||||
Object.entries(agentGroups).forEach(([, agents]) => {
|
||||
agents.forEach((agent) => {
|
||||
if (
|
||||
(agent.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(search.toLowerCase())) &&
|
||||
!uniqueAgents.has(agent.name)
|
||||
) {
|
||||
uniqueAgents.set(agent.name, agent)
|
||||
}
|
||||
})
|
||||
})
|
||||
agents = Array.from(uniqueAgents.values())
|
||||
} else {
|
||||
agents = agentGroups[activeGroup] || []
|
||||
// 搜索框为空直接返回「我的」分组下的 agent
|
||||
if (!search.trim()) {
|
||||
return agentGroups[activeGroup] || []
|
||||
}
|
||||
return agents.filter((agent) => agent.name.toLowerCase().includes(search.toLowerCase()))
|
||||
const uniqueAgents = new Map<string, Agent>()
|
||||
Object.entries(agentGroups).forEach(([, agents]) => {
|
||||
agents.forEach((agent) => {
|
||||
if (
|
||||
agent.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(search.toLowerCase())
|
||||
) {
|
||||
uniqueAgents.set(agent.id, agent)
|
||||
}
|
||||
})
|
||||
})
|
||||
return Array.from(uniqueAgents.values())
|
||||
}, [agentGroups, activeGroup, search])
|
||||
|
||||
const { t, i18n } = useTranslation()
|
||||
@ -218,11 +214,11 @@ const AgentsPage: FC = () => {
|
||||
{getLocalizedGroupName(group)}
|
||||
</Flex>
|
||||
{
|
||||
<div style={{ minWidth: 40, textAlign: 'center' }}>
|
||||
<HStack alignItems="center" justifyContent="center" style={{ minWidth: 40 }}>
|
||||
<CustomTag color="#A0A0A0" size={8}>
|
||||
{agentGroups[group].length}
|
||||
</CustomTag>
|
||||
</div>
|
||||
</HStack>
|
||||
}
|
||||
</Flex>
|
||||
}
|
||||
|
||||
@ -1,12 +1,5 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
ExportOutlined,
|
||||
PlusOutlined,
|
||||
SortAscendingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import CustomTag from '@renderer/components/CustomTag'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
@ -14,6 +7,7 @@ import type { Agent } from '@renderer/types'
|
||||
import { getLeadingEmoji } from '@renderer/utils'
|
||||
import { Button, Dropdown } from 'antd'
|
||||
import { t } from 'i18next'
|
||||
import { ArrowDownAZ, Ellipsis, PlusIcon, SquareArrowOutUpRight } from 'lucide-react'
|
||||
import { type FC, memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -66,7 +60,7 @@ const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupNa
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('agents.edit.title'),
|
||||
icon: <EditOutlined />,
|
||||
icon: <EditIcon size={14} />,
|
||||
onClick: (e: any) => {
|
||||
e.domEvent.stopPropagation()
|
||||
AssistantSettingsPopup.show({ assistant: agent })
|
||||
@ -75,7 +69,7 @@ const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupNa
|
||||
{
|
||||
key: 'create',
|
||||
label: t('agents.add.button'),
|
||||
icon: <PlusOutlined />,
|
||||
icon: <PlusIcon size={14} />,
|
||||
onClick: (e: any) => {
|
||||
e.domEvent.stopPropagation()
|
||||
createAssistantFromAgent(agent)
|
||||
@ -84,7 +78,7 @@ const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupNa
|
||||
{
|
||||
key: 'sort',
|
||||
label: t('agents.sorting.title'),
|
||||
icon: <SortAscendingOutlined />,
|
||||
icon: <ArrowDownAZ size={14} />,
|
||||
onClick: (e: any) => {
|
||||
e.domEvent.stopPropagation()
|
||||
ManageAgentsPopup.show()
|
||||
@ -93,7 +87,7 @@ const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupNa
|
||||
{
|
||||
key: 'export',
|
||||
label: t('agents.export.agent'),
|
||||
icon: <ExportOutlined />,
|
||||
icon: <SquareArrowOutUpRight size={14} />,
|
||||
onClick: (e: any) => {
|
||||
e.domEvent.stopPropagation()
|
||||
exportAgent()
|
||||
@ -102,7 +96,7 @@ const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupNa
|
||||
{
|
||||
key: 'delete',
|
||||
label: t('common.delete'),
|
||||
icon: <DeleteOutlined />,
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: (e: any) => {
|
||||
e.domEvent.stopPropagation()
|
||||
@ -173,7 +167,7 @@ const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupNa
|
||||
color="default"
|
||||
variant="filled"
|
||||
shape="circle"
|
||||
icon={<EllipsisOutlined />}
|
||||
icon={<Ellipsis size={14} color="var(--color-text-3)" />}
|
||||
/>
|
||||
</Dropdown>
|
||||
</AgentCardHeaderInfoAction>
|
||||
|
||||
@ -98,6 +98,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
title={t('agents.import.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
maskClosable={false}
|
||||
footer={
|
||||
<Flex justify="end" gap={8}>
|
||||
<Button onClick={onCancel}>{t('common.cancel')}</Button>
|
||||
|
||||
@ -104,6 +104,7 @@ const NewAppButton: FC<Props> = ({ size = 60 }) => {
|
||||
setIsModalVisible(false)
|
||||
setFileList([])
|
||||
}}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
transitionName="animation-move-down"
|
||||
centered>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { handleDelete } from '@renderer/services/FileAction'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
@ -68,7 +69,7 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
||||
icon: <ExclamationCircleOutlined style={{ color: 'red' }} />
|
||||
})
|
||||
}}>
|
||||
<DeleteOutlined />
|
||||
<DeleteIcon size={14} className="lucide-custom" />
|
||||
</DeleteButton>
|
||||
</ImageWrapper>
|
||||
</Col>
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
SortAscendingOutlined,
|
||||
SortDescendingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import db from '@renderer/databases'
|
||||
import { getFileFieldLabel } from '@renderer/i18n/label'
|
||||
@ -16,7 +11,14 @@ import { formatFileSize } from '@renderer/utils'
|
||||
import { Button, Empty, Flex, Popconfirm } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
|
||||
import {
|
||||
ArrowDownNarrowWide,
|
||||
ArrowUpWideNarrow,
|
||||
File as FileIcon,
|
||||
FileImage,
|
||||
FileText,
|
||||
FileType as FileTypeIcon
|
||||
} from 'lucide-react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -54,15 +56,16 @@ const FilesPage: FC = () => {
|
||||
created_at_unix: dayjs(file.created_at).unix(),
|
||||
actions: (
|
||||
<Flex align="center" gap={0} style={{ opacity: 0.7 }}>
|
||||
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
|
||||
<Button type="text" icon={<EditIcon size={14} />} onClick={() => handleRename(file.id)} />
|
||||
<Popconfirm
|
||||
title={t('files.delete.title')}
|
||||
description={t('files.delete.content')}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={() => handleDelete(file.id, t)}
|
||||
placement="left"
|
||||
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||
<Button type="text" danger icon={<DeleteIcon size={14} className="lucide-custom" />} />
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
)
|
||||
@ -108,7 +111,8 @@ const FilesPage: FC = () => {
|
||||
}
|
||||
}}>
|
||||
{getFileFieldLabel(field)}
|
||||
{sortField === field && (sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
||||
{sortField === field &&
|
||||
(sortOrder === 'desc' ? <ArrowUpWideNarrow size={12} /> : <ArrowDownNarrowWide size={12} />)}
|
||||
</SortButton>
|
||||
))}
|
||||
</SortContainer>
|
||||
|
||||
@ -940,8 +940,8 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||
{loading && (
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} mouseLeaveDelay={0} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
||||
<CirclePause style={{ color: 'var(--color-error)', fontSize: 20 }} />
|
||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2 }}>
|
||||
<CirclePause size={20} color="var(--color-error)" />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@ -158,6 +158,7 @@ const QuickPhrasesButton = ({ ref, setInputValue, resizeTextArea, ToolbarButton,
|
||||
title={t('settings.quickPhrase.add')}
|
||||
open={isModalOpen}
|
||||
onOk={handleModalOk}
|
||||
maskClosable={false}
|
||||
onCancel={() => {
|
||||
setIsModalOpen(false)
|
||||
setFormData({ title: '', content: '', location: 'global' })
|
||||
|
||||
@ -15,6 +15,7 @@ const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
|
||||
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
|
||||
fontSize: 22,
|
||||
transition: 'all 0.2s',
|
||||
marginTop: 1,
|
||||
marginRight: 2
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { ArrowUpOutlined, MenuOutlined } from '@ant-design/icons'
|
||||
import { HStack, VStack } from '@renderer/components/Layout'
|
||||
import MaxContextCount from '@renderer/components/MaxContextCount'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Divider, Popover } from 'antd'
|
||||
import { ArrowUp, MenuIcon } from 'lucide-react'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -49,13 +49,14 @@ const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCou
|
||||
<Popover content={PopoverContent} arrow={false}>
|
||||
<HStack>
|
||||
<HStack style={{ alignItems: 'center' }}>
|
||||
<MenuOutlined /> {contextCount.current}
|
||||
<MenuIcon size={12} className="icon" />
|
||||
{contextCount.current}
|
||||
<SlashSeparatorSpan>/</SlashSeparatorSpan>
|
||||
<MaxContextCount maxContext={contextCount.max} />
|
||||
</HStack>
|
||||
<Divider type="vertical" style={{ marginTop: 0, marginLeft: 5, marginRight: 5 }} />
|
||||
<Divider type="vertical" style={{ marginTop: 3, marginLeft: 5, marginRight: 3 }} />
|
||||
<HStack style={{ alignItems: 'center' }}>
|
||||
<ArrowUpOutlined />
|
||||
<ArrowUp size={12} className="icon" />
|
||||
{inputTokenCount}
|
||||
<SlashSeparatorSpan>/</SlashSeparatorSpan>
|
||||
{estimateTokenCount}
|
||||
@ -77,8 +78,7 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.anticon {
|
||||
font-size: 10px;
|
||||
.icon {
|
||||
margin-right: 3px;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { CopyIcon } from '@renderer/components/Icons'
|
||||
import store from '@renderer/store'
|
||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import { Check } from 'lucide-react'
|
||||
import React, { memo, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -40,11 +41,7 @@ const Table: React.FC<Props> = ({ children, node, blockId }) => {
|
||||
<ToolbarWrapper className="table-toolbar">
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ToolButton role="button" aria-label={t('common.copy')} onClick={handleCopyTable}>
|
||||
{copied ? (
|
||||
<Check size={14} style={{ color: 'var(--color-primary)' }} data-testid="check-icon" />
|
||||
) : (
|
||||
<Copy size={14} data-testid="copy-icon" />
|
||||
)}
|
||||
{copied ? <Check size={14} color="var(--color-primary)" /> : <CopyIcon size={14} />}
|
||||
</ToolButton>
|
||||
</Tooltip>
|
||||
</ToolbarWrapper>
|
||||
|
||||
@ -28,6 +28,14 @@ vi.mock('@renderer/store/messageBlock', () => ({
|
||||
messageBlocksSelectors: mocks.messageBlocksSelectors
|
||||
}))
|
||||
|
||||
vi.mock('@renderer/components/Icons', () => ({
|
||||
CopyIcon: ({ size }: { size: number }) => <div data-testid="copy-icon" style={{ width: size, height: size }} />
|
||||
}))
|
||||
|
||||
vi.mock('lucide-react', () => ({
|
||||
Check: ({ size }: { size: number }) => <div data-testid="check-icon" style={{ width: size, height: size }} />
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key
|
||||
|
||||
@ -71,32 +71,10 @@ exports[`Table > rendering > should match snapshot 1`] = `
|
||||
class="c2"
|
||||
role="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="lucide lucide-copy"
|
||||
<div
|
||||
data-testid="copy-icon"
|
||||
fill="none"
|
||||
height="14"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="14"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
height="14"
|
||||
rx="2"
|
||||
ry="2"
|
||||
width="14"
|
||||
x="8"
|
||||
y="8"
|
||||
/>
|
||||
<path
|
||||
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
|
||||
/>
|
||||
</svg>
|
||||
style="width: 14px; height: 14px;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import { MessageBlockStatus, MessageBlockType, type PlaceholderMessageBlock } from '@renderer/types/newMessage'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
@ -10,7 +10,7 @@ const PlaceholderBlock: React.FC<PlaceholderBlockProps> = ({ block }) => {
|
||||
if (block.status === MessageBlockStatus.PROCESSING && block.type === MessageBlockType.UNKNOWN) {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<SvgSpinners180Ring />
|
||||
<LoadingIcon />
|
||||
</MessageContentLoading>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { CheckOutlined, EditOutlined, QuestionCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
@ -32,20 +33,7 @@ import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
|
||||
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||
import { Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
AtSign,
|
||||
Copy,
|
||||
FilePenLine,
|
||||
Languages,
|
||||
ListChecks,
|
||||
Menu,
|
||||
RefreshCw,
|
||||
Save,
|
||||
Share,
|
||||
Split,
|
||||
ThumbsUp,
|
||||
Trash
|
||||
} from 'lucide-react'
|
||||
import { AtSign, Check, FilePenLine, Languages, ListChecks, Menu, Save, Split, ThumbsUp, Upload } from 'lucide-react'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -223,7 +211,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{
|
||||
label: t('chat.save.label'),
|
||||
key: 'save',
|
||||
icon: <Save size={15} color="var(--color-icon)" style={{ marginTop: 3 }} />,
|
||||
icon: <Save size={15} />,
|
||||
children: [
|
||||
{
|
||||
label: t('chat.save.file.title'),
|
||||
@ -245,7 +233,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{
|
||||
label: t('chat.topics.export.title'),
|
||||
key: 'export',
|
||||
icon: <Share size={15} color="var(--color-icon)" style={{ marginTop: 3 }} />,
|
||||
icon: <Upload size={15} />,
|
||||
children: [
|
||||
exportMenuOptions.plain_text && {
|
||||
label: t('chat.topics.copy.plain_text'),
|
||||
@ -440,28 +428,28 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
className="message-action-button"
|
||||
onClick={() => handleResendUserMessage()}
|
||||
$softHoverBg={isBubbleStyle}>
|
||||
<SyncOutlined />
|
||||
<RefreshIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
|
||||
<EditOutlined />
|
||||
<EditIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}>
|
||||
{!copied && <Copy size={15} />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
{!copied && <CopyIcon size={15} />}
|
||||
{copied && <Check size={15} color="var(--color-primary)" />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{isAssistantMessage && (
|
||||
<Popconfirm
|
||||
title={t('message.regenerate.confirm')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
icon={<InfoCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={onRegenerate}
|
||||
onOpenChange={(open) => open && setShowRegenerateTooltip(false)}>
|
||||
<Tooltip
|
||||
@ -470,7 +458,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
open={showRegenerateTooltip}
|
||||
onOpenChange={setShowRegenerateTooltip}>
|
||||
<ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
|
||||
<RefreshCw size={15} />
|
||||
<RefreshIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
@ -571,7 +559,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
icon={<InfoCircleOutlined style={{ color: 'red' }} />}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
|
||||
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}>
|
||||
<ActionButton
|
||||
@ -583,7 +571,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
mouseEnterDelay={1}
|
||||
open={showDeleteTooltip}
|
||||
onOpenChange={setShowDeleteTooltip}>
|
||||
<Trash size={15} />
|
||||
<DeleteIcon size={15} />
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</Popconfirm>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CheckOutlined, CloseOutlined, ExpandOutlined, LoadingOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { CopyIcon, LoadingIcon } from '@renderer/components/Icons'
|
||||
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@ -19,7 +19,18 @@ import {
|
||||
Tooltip
|
||||
} from 'antd'
|
||||
import { message } from 'antd'
|
||||
import { ChevronDown, ChevronRight, CirclePlay, CircleX, PauseCircle, ShieldCheck } from 'lucide-react'
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CirclePlay,
|
||||
CircleX,
|
||||
Maximize,
|
||||
PauseCircle,
|
||||
ShieldCheck,
|
||||
TriangleAlert,
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import { FC, memo, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -191,23 +202,23 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
label = t('message.tools.pending', 'Awaiting Approval')
|
||||
icon = <LoadingOutlined spin style={{ marginLeft: 6, color: 'var(--status-color-warning)' }} />
|
||||
icon = <LoadingIcon style={{ marginLeft: 6, color: 'var(--status-color-warning)' }} />
|
||||
break
|
||||
case 'invoking':
|
||||
label = t('message.tools.invoking')
|
||||
icon = <LoadingOutlined spin style={{ marginLeft: 6 }} />
|
||||
icon = <LoadingIcon style={{ marginLeft: 6 }} />
|
||||
break
|
||||
case 'cancelled':
|
||||
label = t('message.tools.cancelled')
|
||||
icon = <CloseOutlined style={{ marginLeft: 6 }} />
|
||||
icon = <X size={13} style={{ marginLeft: 6 }} className="lucide-custom" />
|
||||
break
|
||||
case 'done':
|
||||
if (hasError) {
|
||||
label = t('message.tools.error')
|
||||
icon = <WarningOutlined style={{ marginLeft: 6 }} />
|
||||
icon = <TriangleAlert size={13} style={{ marginLeft: 6 }} className="lucide-custom" />
|
||||
} else {
|
||||
label = t('message.tools.completed')
|
||||
icon = <CheckOutlined style={{ marginLeft: 6 }} />
|
||||
icon = <Check size={13} style={{ marginLeft: 6 }} className="lucide-custom" />
|
||||
}
|
||||
break
|
||||
default:
|
||||
@ -262,7 +273,7 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
})
|
||||
}}
|
||||
aria-label={t('common.expand')}>
|
||||
<ExpandOutlined />
|
||||
<Maximize size={14} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{!isPending && !isInvoking && (
|
||||
@ -274,8 +285,8 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
copyContent(JSON.stringify(result, null, 2), id)
|
||||
}}
|
||||
aria-label={t('common.copy')}>
|
||||
{!copiedMap[id] && <i className="iconfont icon-copy"></i>}
|
||||
{copiedMap[id] && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
{!copiedMap[id] && <CopyIcon size={14} />}
|
||||
{copiedMap[id] && <Check size={14} color="var(--status-color-success)" />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -394,7 +405,7 @@ const MessageTools: FC<Props> = ({ block }) => {
|
||||
e.stopPropagation()
|
||||
handleAbortTool()
|
||||
}}>
|
||||
<PauseCircle className="lucide-custom" size={14} />
|
||||
<PauseCircle size={14} className="lucide-custom" />
|
||||
{t('chat.input.pause')}
|
||||
</Button>
|
||||
) : (
|
||||
@ -572,10 +583,10 @@ const ExpandIcon = styled(ChevronRight)<{ $isActive?: boolean }>`
|
||||
`
|
||||
|
||||
const CollapseContainer = styled(Collapse)`
|
||||
--status-color-warning: var(--color-warning, #faad14);
|
||||
--status-color-warning: var(--color-status-warning, #faad14);
|
||||
--status-color-invoking: var(--color-primary);
|
||||
--status-color-error: var(--color-error, #ff4d4f);
|
||||
--status-color-success: var(--color-success, green);
|
||||
--status-color-error: var(--color-status-error, #ff4d4f);
|
||||
--status-color-success: var(--color-primary, green);
|
||||
border-radius: 7px;
|
||||
border: none;
|
||||
background-color: var(--color-background);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { TranslationOutlined } from '@ant-design/icons'
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import type { TranslationMessageBlock } from '@renderer/types/newMessage'
|
||||
import { Divider } from 'antd'
|
||||
import { FC, Fragment } from 'react'
|
||||
@ -20,7 +20,7 @@ const MessageTranslate: FC<Props> = ({ block }) => {
|
||||
<TranslationOutlined />
|
||||
</Divider>
|
||||
{!block.content || block.content === t('translate.processing') ? (
|
||||
<SvgSpinners180Ring color="var(--color-text-2)" style={{ marginBottom: 15 }} />
|
||||
<LoadingIcon color="var(--color-text-2)" style={{ marginBottom: 15 }} />
|
||||
) : (
|
||||
<Markdown block={block} />
|
||||
)}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { loggerService } from '@logger'
|
||||
import ContextMenu from '@renderer/components/ContextMenu'
|
||||
import SvgSpinners180Ring from '@renderer/components/Icons/SvgSpinners180Ring'
|
||||
import { LoadingIcon } from '@renderer/components/Icons'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { LOAD_MORE_COUNT } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
@ -309,7 +309,7 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
))}
|
||||
{isLoadingMore && (
|
||||
<LoaderContainer>
|
||||
<SvgSpinners180Ring color="var(--color-text-2)" />
|
||||
<LoadingIcon color="var(--color-text-2)" />
|
||||
</LoaderContainer>
|
||||
)}
|
||||
</ScrollContainer>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DownOutlined, PlusOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import { DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
@ -6,8 +6,9 @@ import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import { Assistant, AssistantsSortType } from '@renderer/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { FC, useCallback, useRef, useState } from 'react'
|
||||
import { Tooltip, Typography } from 'antd'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@ -69,6 +70,19 @@ const Assistants: FC<AssistantsTabProps> = ({
|
||||
[assistants, t, updateAssistants]
|
||||
)
|
||||
|
||||
const renderAddAssistantButton = useMemo(() => {
|
||||
return (
|
||||
<AssistantAddItem onClick={onCreateAssistant}>
|
||||
<AddItemWrapper>
|
||||
<Plus size={16} style={{ marginRight: 4, flexShrink: 0 }} />
|
||||
<Typography.Text style={{ color: 'inherit' }} ellipsis={{ tooltip: t('chat.add.assistant.title') }}>
|
||||
{t('chat.add.assistant.title')}
|
||||
</Typography.Text>
|
||||
</AddItemWrapper>
|
||||
</AssistantAddItem>
|
||||
)
|
||||
}, [onCreateAssistant, t])
|
||||
|
||||
if (assistantsTabSortType === 'tags') {
|
||||
return (
|
||||
<Container className="assistants-tab" ref={containerRef}>
|
||||
@ -117,12 +131,7 @@ const Assistants: FC<AssistantsTabProps> = ({
|
||||
</TagsContainer>
|
||||
))}
|
||||
</div>
|
||||
<AssistantAddItem onClick={onCreateAssistant}>
|
||||
<AssistantName>
|
||||
<PlusOutlined style={{ color: 'var(--color-text-2)', marginRight: 4 }} />
|
||||
{t('chat.add.assistant.title')}
|
||||
</AssistantName>
|
||||
</AssistantAddItem>
|
||||
{renderAddAssistantButton}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@ -149,14 +158,7 @@ const Assistants: FC<AssistantsTabProps> = ({
|
||||
/>
|
||||
)}
|
||||
</DraggableList>
|
||||
{!dragging && (
|
||||
<AssistantAddItem onClick={onCreateAssistant}>
|
||||
<AssistantName>
|
||||
<PlusOutlined style={{ color: 'var(--color-text-2)', marginRight: 4 }} />
|
||||
{t('chat.add.assistant.title')}
|
||||
</AssistantName>
|
||||
</AssistantAddItem>
|
||||
)}
|
||||
{!dragging && renderAddAssistantButton}
|
||||
<div style={{ minHeight: 10 }}></div>
|
||||
</Container>
|
||||
)
|
||||
@ -224,13 +226,13 @@ const GroupTitleDivider = styled.div`
|
||||
border-top: 1px solid var(--color-border);
|
||||
`
|
||||
|
||||
const AssistantName = styled.div`
|
||||
color: var(--color-text);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
const AddItemWrapper = styled.div`
|
||||
color: var(--color-text-2);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default Assistants
|
||||
|
||||
@ -1,21 +1,10 @@
|
||||
import {
|
||||
ClearOutlined,
|
||||
CloseOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
FolderOutlined,
|
||||
MenuOutlined,
|
||||
PlusOutlined,
|
||||
PushpinOutlined,
|
||||
QuestionCircleOutlined,
|
||||
UploadOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { DraggableVirtualList } from '@renderer/components/DraggableList'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { finishTopicRenaming, startTopicRenaming, TopicManager } from '@renderer/hooks/useTopic'
|
||||
@ -40,6 +29,19 @@ import { Dropdown, MenuProps, Tooltip } from 'antd'
|
||||
import { ItemType, MenuItemType } from 'antd/es/menu/interface'
|
||||
import dayjs from 'dayjs'
|
||||
import { findIndex } from 'lodash'
|
||||
import {
|
||||
BrushCleaning,
|
||||
FolderOpen,
|
||||
HelpCircle,
|
||||
MenuIcon,
|
||||
PackagePlus,
|
||||
PinIcon,
|
||||
PinOffIcon,
|
||||
PlusIcon,
|
||||
Sparkles,
|
||||
UploadIcon,
|
||||
XIcon
|
||||
} from 'lucide-react'
|
||||
import { FC, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
@ -69,6 +71,22 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
|
||||
const [deletingTopicId, setDeletingTopicId] = useState<string | null>(null)
|
||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
||||
|
||||
const topicEdit = useInPlaceEdit({
|
||||
onSave: (name: string) => {
|
||||
const topic = assistant.topics.find((t) => t.id === editingTopicId)
|
||||
if (topic && name !== topic.name) {
|
||||
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
|
||||
updateTopic(updatedTopic)
|
||||
window.message.success(t('common.saved'))
|
||||
}
|
||||
setEditingTopicId(null)
|
||||
},
|
||||
onCancel: () => {
|
||||
setEditingTopicId(null)
|
||||
}
|
||||
})
|
||||
|
||||
const isPending = useCallback((topicId: string) => topicLoadingQuery[topicId], [topicLoadingQuery])
|
||||
const isFulfilled = useCallback((topicId: string) => topicFulfilledQuery[topicId], [topicFulfilledQuery])
|
||||
@ -177,7 +195,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
{
|
||||
label: t('chat.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <i className="iconfont icon-business-smart-assistant" style={{ fontSize: '14px' }} />,
|
||||
icon: <Sparkles size={14} />,
|
||||
disabled: isRenaming(topic.id),
|
||||
async onClick() {
|
||||
const messages = await TopicManager.getTopicMessages(topic.id)
|
||||
@ -200,27 +218,20 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
{
|
||||
label: t('chat.topics.edit.title'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
icon: <EditIcon size={14} />,
|
||||
disabled: isRenaming(topic.id),
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('chat.topics.edit.title'),
|
||||
message: '',
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
const updatedTopic = { ...topic, name, isNameManuallyEdited: true }
|
||||
updateTopic(updatedTopic)
|
||||
}
|
||||
onClick() {
|
||||
setEditingTopicId(topic.id)
|
||||
topicEdit.startEdit(topic.name)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.prompt.label'),
|
||||
key: 'topic-prompt',
|
||||
icon: <i className="iconfont icon-ai-model1" style={{ fontSize: '14px' }} />,
|
||||
icon: <PackagePlus size={14} />,
|
||||
extra: (
|
||||
<Tooltip title={t('chat.topics.prompt.tips')}>
|
||||
<QuestionIcon />
|
||||
<HelpCircle size={14} />
|
||||
</Tooltip>
|
||||
),
|
||||
async onClick() {
|
||||
@ -243,9 +254,9 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
}
|
||||
},
|
||||
{
|
||||
label: topic.pinned ? t('chat.topics.unpinned') : t('chat.topics.pinned'),
|
||||
label: topic.pinned ? t('chat.topics.unpin') : t('chat.topics.pin'),
|
||||
key: 'pin',
|
||||
icon: <PushpinOutlined />,
|
||||
icon: topic.pinned ? <PinOffIcon size={14} /> : <PinIcon size={14} />,
|
||||
onClick() {
|
||||
onPinTopic(topic)
|
||||
}
|
||||
@ -253,7 +264,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
{
|
||||
label: t('chat.topics.clear.title'),
|
||||
key: 'clear-messages',
|
||||
icon: <ClearOutlined />,
|
||||
icon: <BrushCleaning size={14} />,
|
||||
async onClick() {
|
||||
window.modal.confirm({
|
||||
title: t('chat.input.clear.content'),
|
||||
@ -265,7 +276,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
{
|
||||
label: t('settings.topic.position.label'),
|
||||
key: 'topic-position',
|
||||
icon: <MenuOutlined />,
|
||||
icon: <MenuIcon size={14} />,
|
||||
children: [
|
||||
{
|
||||
label: t('settings.topic.position.left'),
|
||||
@ -282,7 +293,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
{
|
||||
label: t('chat.topics.copy.title'),
|
||||
key: 'copy',
|
||||
icon: <CopyIcon />,
|
||||
icon: <CopyIcon size={14} />,
|
||||
children: [
|
||||
{
|
||||
label: t('chat.topics.copy.image'),
|
||||
@ -304,7 +315,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
{
|
||||
label: t('chat.topics.export.title'),
|
||||
key: 'export',
|
||||
icon: <UploadOutlined />,
|
||||
icon: <UploadIcon size={14} />,
|
||||
children: [
|
||||
exportMenuOptions.image && {
|
||||
label: t('chat.topics.export.image'),
|
||||
@ -375,7 +386,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
menus.push({
|
||||
label: t('chat.topics.move_to'),
|
||||
key: 'move',
|
||||
icon: <FolderOutlined />,
|
||||
icon: <FolderOpen size={14} />,
|
||||
children: assistants
|
||||
.filter((a) => a.id !== assistant.id)
|
||||
.map((a) => ({
|
||||
@ -392,7 +403,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
onClick: () => onDeleteTopic(topic)
|
||||
})
|
||||
}
|
||||
@ -414,6 +425,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
assistants,
|
||||
assistant,
|
||||
updateTopic,
|
||||
topicEdit,
|
||||
activeTopic.id,
|
||||
setActiveTopic,
|
||||
onPinTopic,
|
||||
@ -446,7 +458,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
itemContainerStyle={{ paddingBottom: '8px' }}
|
||||
header={
|
||||
<AddTopicButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||
<PlusOutlined />
|
||||
<PlusIcon size={16} />
|
||||
{t('chat.add.topic.title')}
|
||||
</AddTopicButton>
|
||||
}>
|
||||
@ -467,14 +479,27 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
<TopicListItem
|
||||
onContextMenu={() => setTargetTopic(topic)}
|
||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||
onClick={() => onSwitchTopic(topic)}
|
||||
style={{ borderRadius }}>
|
||||
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}
|
||||
style={{
|
||||
borderRadius,
|
||||
cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer'
|
||||
}}>
|
||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
||||
<TopicNameContainer>
|
||||
<TopicName className={getTopicNameClassName()} title={topicName}>
|
||||
{topicName}
|
||||
</TopicName>
|
||||
{editingTopicId === topic.id && topicEdit.isEditing ? (
|
||||
<TopicEditInput
|
||||
ref={topicEdit.inputRef}
|
||||
value={topicEdit.editValue}
|
||||
onChange={topicEdit.handleInputChange}
|
||||
onKeyDown={topicEdit.handleKeyDown}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
) : (
|
||||
<TopicName className={getTopicNameClassName()} title={topicName}>
|
||||
{topicName}
|
||||
</TopicName>
|
||||
)}
|
||||
{!topic.pinned && (
|
||||
<Tooltip
|
||||
placement="bottom"
|
||||
@ -498,16 +523,16 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic,
|
||||
}
|
||||
}}>
|
||||
{deletingTopicId === topic.id ? (
|
||||
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
|
||||
<DeleteIcon size={14} color="var(--color-error)" />
|
||||
) : (
|
||||
<CloseOutlined />
|
||||
<XIcon size={14} color="var(--color-text-3)" />
|
||||
)}
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{topic.pinned && (
|
||||
<MenuButton className="pin">
|
||||
<PushpinOutlined />
|
||||
<PinIcon size={14} color="var(--color-text-3)" />
|
||||
</MenuButton>
|
||||
)}
|
||||
</TopicNameContainer>
|
||||
@ -625,6 +650,23 @@ const TopicName = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const TopicEditInput = styled.input`
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text-1);
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
padding: 2px 6px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-alpha);
|
||||
}
|
||||
`
|
||||
|
||||
const PendingIndicator = styled.div.attrs({
|
||||
className: 'animation-pulse'
|
||||
})`
|
||||
@ -704,10 +746,5 @@ const MenuButton = styled.div`
|
||||
font-size: 12px;
|
||||
}
|
||||
`
|
||||
const QuestionIcon = styled(QuestionCircleOutlined)`
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
export default Topics
|
||||
|
||||
@ -1,17 +1,6 @@
|
||||
import {
|
||||
CheckOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined,
|
||||
SaveOutlined,
|
||||
SmileOutlined,
|
||||
SortAscendingOutlined,
|
||||
SortDescendingOutlined
|
||||
} from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import EmojiIcon from '@renderer/components/EmojiIcon'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@ -24,7 +13,19 @@ import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { hasTopicPendingRequests } from '@renderer/utils/queue'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { omit } from 'lodash'
|
||||
import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react'
|
||||
import {
|
||||
AlignJustify,
|
||||
ArrowDownAZ,
|
||||
ArrowUpAZ,
|
||||
BrushCleaning,
|
||||
Check,
|
||||
Plus,
|
||||
Save,
|
||||
Settings2,
|
||||
Smile,
|
||||
Tag,
|
||||
Tags
|
||||
} from 'lucide-react'
|
||||
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -198,7 +199,7 @@ const createTagMenuItems = (
|
||||
const items: MenuProps['items'] = [
|
||||
...allTags.map((tag) => ({
|
||||
label: tag,
|
||||
icon: assistant.tags?.includes(tag) ? <CheckOutlined size={14} /> : <Tag size={12} />,
|
||||
icon: assistant.tags?.includes(tag) ? <Check size={14} /> : <Tag size={14} />,
|
||||
key: `all-tag-${tag}`,
|
||||
onClick: () => handleTagOperation(tag, assistant, assistants, updateAssistants)
|
||||
}))
|
||||
@ -211,7 +212,7 @@ const createTagMenuItems = (
|
||||
items.push({
|
||||
label: t('assistants.tags.add'),
|
||||
key: 'new-tag',
|
||||
icon: <Plus size={16} />,
|
||||
icon: <Plus size={14} />,
|
||||
onClick: async () => {
|
||||
const tagName = await PromptPopup.show({
|
||||
title: t('assistants.tags.add'),
|
||||
@ -228,7 +229,7 @@ const createTagMenuItems = (
|
||||
items.push({
|
||||
label: t('assistants.tags.manage'),
|
||||
key: 'manage-tags',
|
||||
icon: <Settings2 size={16} />,
|
||||
icon: <Settings2 size={14} />,
|
||||
onClick: () => {
|
||||
AssistantTagsPopup.show({ title: t('assistants.tags.manage') })
|
||||
}
|
||||
@ -260,13 +261,13 @@ function getMenuItems({
|
||||
{
|
||||
label: t('assistants.edit.title'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
icon: <EditIcon size={14} />,
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant })
|
||||
},
|
||||
{
|
||||
label: t('assistants.copy.title'),
|
||||
key: 'duplicate',
|
||||
icon: <CopyIcon />,
|
||||
icon: <CopyIcon size={14} />,
|
||||
onClick: async () => {
|
||||
const _assistant = copyAssistant(assistant)
|
||||
if (_assistant) {
|
||||
@ -277,7 +278,7 @@ function getMenuItems({
|
||||
{
|
||||
label: t('assistants.clear.title'),
|
||||
key: 'clear',
|
||||
icon: <MinusCircleOutlined />,
|
||||
icon: <BrushCleaning size={14} />,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('assistants.clear.title'),
|
||||
@ -291,7 +292,7 @@ function getMenuItems({
|
||||
{
|
||||
label: t('assistants.save.title'),
|
||||
key: 'save-to-agent',
|
||||
icon: <SaveOutlined />,
|
||||
icon: <Save size={14} />,
|
||||
onClick: async () => {
|
||||
const agent = omit(assistant, ['model', 'emoji'])
|
||||
agent.id = uuid()
|
||||
@ -306,7 +307,7 @@ function getMenuItems({
|
||||
{
|
||||
label: t('assistants.icon.type'),
|
||||
key: 'icon-type',
|
||||
icon: <SmileOutlined />,
|
||||
icon: <Smile size={14} />,
|
||||
children: [
|
||||
{
|
||||
label: t('settings.assistant.icon.type.model'),
|
||||
@ -331,7 +332,7 @@ function getMenuItems({
|
||||
{
|
||||
label: t('assistants.tags.manage'),
|
||||
key: 'all-tags',
|
||||
icon: <PlusOutlined />,
|
||||
icon: <Plus size={14} />,
|
||||
children: createTagMenuItems(allTags, assistant, assistants, updateAssistants, t)
|
||||
},
|
||||
{
|
||||
@ -345,13 +346,13 @@ function getMenuItems({
|
||||
{
|
||||
label: t('common.sort.pinyin.asc'),
|
||||
key: 'sort-asc',
|
||||
icon: <SortAscendingOutlined />,
|
||||
icon: <ArrowDownAZ size={14} />,
|
||||
onClick: sortByPinyinAsc
|
||||
},
|
||||
{
|
||||
label: t('common.sort.pinyin.desc'),
|
||||
key: 'sort-desc',
|
||||
icon: <SortDescendingOutlined />,
|
||||
icon: <ArrowUpAZ size={14} />,
|
||||
onClick: sortByPinyinDesc
|
||||
},
|
||||
{
|
||||
@ -360,7 +361,7 @@ function getMenuItems({
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import { Box } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useTags } from '@renderer/hooks/useTags'
|
||||
import { Button, Empty, Modal } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { Trash } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -94,7 +94,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
<Box mr={8}>{tag}</Box>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<Trash size={16} />}
|
||||
icon={<DeleteIcon size={16} className="lucide-custom" />}
|
||||
danger
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
|
||||
@ -289,9 +289,12 @@ export const ItemHeader = styled.div`
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
top: calc(var(--navbar-height) + 14px);
|
||||
right: 16px;
|
||||
z-index: 1000;
|
||||
top: calc(var(--navbar-height) + 12px);
|
||||
[navbar-position='top'] & {
|
||||
top: calc(var(--navbar-height) + 10px);
|
||||
}
|
||||
`
|
||||
|
||||
export const StatusIconWrapper = styled.div`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import ListItem from '@renderer/components/ListItem'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
@ -9,7 +9,7 @@ import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import KnowledgeSearchPopup from '@renderer/pages/knowledge/components/KnowledgeSearchPopup'
|
||||
import { KnowledgeBase } from '@renderer/types'
|
||||
import { Dropdown, Empty, MenuProps } from 'antd'
|
||||
import { Book, Plus } from 'lucide-react'
|
||||
import { Book, Plus, Settings } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -49,7 +49,7 @@ const KnowledgePage: FC = () => {
|
||||
{
|
||||
label: t('knowledge.rename'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
icon: <EditIcon size={14} />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('knowledge.rename'),
|
||||
@ -62,9 +62,9 @@ const KnowledgePage: FC = () => {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('knowledge.settings.title'),
|
||||
label: t('common.settings'),
|
||||
key: 'settings',
|
||||
icon: <SettingOutlined />,
|
||||
icon: <Settings size={14} />,
|
||||
onClick: () => handleEditKnowledgeBase(base)
|
||||
},
|
||||
{ type: 'divider' },
|
||||
@ -72,7 +72,7 @@ const KnowledgePage: FC = () => {
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('knowledge.delete_confirm'),
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { KnowledgeBase, Model } from '@renderer/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { userEvent } from '@testing-library/user-event'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import AdvancedSettingsPanel from '../components/KnowledgeSettings/AdvancedSettingsPanel'
|
||||
@ -36,6 +35,27 @@ vi.mock('react-i18next', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('lucide-react', () => ({
|
||||
TriangleAlert: () => <span>warning</span>
|
||||
}))
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
Alert: ({ message }: { message: string }) => <div role="alert">{message}</div>,
|
||||
InputNumber: ({ ref, value, onChange, placeholder, disabled, style, 'aria-label': ariaLabel }: any) => (
|
||||
<input
|
||||
ref={ref}
|
||||
type="number"
|
||||
data-testid="input-number"
|
||||
aria-label={ariaLabel}
|
||||
placeholder={placeholder}
|
||||
value={value ?? ''}
|
||||
onChange={(e) => onChange(e.target.valueAsNumber)}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
/**
|
||||
* 创建测试用的 KnowledgeBase 对象
|
||||
* @param overrides 可选的属性覆盖
|
||||
@ -78,23 +98,19 @@ describe('AdvancedSettingsPanel', () => {
|
||||
})
|
||||
|
||||
describe('handlers', () => {
|
||||
it('should call handlers when values are changed', async () => {
|
||||
const user = userEvent.setup()
|
||||
it('should call handlers when values are changed', () => {
|
||||
render(<AdvancedSettingsPanel newBase={mockBase} handlers={mocks.handlers} />)
|
||||
|
||||
const chunkSizeInput = screen.getByLabelText('分块大小')
|
||||
await user.clear(chunkSizeInput)
|
||||
await user.type(chunkSizeInput, '600')
|
||||
fireEvent.change(chunkSizeInput, { target: { value: '600' } })
|
||||
expect(mocks.handlers.handleChunkSizeChange).toHaveBeenCalledWith(600)
|
||||
|
||||
const chunkOverlapInput = screen.getByLabelText('分块重叠')
|
||||
await user.clear(chunkOverlapInput)
|
||||
await user.type(chunkOverlapInput, '300')
|
||||
fireEvent.change(chunkOverlapInput, { target: { value: '300' } })
|
||||
expect(mocks.handlers.handleChunkOverlapChange).toHaveBeenCalledWith(300)
|
||||
|
||||
const thresholdInput = screen.getByLabelText('检索相似度阈值')
|
||||
await user.clear(thresholdInput)
|
||||
await user.type(thresholdInput, '0.6')
|
||||
fireEvent.change(thresholdInput, { target: { value: '0.6' } })
|
||||
expect(mocks.handlers.handleThresholdChange).toHaveBeenCalledWith(0.6)
|
||||
})
|
||||
})
|
||||
|
||||
@ -31,84 +31,14 @@ exports[`AdvancedSettingsPanel > basic rendering > should match snapshot 1`] = `
|
||||
knowledge.chunk_size_tooltip
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number css-dev-only-do-not-override-1261szd ant-input-number-outlined"
|
||||
<input
|
||||
aria-label="分块大小"
|
||||
data-testid="input-number"
|
||||
placeholder="knowledge.chunk_size_placeholder"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-label="分块大小"
|
||||
aria-valuemin="100"
|
||||
aria-valuenow="500"
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
placeholder="knowledge.chunk_size_placeholder"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value="500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
type="number"
|
||||
value="500"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c1"
|
||||
@ -121,84 +51,14 @@ exports[`AdvancedSettingsPanel > basic rendering > should match snapshot 1`] = `
|
||||
knowledge.chunk_overlap_tooltip
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number css-dev-only-do-not-override-1261szd ant-input-number-outlined"
|
||||
<input
|
||||
aria-label="分块重叠"
|
||||
data-testid="input-number"
|
||||
placeholder="knowledge.chunk_overlap_placeholder"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-label="分块重叠"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="200"
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
placeholder="knowledge.chunk_overlap_placeholder"
|
||||
role="spinbutton"
|
||||
step="1"
|
||||
value="200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
type="number"
|
||||
value="200"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="c1"
|
||||
@ -211,119 +71,19 @@ exports[`AdvancedSettingsPanel > basic rendering > should match snapshot 1`] = `
|
||||
knowledge.threshold_tooltip
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number css-dev-only-do-not-override-1261szd ant-input-number-outlined"
|
||||
<input
|
||||
aria-label="检索相似度阈值"
|
||||
data-testid="input-number"
|
||||
placeholder="knowledge.threshold_placeholder"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<div
|
||||
class="ant-input-number-handler-wrap"
|
||||
>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Increase Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-up"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="up"
|
||||
class="anticon anticon-up ant-input-number-handler-up-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="up"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
aria-disabled="false"
|
||||
aria-label="Decrease Value"
|
||||
class="ant-input-number-handler ant-input-number-handler-down"
|
||||
role="button"
|
||||
unselectable="on"
|
||||
>
|
||||
<span
|
||||
aria-label="down"
|
||||
class="anticon anticon-down ant-input-number-handler-down-inner"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="down"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="ant-input-number-input-wrap"
|
||||
>
|
||||
<input
|
||||
aria-label="检索相似度阈值"
|
||||
aria-valuemax="1"
|
||||
aria-valuemin="0"
|
||||
aria-valuenow="0.5"
|
||||
autocomplete="off"
|
||||
class="ant-input-number-input"
|
||||
placeholder="knowledge.threshold_placeholder"
|
||||
role="spinbutton"
|
||||
step="0.1"
|
||||
value="0.5"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
type="number"
|
||||
value="0.5"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="ant-alert ant-alert-warning css-dev-only-do-not-override-1261szd"
|
||||
data-show="true"
|
||||
role="alert"
|
||||
>
|
||||
<span
|
||||
aria-label="warning"
|
||||
class="anticon anticon-warning ant-alert-icon"
|
||||
role="img"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
data-icon="warning"
|
||||
fill="currentColor"
|
||||
focusable="false"
|
||||
height="1em"
|
||||
viewBox="64 64 896 896"
|
||||
width="1em"
|
||||
>
|
||||
<path
|
||||
d="M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div
|
||||
class="ant-alert-content"
|
||||
>
|
||||
<div
|
||||
class="ant-alert-message"
|
||||
>
|
||||
避免修改这个高级设置。
|
||||
</div>
|
||||
</div>
|
||||
避免修改这个高级设置。
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { WarningOutlined } from '@ant-design/icons'
|
||||
import InfoTooltip from '@renderer/components/InfoTooltip'
|
||||
import { KnowledgeBase } from '@renderer/types'
|
||||
import { Alert, InputNumber } from 'antd'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingsItem, SettingsPanel } from './styles'
|
||||
@ -68,7 +68,12 @@ const AdvancedSettingsPanel: React.FC<AdvancedSettingsPanelProps> = ({ newBase,
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<Alert message={t('knowledge.chunk_size_change_warning')} type="warning" showIcon icon={<WarningOutlined />} />
|
||||
<Alert
|
||||
message={t('knowledge.chunk_size_change_warning')}
|
||||
type="warning"
|
||||
showIcon
|
||||
icon={<TriangleAlert size={16} className="lucide-custom" />}
|
||||
/>
|
||||
</SettingsPanel>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
@ -8,7 +8,7 @@ import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -68,7 +68,7 @@ const KnowledgeDirectories: FC<KnowledgeContentProps> = ({ selectedBase, progres
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
icon={<PlusIcon size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddDirectory()
|
||||
@ -111,7 +111,12 @@ const KnowledgeDirectories: FC<KnowledgeContentProps> = ({ selectedBase, progres
|
||||
type="directory"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => removeItem(item)}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
/>
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
@ -11,14 +10,15 @@ import { formatFileSize, uuid } from '@renderer/utils'
|
||||
import { bookExts, documentExts, textExts, thirdPartyApplicationExts } from '@shared/config/constant'
|
||||
import { Button, Tooltip, Upload } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const logger = loggerService.withContext('KnowledgeFiles')
|
||||
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
|
||||
import {
|
||||
ClickableSpan,
|
||||
@ -139,7 +139,7 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
icon={<PlusIcon size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddFile()
|
||||
@ -210,7 +210,12 @@ const KnowledgeFiles: FC<KnowledgeContentProps> = ({ selectedBase, progressMap,
|
||||
type="file"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => removeItem(item)}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
/>
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
@ -7,7 +7,7 @@ import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -64,7 +64,7 @@ const KnowledgeNotes: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
icon={<PlusIcon size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddNote()
|
||||
@ -91,7 +91,7 @@ const KnowledgeNotes: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
extra: getDisplayTime(note),
|
||||
actions: (
|
||||
<FlexAlignCenter>
|
||||
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditOutlined />} />
|
||||
<Button type="text" onClick={() => handleEditNote(note)} icon={<EditIcon size={14} />} />
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon
|
||||
sourceId={note.id}
|
||||
@ -100,7 +100,12 @@ const KnowledgeNotes: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
type="note"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(note)} icon={<DeleteOutlined />} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => removeItem(note)}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
/>
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
@ -9,7 +9,7 @@ import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button, message, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -87,7 +87,7 @@ const KnowledgeSitemaps: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
icon={<PlusIcon size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddSitemap()
|
||||
@ -133,7 +133,12 @@ const KnowledgeSitemaps: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
type="sitemap"
|
||||
/>
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => removeItem(item)}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
/>
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import Ellipsis from '@renderer/components/Ellipsis'
|
||||
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { DynamicVirtualList } from '@renderer/components/VirtualList'
|
||||
import { useKnowledge } from '@renderer/hooks/useKnowledge'
|
||||
@ -8,7 +8,7 @@ import { getProviderName } from '@renderer/services/ProviderService'
|
||||
import { KnowledgeBase, KnowledgeItem } from '@renderer/types'
|
||||
import { Button, Dropdown, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -115,7 +115,7 @@ const KnowledgeUrls: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
<ItemHeader>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
icon={<PlusIcon size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleAddUrl()
|
||||
@ -143,13 +143,13 @@ const KnowledgeUrls: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
items: [
|
||||
{
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
icon: <EditIcon size={14} />,
|
||||
label: t('knowledge.edit_remark'),
|
||||
onClick: () => handleEditRemark(item)
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <CopyOutlined />,
|
||||
icon: <CopyIcon size={14} />,
|
||||
label: t('common.copy'),
|
||||
onClick: () => {
|
||||
navigator.clipboard.writeText(item.content as string)
|
||||
@ -178,7 +178,12 @@ const KnowledgeUrls: FC<KnowledgeContentProps> = ({ selectedBase }) => {
|
||||
<StatusIconWrapper>
|
||||
<StatusIcon sourceId={item.id} base={base} getProcessingStatus={getProcessingStatus} type="url" />
|
||||
</StatusIconWrapper>
|
||||
<Button type="text" danger onClick={() => removeItem(item)} icon={<DeleteOutlined />} />
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
onClick={() => removeItem(item)}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
/>
|
||||
</FlexAlignCenter>
|
||||
)
|
||||
}}
|
||||
|
||||
@ -92,8 +92,8 @@ const LaunchpadPage: FC = () => {
|
||||
<SectionTitle>{t('launchpad.minapps')}</SectionTitle>
|
||||
<Grid>
|
||||
{sortedMinapps.map((app) => (
|
||||
<AppWrapper key={app.id} onClick={() => setTimeout(() => tabsService.closeTab('launchpad'), 350)}>
|
||||
<App app={app} size={56} />
|
||||
<AppWrapper key={app.id}>
|
||||
<App app={app} size={56} onClick={() => setTimeout(() => tabsService.closeTab('launchpad'), 350)} />
|
||||
</AppWrapper>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -76,8 +76,8 @@ export const DEFAULT_PAINTING: DmxapiPainting = {
|
||||
|
||||
export const MODEOPTIONS = [
|
||||
{ label: 'paintings.mode.generate', value: generationModeType.GENERATION },
|
||||
{ label: '改图', value: generationModeType.EDIT },
|
||||
{ label: '合并图', value: generationModeType.MERGE }
|
||||
{ label: 'paintings.mode.edit', value: generationModeType.EDIT },
|
||||
{ label: 'paintings.mode.merge', value: generationModeType.MERGE }
|
||||
]
|
||||
|
||||
// 获取模型分组数据
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { DeleteOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import EditableNumber from '@renderer/components/EditableNumber'
|
||||
import { DeleteIcon, ResetIcon } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
@ -10,6 +11,7 @@ import { Assistant, AssistantSettingCustomParameters, AssistantSettings } from '
|
||||
import { modalConfirm } from '@renderer/utils'
|
||||
import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { isNull } from 'lodash'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -213,7 +215,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Label>{t('assistants.settings.default_model')}</Label>
|
||||
<HStack alignItems="center" gap={5}>
|
||||
<ModelSelectButton
|
||||
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusOutlined />}
|
||||
icon={defaultModel ? <ModelAvatar model={defaultModel} size={20} /> : <PlusIcon size={18} />}
|
||||
onClick={onSelectModel}>
|
||||
<ModelName>{defaultModel ? defaultModel.name : t('agents.edit.model.select.title')}</ModelName>
|
||||
</ModelSelectButton>
|
||||
@ -221,7 +223,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Button
|
||||
color="danger"
|
||||
variant="filled"
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
onClick={() => {
|
||||
setDefaultModel(undefined)
|
||||
updateAssistant({ ...assistant, defaultModel: undefined })
|
||||
@ -449,7 +451,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<SettingRow style={{ minHeight: 30 }}>
|
||||
<Label>{t('models.custom_parameters')}</Label>
|
||||
<Button icon={<PlusOutlined />} onClick={onAddCustomParameter}>
|
||||
<Button icon={<PlusIcon size={18} />} onClick={onAddCustomParameter}>
|
||||
{t('models.add_parameter')}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
@ -478,7 +480,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
<Button
|
||||
color="danger"
|
||||
variant="filled"
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
onClick={() => onDeleteCustomParameter(index)}
|
||||
/>
|
||||
</Col>
|
||||
@ -486,7 +488,7 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
|
||||
))}
|
||||
<Divider style={{ margin: '15px 0' }} />
|
||||
<HStack justifyContent="flex-end">
|
||||
<Button onClick={onReset} style={{ width: 80 }} danger type="primary">
|
||||
<Button onClick={onReset} danger type="primary" icon={<ResetIcon size={16} />}>
|
||||
{t('chat.settings.reset')}
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
@ -165,10 +165,6 @@ const Container = styled.div`
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.ant-btn {
|
||||
line-height: 0;
|
||||
}
|
||||
`
|
||||
|
||||
const EmojiButtonWrapper = styled.div`
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { DeleteOutlined, EditOutlined, ExclamationCircleOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
|
||||
import FileItem from '@renderer/pages/files/FileItem'
|
||||
import { Assistant, QuickPhrase } from '@renderer/types'
|
||||
import { Button, Flex, Input, Modal, Popconfirm, Space } from 'antd'
|
||||
import { PlusIcon } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -82,7 +84,7 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
|
||||
<Container>
|
||||
<SettingTitle>
|
||||
{t('assistants.settings.regular_phrases.title', 'Regular Prompts')}
|
||||
<Button type="text" icon={<PlusOutlined />} onClick={handleAdd} />
|
||||
<Button type="text" icon={<PlusIcon size={18} />} onClick={handleAdd} />
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
@ -102,7 +104,7 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
|
||||
extra: prompt.content,
|
||||
actions: (
|
||||
<Flex gap={4} style={{ opacity: 0.6 }}>
|
||||
<Button key="edit" type="text" icon={<EditOutlined />} onClick={() => handleEdit(prompt)} />
|
||||
<Button key="edit" type="text" icon={<EditIcon size={14} />} onClick={() => handleEdit(prompt)} />
|
||||
<Popconfirm
|
||||
title={t('assistants.settings.regular_phrases.delete', 'Delete Prompt')}
|
||||
description={t(
|
||||
@ -113,7 +115,12 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={() => handleDelete(prompt.id)}
|
||||
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
||||
<Button key="delete" type="text" danger icon={<DeleteOutlined />} />
|
||||
<Button
|
||||
key="delete"
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@ -92,9 +92,9 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ resolve, tab, ...prop
|
||||
<StyledModal
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onClose={onCancel}
|
||||
onCancel={onCancel}
|
||||
afterClose={afterClose}
|
||||
maskClosable={false}
|
||||
footer={null}
|
||||
title={assistant.name}
|
||||
transitionName="animation-move-down"
|
||||
|
||||
@ -2,9 +2,7 @@ import {
|
||||
CloudServerOutlined,
|
||||
CloudSyncOutlined,
|
||||
FileSearchOutlined,
|
||||
FolderOpenOutlined,
|
||||
LoadingOutlined,
|
||||
SaveOutlined,
|
||||
YuqueOutlined
|
||||
} from '@ant-design/icons'
|
||||
import DividerWithText from '@renderer/components/DividerWithText'
|
||||
@ -22,7 +20,7 @@ import { AppInfo } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { occupiedDirs } from '@shared/config/constant'
|
||||
import { Button, Progress, Switch, Typography } from 'antd'
|
||||
import { FileText, FolderCog, FolderInput, Sparkle } from 'lucide-react'
|
||||
import { FileText, FolderCog, FolderInput, FolderOpen, SaveIcon, Sparkle } from 'lucide-react'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -588,10 +586,10 @@ const DataSettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<HStack gap="5px" justifyContent="space-between">
|
||||
<Button onClick={BackupPopup.show} icon={<SaveOutlined />}>
|
||||
<Button onClick={BackupPopup.show} icon={<SaveIcon size={14} />}>
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={RestorePopup.show} icon={<FolderOpenOutlined />}>
|
||||
<Button onClick={RestorePopup.show} icon={<FolderOpen size={14} />}>
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import CodeEditor from '@renderer/components/CodeEditor'
|
||||
import { ResetIcon } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import TextBadge from '@renderer/components/TextBadge'
|
||||
import { isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant'
|
||||
@ -18,7 +19,7 @@ import {
|
||||
} from '@renderer/store/settings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { Button, ColorPicker, Segmented, Switch } from 'antd'
|
||||
import { Minus, Monitor, Moon, Plus, RotateCcw, Sun } from 'lucide-react'
|
||||
import { Minus, Monitor, Moon, Plus, Sun } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -247,7 +248,7 @@ const DisplaySettings: FC = () => {
|
||||
<Button
|
||||
onClick={() => handleZoomFactor(0, true)}
|
||||
style={{ marginLeft: 8 }}
|
||||
icon={<RotateCcw size="14" />}
|
||||
icon={<ResetIcon size="14" />}
|
||||
color="default"
|
||||
variant="text"
|
||||
/>
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
setEnableSpellCheck,
|
||||
setLanguage,
|
||||
setNotificationSettings,
|
||||
setProxyBypassRules as _setProxyBypassRules,
|
||||
setProxyMode,
|
||||
setProxyUrl as _setProxyUrl,
|
||||
setSpellCheckLanguages
|
||||
@ -17,7 +18,7 @@ import {
|
||||
import { LanguageVarious } from '@renderer/types'
|
||||
import { NotificationSource } from '@renderer/types/notification'
|
||||
import { isValidProxyUrl } from '@renderer/utils'
|
||||
import { defaultLanguage } from '@shared/config/constant'
|
||||
import { defaultByPassRules, defaultLanguage } from '@shared/config/constant'
|
||||
import { Flex, Input, Switch, Tooltip } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -29,6 +30,7 @@ const GeneralSettings: FC = () => {
|
||||
const {
|
||||
language,
|
||||
proxyUrl: storeProxyUrl,
|
||||
proxyBypassRules: storeProxyBypassRules,
|
||||
setLaunch,
|
||||
setTray,
|
||||
launchOnBoot,
|
||||
@ -42,6 +44,7 @@ const GeneralSettings: FC = () => {
|
||||
setDisableHardwareAcceleration
|
||||
} = useSettings()
|
||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||
const [proxyBypassRules, setProxyBypassRules] = useState<string | undefined>(storeProxyBypassRules)
|
||||
const { theme } = useTheme()
|
||||
const { enableDeveloperMode, setEnableDeveloperMode } = useEnableDeveloperMode()
|
||||
|
||||
@ -97,6 +100,10 @@ const GeneralSettings: FC = () => {
|
||||
dispatch(_setProxyUrl(proxyUrl))
|
||||
}
|
||||
|
||||
const onSetProxyBypassRules = () => {
|
||||
dispatch(_setProxyBypassRules(proxyBypassRules))
|
||||
}
|
||||
|
||||
const proxyModeOptions: { value: 'system' | 'custom' | 'none'; label: string }[] = [
|
||||
{ value: 'system', label: t('settings.proxy.mode.system') },
|
||||
{ value: 'custom', label: t('settings.proxy.mode.custom') },
|
||||
@ -109,6 +116,7 @@ const GeneralSettings: FC = () => {
|
||||
dispatch(_setProxyUrl(undefined))
|
||||
} else if (mode === 'none') {
|
||||
dispatch(_setProxyUrl(undefined))
|
||||
dispatch(_setProxyBypassRules(undefined))
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,6 +218,7 @@ const GeneralSettings: FC = () => {
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.proxy.address')}</SettingRowTitle>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
placeholder="socks5://127.0.0.1:6153"
|
||||
value={proxyUrl}
|
||||
onChange={(e) => setProxyUrl(e.target.value)}
|
||||
@ -220,6 +229,22 @@ const GeneralSettings: FC = () => {
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
{(storeProxyMode === 'custom' || storeProxyMode === 'system') && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.proxy.bypass')}</SettingRowTitle>
|
||||
<Input
|
||||
spellCheck={false}
|
||||
placeholder={defaultByPassRules}
|
||||
value={proxyBypassRules}
|
||||
onChange={(e) => setProxyBypassRules(e.target.value)}
|
||||
style={{ width: 180 }}
|
||||
onBlur={() => onSetProxyBypassRules()}
|
||||
/>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<HStack justifyContent="space-between" alignItems="center" style={{ flex: 1, marginRight: 16 }}>
|
||||
|
||||
@ -115,6 +115,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
maskClosable={false}
|
||||
width={800}
|
||||
height="80vh"
|
||||
loading={jsonSaving}
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { EditOutlined } from '@ant-design/icons'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { DraggableList } from '@renderer/components/DraggableList'
|
||||
import { EditIcon, RefreshIcon } from '@renderer/components/Icons'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import { getMcpTypeLabel } from '@renderer/i18n/label'
|
||||
import { MCPServer } from '@renderer/types'
|
||||
import { formatMcpError } from '@renderer/utils/error'
|
||||
import { Badge, Button, Dropdown, Empty, Switch, Tag } from 'antd'
|
||||
import { MonitorCheck, Plus, RefreshCw, Settings2, SquareArrowOutUpRight } from 'lucide-react'
|
||||
import { MonitorCheck, Plus, Settings2, SquareArrowOutUpRight } from 'lucide-react'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router'
|
||||
@ -139,7 +139,7 @@ const McpServersList: FC = () => {
|
||||
<ListHeader>
|
||||
<SettingTitle style={{ gap: 3 }}>
|
||||
<span>{t('settings.mcp.newServer')}</span>
|
||||
<Button icon={<EditOutlined />} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
|
||||
<Button icon={<EditIcon size={14} />} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
|
||||
</SettingTitle>
|
||||
<ButtonGroup>
|
||||
<InstallNpxUv mini />
|
||||
@ -176,7 +176,7 @@ const McpServersList: FC = () => {
|
||||
{t('settings.mcp.addServer.label')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<Button icon={<RefreshCw size={16} />} type="default" onClick={onSyncServers} shape="round">
|
||||
<Button icon={<RefreshIcon size={16} />} type="default" onClick={onSyncServers} shape="round">
|
||||
{t('settings.mcp.sync.title', 'Sync Servers')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { DeleteIcon } from '@renderer/components/Icons'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
|
||||
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
|
||||
@ -7,7 +7,7 @@ import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
|
||||
import { formatMcpError } from '@renderer/utils/error'
|
||||
import { Badge, Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { ChevronDown, SaveIcon } from 'lucide-react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate, useParams } from 'react-router'
|
||||
@ -633,8 +633,13 @@ const McpSettings: React.FC = () => {
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item name="longRunning" label={t('settings.mcp.longRunning', 'Long Running')} valuePropName="checked">
|
||||
<Switch />
|
||||
<Form.Item
|
||||
name="longRunning"
|
||||
label={t('settings.mcp.longRunning', 'Long Running')}
|
||||
tooltip={t('settings.mcp.longRunningTooltip')}
|
||||
layout="horizontal"
|
||||
valuePropName="checked">
|
||||
<Switch size="small" style={{ marginLeft: 10 }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="timeout"
|
||||
@ -732,7 +737,12 @@ const McpSettings: React.FC = () => {
|
||||
<ServerName className="text-nowrap">{server?.name}</ServerName>
|
||||
{serverVersion && <VersionBadge count={serverVersion} color="blue" />}
|
||||
</Flex>
|
||||
<Button danger icon={<DeleteOutlined />} type="text" onClick={() => onDeleteMcpServer(server)} />
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
type="text"
|
||||
onClick={() => onDeleteMcpServer(server)}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex align="center" gap={16}>
|
||||
<Switch
|
||||
@ -743,7 +753,7 @@ const McpSettings: React.FC = () => {
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
icon={<SaveIcon size={14} />}
|
||||
onClick={onSave}
|
||||
loading={loading}
|
||||
shape="round"
|
||||
|
||||
@ -1,16 +1,6 @@
|
||||
import {
|
||||
CalendarOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
MoreOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
UserAddOutlined,
|
||||
UserDeleteOutlined,
|
||||
UserOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { DeleteIcon, EditIcon, LoadingIcon, RefreshIcon } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import TextBadge from '@renderer/components/TextBadge'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
@ -25,24 +15,10 @@ import {
|
||||
setGlobalMemoryEnabled
|
||||
} from '@renderer/store/memory'
|
||||
import type { MemoryItem } from '@types'
|
||||
import {
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
Dropdown,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Pagination,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Switch
|
||||
} from 'antd'
|
||||
import { Badge, Button, Dropdown, Empty, Flex, Form, Input, Modal, Pagination, Space, Spin, Switch } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { Brain, Settings2 } from 'lucide-react'
|
||||
import { Brain, Calendar, MenuIcon, PlusIcon, Settings2, UserRound, UserRoundMinus, UserRoundPlus } from 'lucide-react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
@ -57,13 +33,13 @@ import {
|
||||
SettingRowTitle,
|
||||
SettingTitle
|
||||
} from '../index'
|
||||
import { DEFAULT_USER_ID } from './constants'
|
||||
import UserSelector from './UserSelector'
|
||||
|
||||
const logger = loggerService.withContext('MemorySettings')
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const DEFAULT_USER_ID = 'default-user'
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
|
||||
interface AddMemoryModalProps {
|
||||
@ -112,10 +88,10 @@ const AddMemoryModal: React.FC<AddMemoryModalProps> = ({ visible, onCancel, onAd
|
||||
onOk={() => form.submit()}
|
||||
okButtonProps={{ loading: loading }}
|
||||
title={
|
||||
<Space>
|
||||
<PlusOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<Flex align="center" gap={8}>
|
||||
<PlusIcon size={16} color="var(--color-primary)" />
|
||||
<span>{t('memory.add_memory')}</span>
|
||||
</Space>
|
||||
</Flex>
|
||||
}
|
||||
styles={{
|
||||
header: {
|
||||
@ -170,10 +146,10 @@ const EditMemoryModal: React.FC<EditMemoryModalProps> = ({ visible, memory, onCa
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<Space>
|
||||
<EditOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<Flex align="center" gap={8}>
|
||||
<EditIcon size={16} color="var(--color-primary)" />
|
||||
<span>{t('memory.edit_memory')}</span>
|
||||
</Space>
|
||||
</Flex>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
@ -268,10 +244,10 @@ const AddUserModal: React.FC<AddUserModalProps> = ({ visible, onCancel, onAdd, e
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<Space>
|
||||
<UserAddOutlined style={{ color: 'var(--color-primary)' }} />
|
||||
<Flex align="center" gap={8}>
|
||||
<UserRoundPlus size={16} color="var(--color-primary)" />
|
||||
<span>{t('memory.add_user')}</span>
|
||||
</Space>
|
||||
</Flex>
|
||||
}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form.Item label={t('memory.new_user_id')} name="userId" rules={[{ validator: validateUserId }]}>
|
||||
@ -279,7 +255,7 @@ const AddUserModal: React.FC<AddUserModalProps> = ({ visible, onCancel, onAdd, e
|
||||
placeholder={t('memory.new_user_id_placeholder')}
|
||||
maxLength={50}
|
||||
size="large"
|
||||
prefix={<UserOutlined />}
|
||||
prefix={<UserRound size={16} />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<div
|
||||
@ -324,10 +300,6 @@ const MemorySettings = () => {
|
||||
return user === DEFAULT_USER_ID ? t('memory.default_user') : user
|
||||
}
|
||||
|
||||
const getUserAvatar = (user: string) => {
|
||||
return user === DEFAULT_USER_ID ? user.slice(0, 1).toUpperCase() : user.slice(0, 2).toUpperCase()
|
||||
}
|
||||
|
||||
// Load unique users from database
|
||||
const loadUniqueUsers = useCallback(async () => {
|
||||
try {
|
||||
@ -616,7 +588,7 @@ const MemorySettings = () => {
|
||||
</HStack>
|
||||
<HStack style={{ alignItems: 'center', gap: 10 }}>
|
||||
<Switch checked={globalMemoryEnabled} onChange={handleGlobalMemoryToggle} />
|
||||
<Button icon={<Settings2 size={16} />} onClick={() => setSettingsModalVisible(true)} />
|
||||
<Button type="text" icon={<Settings2 size={16} />} onClick={() => setSettingsModalVisible(true)} />
|
||||
</HStack>
|
||||
</HStack>
|
||||
</SettingGroup>
|
||||
@ -632,52 +604,12 @@ const MemorySettings = () => {
|
||||
{allMemories.length} {t('memory.total_memories')}
|
||||
</SettingHelpText>
|
||||
</div>
|
||||
<Select
|
||||
value={currentUser}
|
||||
onChange={handleUserSwitch}
|
||||
style={{ width: 200 }}
|
||||
dropdownRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<div style={{ padding: '8px' }}>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => setAddUserModalVisible(true)}
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start'
|
||||
}}>
|
||||
<HStack alignItems="center" gap={10}>
|
||||
<UserAddOutlined />
|
||||
<span>{t('memory.add_new_user')}</span>
|
||||
</HStack>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}>
|
||||
<Option value={DEFAULT_USER_ID}>
|
||||
<HStack alignItems="center" gap={10}>
|
||||
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
|
||||
{getUserAvatar(DEFAULT_USER_ID)}
|
||||
</Avatar>
|
||||
<span>{t('memory.default_user')}</span>
|
||||
</HStack>
|
||||
</Option>
|
||||
{uniqueUsers
|
||||
.filter((user) => user !== DEFAULT_USER_ID)
|
||||
.map((user) => (
|
||||
<Option key={user} value={user}>
|
||||
<HStack alignItems="center" gap={10}>
|
||||
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
|
||||
{getUserAvatar(user)}
|
||||
</Avatar>
|
||||
<span>{user}</span>
|
||||
</HStack>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<UserSelector
|
||||
currentUser={currentUser}
|
||||
uniqueUsers={uniqueUsers}
|
||||
onUserSwitch={handleUserSwitch}
|
||||
onAddUser={() => setAddUserModalVisible(true)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
@ -703,7 +635,7 @@ const MemorySettings = () => {
|
||||
allowClear
|
||||
style={{ width: 240 }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setAddMemoryModalVisible(true)}>
|
||||
<Button type="primary" icon={<PlusIcon size={18} />} onClick={() => setAddMemoryModalVisible(true)}>
|
||||
{t('memory.add_memory')}
|
||||
</Button>
|
||||
<Dropdown
|
||||
@ -712,7 +644,7 @@ const MemorySettings = () => {
|
||||
{
|
||||
key: 'refresh',
|
||||
label: t('common.refresh'),
|
||||
icon: <ReloadOutlined />,
|
||||
icon: <RefreshIcon size={14} />,
|
||||
onClick: () => loadMemories(currentUser)
|
||||
},
|
||||
{
|
||||
@ -722,7 +654,7 @@ const MemorySettings = () => {
|
||||
{
|
||||
key: 'reset',
|
||||
label: t('memory.reset_memories'),
|
||||
icon: <DeleteOutlined />,
|
||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: () => handleResetMemories(currentUser)
|
||||
},
|
||||
@ -735,7 +667,7 @@ const MemorySettings = () => {
|
||||
{
|
||||
key: 'deleteUser',
|
||||
label: t('memory.delete_user'),
|
||||
icon: <UserDeleteOutlined />,
|
||||
icon: <UserRoundMinus size={14} className="lucide-custom" />,
|
||||
danger: true,
|
||||
onClick: () => handleDeleteUser(currentUser)
|
||||
}
|
||||
@ -745,7 +677,7 @@ const MemorySettings = () => {
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight">
|
||||
<Button icon={<MoreOutlined />}>{t('common.more')}</Button>
|
||||
<Button icon={<MenuIcon size={16} />}>{t('common.more')}</Button>
|
||||
</Dropdown>
|
||||
</Space>
|
||||
</div>
|
||||
@ -765,7 +697,7 @@ const MemorySettings = () => {
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
icon={<PlusIcon size={18} />}
|
||||
onClick={() => setAddMemoryModalVisible(true)}
|
||||
size="large">
|
||||
{t('memory.add_first_memory')}
|
||||
@ -777,7 +709,7 @@ const MemorySettings = () => {
|
||||
<>
|
||||
{loading && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 300 }}>
|
||||
<Spin size="large" />
|
||||
<Spin indicator={<LoadingIcon color="var(--color-text-2)" />} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -792,21 +724,21 @@ const MemorySettings = () => {
|
||||
<MemoryItem key={memory.id}>
|
||||
<div className="memory-header">
|
||||
<div className="memory-meta">
|
||||
<CalendarOutlined style={{ marginRight: 4 }} />
|
||||
<Calendar size={14} style={{ marginRight: 4 }} />
|
||||
<span>{memory.createdAt ? dayjs(memory.createdAt).fromNow() : '-'}</span>
|
||||
</div>
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
icon={<EditIcon size={14} />}
|
||||
onClick={() => handleEditMemory(memory)}
|
||||
/>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
icon={<DeleteIcon size={14} className="lucide-custom" />}
|
||||
onClick={() => {
|
||||
window.modal.confirm({
|
||||
centered: true,
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { Avatar, Button, Select, Space, Tooltip } from 'antd'
|
||||
import { UserRoundPlus } from 'lucide-react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { DEFAULT_USER_ID } from './constants'
|
||||
|
||||
interface UserSelectorProps {
|
||||
currentUser: string
|
||||
uniqueUsers: string[]
|
||||
onUserSwitch: (userId: string) => void
|
||||
onAddUser: () => void
|
||||
}
|
||||
|
||||
const UserSelector: React.FC<UserSelectorProps> = ({ currentUser, uniqueUsers, onUserSwitch, onAddUser }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getUserAvatar = useCallback((user: string) => {
|
||||
return user === DEFAULT_USER_ID ? user.slice(0, 1).toUpperCase() : user.slice(0, 2).toUpperCase()
|
||||
}, [])
|
||||
|
||||
const renderLabel = useCallback(
|
||||
(userId: string, userName: string) => {
|
||||
return (
|
||||
<HStack alignItems="center" gap={10}>
|
||||
<Avatar size={20} style={{ background: 'var(--color-primary)' }}>
|
||||
{getUserAvatar(userId)}
|
||||
</Avatar>
|
||||
<span>{userName}</span>
|
||||
</HStack>
|
||||
)
|
||||
},
|
||||
[getUserAvatar]
|
||||
)
|
||||
|
||||
const options = useMemo(() => {
|
||||
const defaultOption = {
|
||||
value: DEFAULT_USER_ID,
|
||||
label: renderLabel(DEFAULT_USER_ID, t('memory.default_user'))
|
||||
}
|
||||
|
||||
const userOptions = uniqueUsers
|
||||
.filter((user) => user !== DEFAULT_USER_ID)
|
||||
.map((user) => ({
|
||||
value: user,
|
||||
label: renderLabel(user, user)
|
||||
}))
|
||||
|
||||
return [defaultOption, ...userOptions]
|
||||
}, [renderLabel, t, uniqueUsers])
|
||||
|
||||
return (
|
||||
<Space.Compact>
|
||||
<Select value={currentUser} onChange={onUserSwitch} style={{ width: 200 }} options={options} />
|
||||
<Tooltip title={t('memory.add_new_user')}>
|
||||
<Button type="default" onClick={onAddUser} icon={<UserRoundPlus size={16} />} />
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserSelector
|
||||
@ -0,0 +1 @@
|
||||
export const DEFAULT_USER_ID = 'default-user'
|
||||
@ -1,5 +1,6 @@
|
||||
import { CloseCircleFilled, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||
import { ResetIcon } from '@renderer/components/Icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { DEFAULT_CONTEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
@ -155,9 +156,9 @@ const AssistantSettings: FC = () => {
|
||||
marginTop: 0
|
||||
}}>
|
||||
{t('settings.assistant.model_params')}
|
||||
<Button onClick={onReset} style={{ width: 81 }}>
|
||||
{t('chat.settings.reset')}
|
||||
</Button>
|
||||
<Tooltip title={t('common.reset')} mouseLeaveDelay={0}>
|
||||
<Button type="text" onClick={onReset} icon={<ResetIcon size={16} />} />
|
||||
</Tooltip>
|
||||
</SettingSubtitle>
|
||||
<SettingRow>
|
||||
<HStack alignItems="center">
|
||||
|
||||
@ -1,26 +1,22 @@
|
||||
import { RedoOutlined } from '@ant-design/icons'
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import ModelSelector from '@renderer/components/ModelSelector'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { isEmbeddingModel, isRerankModel, isTextToImageModel } from '@renderer/config/models'
|
||||
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAssistants, useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getModelUniqId, hasModel } from '@renderer/services/ModelService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setQuickAssistantId } from '@renderer/store/llm'
|
||||
import { setTranslateModelPrompt } from '@renderer/store/settings'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Button, Select, Tooltip } from 'antd'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { find } from 'lodash'
|
||||
import { CircleHelp, FolderPen, Languages, MessageSquareMore, Rocket, Settings2 } from 'lucide-react'
|
||||
import { FolderPen, Languages, MessageSquareMore, Settings2 } from 'lucide-react'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDescription, SettingGroup, SettingTitle } from '..'
|
||||
import DefaultAssistantSettings from './DefaultAssistantSettings'
|
||||
@ -29,8 +25,6 @@ import TopicNamingModalPopup from './TopicNamingModalPopup'
|
||||
const ModelSettings: FC = () => {
|
||||
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
|
||||
useDefaultModel()
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { assistants } = useAssistants()
|
||||
const { providers } = useProviders()
|
||||
const allModels = providers.map((p) => p.models).flat()
|
||||
const { theme } = useTheme()
|
||||
@ -38,7 +32,6 @@ const ModelSettings: FC = () => {
|
||||
const { translateModelPrompt } = useSettings()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const { quickAssistantId } = useAppSelector((state) => state.llm)
|
||||
|
||||
const modelPredicate = useCallback(
|
||||
(m: Model) => !isEmbeddingModel(m) && !isRerankModel(m) && !isTextToImageModel(m),
|
||||
@ -149,127 +142,8 @@ const ModelSettings: FC = () => {
|
||||
</HStack>
|
||||
<SettingDescription>{t('settings.models.translate_model_description')}</SettingDescription>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<HStack alignItems="center" style={{ marginBottom: 12 }}>
|
||||
<SettingTitle>
|
||||
<HStack alignItems="center" gap={10}>
|
||||
<Rocket size={18} color="var(--color-text)" />
|
||||
{t('settings.models.quick_assistant_model')}
|
||||
<Tooltip title={t('selection.settings.user_modal.model.tooltip')} arrow>
|
||||
<QuestionIcon size={12} />
|
||||
</Tooltip>
|
||||
<Spacer />
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={0}>
|
||||
<StyledButton
|
||||
type={!quickAssistantId ? 'primary' : 'default'}
|
||||
onClick={() => dispatch(setQuickAssistantId(''))}
|
||||
selected={!quickAssistantId}>
|
||||
{t('settings.models.use_model')}
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
type={quickAssistantId ? 'primary' : 'default'}
|
||||
onClick={() => {
|
||||
dispatch(setQuickAssistantId(defaultAssistant.id))
|
||||
}}
|
||||
selected={!!quickAssistantId}>
|
||||
{t('settings.models.use_assistant')}
|
||||
</StyledButton>
|
||||
</HStack>
|
||||
</SettingTitle>
|
||||
</HStack>
|
||||
{!quickAssistantId ? null : (
|
||||
<HStack alignItems="center" style={{ marginTop: 12 }}>
|
||||
<Select
|
||||
value={quickAssistantId || defaultAssistant.id}
|
||||
style={{ width: 360 }}
|
||||
onChange={(value) => dispatch(setQuickAssistantId(value))}
|
||||
placeholder={t('settings.models.quick_assistant_selection')}>
|
||||
<Select.Option key={defaultAssistant.id} value={defaultAssistant.id}>
|
||||
<AssistantItem>
|
||||
<ModelAvatar model={defaultAssistant.model || defaultModel} size={18} />
|
||||
<AssistantName>{defaultAssistant.name}</AssistantName>
|
||||
<Spacer />
|
||||
<DefaultTag isCurrent={true}>{t('settings.models.quick_assistant_default_tag')}</DefaultTag>
|
||||
</AssistantItem>
|
||||
</Select.Option>
|
||||
{assistants
|
||||
.filter((a) => a.id !== defaultAssistant.id)
|
||||
.map((a) => (
|
||||
<Select.Option key={a.id} value={a.id}>
|
||||
<AssistantItem>
|
||||
<ModelAvatar model={a.model || defaultModel} size={18} />
|
||||
<AssistantName>{a.name}</AssistantName>
|
||||
<Spacer />
|
||||
</AssistantItem>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</HStack>
|
||||
)}
|
||||
<SettingDescription>{t('settings.models.quick_assistant_model_description')}</SettingDescription>
|
||||
</SettingGroup>
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const QuestionIcon = styled(CircleHelp)`
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
`
|
||||
|
||||
const StyledButton = styled(Button)<{ selected: boolean }>`
|
||||
border-radius: ${(props) => (props.selected ? '6px' : '6px')};
|
||||
z-index: ${(props) => (props.selected ? 1 : 0)};
|
||||
min-width: 80px;
|
||||
|
||||
&:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right-width: 0; // No right border for the first button when not selected
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left-width: 1px; // Ensure left border for the last button
|
||||
}
|
||||
|
||||
// Override Ant Design's default hover and focus styles for a cleaner look
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
z-index: 1;
|
||||
border-color: ${(props) => (props.selected ? 'var(--ant-primary-color)' : 'var(--ant-primary-color-hover)')};
|
||||
box-shadow: ${(props) =>
|
||||
props.selected ? '0 0 0 2px var(--ant-primary-color-outline)' : '0 0 0 2px var(--ant-primary-color-outline)'};
|
||||
}
|
||||
`
|
||||
|
||||
const AssistantItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
height: 28px;
|
||||
`
|
||||
|
||||
const AssistantName = styled.span`
|
||||
max-width: calc(100% - 60px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const Spacer = styled.div`
|
||||
flex: 1;
|
||||
`
|
||||
|
||||
const DefaultTag = styled.span<{ isCurrent: boolean }>`
|
||||
color: ${(props) => (props.isCurrent ? 'var(--color-primary)' : 'var(--color-text-3)')};
|
||||
font-size: 12px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export default ModelSettings
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user