Merge remote-tracking branch 'origin/main' into feat/cherry-store

This commit is contained in:
MyPrototypeWhat 2025-08-05 11:08:51 +08:00
commit 98f83e096b
137 changed files with 6186 additions and 9409 deletions

View File

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

View File

@ -1 +0,0 @@
CLAUDE.md

View File

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

View File

@ -128,3 +128,4 @@ releaseInfo:
内存泄漏修复:优化代码逻辑,解决内存泄漏问题,提升运行稳定性
嵌入模型简化:降低嵌入模型配置复杂度,提高易用性
MCP Tool 长时间运行:增强 MCP 工具的稳定性,支持长时间任务执行
设置页面优化:优化设置页面布局,提升用户体验

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import { Trash } from 'lucide-react'
const DeleteIcon = (props: React.ComponentProps<typeof Trash>) => <Trash size="1rem" {...props} />
export default DeleteIcon

View File

@ -0,0 +1,5 @@
import { Pencil } from 'lucide-react'
const EditIcon = (props: React.ComponentProps<typeof Pencil>) => <Pencil size="1rem" {...props} />
export default EditIcon

View File

@ -0,0 +1,5 @@
import { RefreshCw } from 'lucide-react'
const RefreshIcon = (props: React.ComponentProps<typeof RefreshCw>) => <RefreshCw size="1rem" {...props} />
export default RefreshIcon

View File

@ -0,0 +1,5 @@
import { RotateCcw } from 'lucide-react'
const ResetIcon = (props: React.ComponentProps<typeof RotateCcw>) => <RotateCcw size="1rem" {...props} />
export default ResetIcon

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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": "ブラックリスト",

View File

@ -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": "Черный список",

View File

@ -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": "黑名单",

View File

@ -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": "黑名單",

View File

@ -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": "χωρίς πρόξενο",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -104,6 +104,7 @@ const NewAppButton: FC<Props> = ({ size = 60 }) => {
setIsModalVisible(false)
setFileList([])
}}
maskClosable={false}
footer={null}
transitionName="animation-move-down"
centered>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 }
]
// 获取模型分组数据

View File

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

View File

@ -165,10 +165,6 @@ const Container = styled.div`
flex: 1;
flex-direction: column;
overflow: hidden;
.ant-btn {
line-height: 0;
}
`
const EmojiButtonWrapper = styled.div`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -115,6 +115,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
maskClosable={false}
width={800}
height="80vh"
loading={jsonSaving}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export const DEFAULT_USER_ID = 'default-user'

View File

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

View File

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