Compare commits

..

31 Commits

Author SHA1 Message Date
MyPrototypeWhat
0ba76af300 refactor: reorganize routing and clean up unused components in Discover and MCP Servers pages
- Introduced a new routers.ts file to centralize routing logic for the Discover page.
- Updated DiscoverContent to utilize dynamic routing based on the new routers configuration.
- Removed commented-out routes and unnecessary imports from various components.
- Simplified navigation logic in MCP Servers pages by adjusting route paths.
- Cleaned up Navbar components in multiple pages for better maintainability.
2025-06-09 19:38:11 +08:00
MyPrototypeWhat
f2c52dfe89 Merge remote-tracking branch 'origin/feat/sidebar' into feat/cherry-store-render 2025-06-09 17:49:42 +08:00
MyPrototypeWhat
880d325028 fix: add missing icon import in AppsPage component 2025-06-09 17:49:30 +08:00
MyPrototypeWhat
2eb421a1de Merge tag 'feat/sidebar' into feat/cherry-store-render 2025-06-09 17:48:54 +08:00
MyPrototypeWhat
4a0924ce15 Merge branch 'feat/cherry-store-render' into feat/sidebar 2025-06-09 17:42:18 +08:00
MyPrototypeWhat
b7eef3b753 refactor: update ESLint configuration and clean up useDiscoverCategories logic
- Added new path to ESLint configuration for better linting coverage.
- Refactored useDiscoverCategories to simplify category handling and improve URL path matching logic.
- Removed unnecessary items from initialCategories for clarity and maintainability.
2025-06-09 16:19:00 +08:00
MyPrototypeWhat
d3f5887980 refactor: update Tailwind CSS integration in electron.vite.config.ts
- Changed the import of Tailwind CSS plugin to a dynamic import for improved performance.
- Ensured compatibility with the existing Vite configuration while maintaining functionality.
2025-06-09 16:08:03 +08:00
Teo
8db2059605 style(color.scss): update border color to improve UI consistency 2025-06-09 15:54:20 +08:00
Teo
d11b98dfbb feat(Inputbar): add SettingButton component for settings access 2025-06-09 15:54:16 +08:00
MyPrototypeWhat
38330c4c81 refactor: update layout of AgentsPage and AppsPage, remove Navbar components
- Refactored AgentsPage and AppsPage to enhance layout by replacing Navbar components with div wrappers.
- Integrated Tailwind CSS for improved styling consistency.
- Adjusted input components for better alignment and spacing.
2025-06-09 15:43:34 +08:00
kangfenmao
b762cfd60b feat: enhance summarization prompt and add topic sidebar visibility toggle 2025-06-09 15:39:30 +08:00
MyPrototypeWhat
278397f7c8 fix: update sidebar icons and enhance DiscoverPage layout
- Updated DiscoverPage to include full height and width styling for better layout.
- Modified sidebar icon visibility logic in migration to ensure 'discover' is added correctly while filtering out specific icons.
- Changed default sidebar icons to replace 'store' with 'discover' for consistency.
2025-06-09 15:27:48 +08:00
MyPrototypeWhat
c6d5faff73 feat: update TypeScript configuration and enhance discover page layout
- Added support for the @modelcontextprotocol/sdk in tsconfig.node.json.
- Updated import paths in provider.ts to include .js extensions.
- Enhanced AgentsPage layout by integrating Navbar and Input components.
- Refactored DiscoverPage to remove DialogManagerProvider and related components, simplifying the structure.
- Removed unused dialog components and hooks to streamline the discover functionality.
- Minor adjustments to AssistantSettings and Vercel tabs for improved code clarity.
2025-06-09 15:22:25 +08:00
kangfenmao
9cac8fba56 feat: add event listener to MainSidebar for topic tab navigation 2025-06-09 15:04:44 +08:00
MyPrototypeWhat
b7d9949832 Merge remote-tracking branch 'origin/main' into feat/cherry-store-render 2025-06-09 14:47:41 +08:00
kangfenmao
b4665509ab fix: set WindowService transparency to false for consistent behavior across platforms 2025-06-09 14:45:12 +08:00
kangfenmao
21e88b02ea refactor: simplify HomeTabs component by removing unused imports and commented code, update AssistantAddItem hover styles 2025-06-09 14:39:29 +08:00
kangfenmao
10caef2c4c refactor: clean up MainSidebar and useChat hooks, remove unused state handling and improve topic selection logic 2025-06-09 14:19:11 +08:00
kangfenmao
6ea1bcc7d1 fix: invert transparency setting for WindowService based on OS 2025-06-09 13:49:40 +08:00
kangfenmao
06a60c4871 feat: implement ChatNavbar component and enhance MainNavbar with search functionality 2025-06-09 12:05:41 +08:00
kangfenmao
684367bf7c fix: adjust navbar and title bar dimensions, update icon handling 2025-06-09 11:50:30 +08:00
kangfenmao
75b9e2f408 feat: new app sidebar 2025-06-09 11:20:41 +08:00
lizhixuan
475c1e38df feat: refactor store to discover transition and enhance UI components
- Updated package.json to include 'usehooks-ts' and upgraded 'lucide-react' to version 0.511.0.
- Replaced 'store' with 'discover' in the routing and sidebar components for improved navigation.
- Introduced new DiscoverPage and related components for better organization of content.
- Enhanced localization support by adding Chinese translations for the discover feature.
- Removed deprecated store components to streamline the codebase and improve maintainability.
2025-05-18 16:08:26 +08:00
MyPrototypeWhat
80289f1dc3 feat: update store components and add dialog management functionality
- Updated package.json to use the latest version of the 'motion' library.
- Refactored store components to improve organization and user experience, including the addition of AssistantCard and MiniAppCard components.
- Introduced a DialogManager for handling dialog states and interactions.
- Enhanced StoreContent and StoreSidebar components to support new item types and improved layout.
- Added new JSON data for mini-apps and updated store categories for better accessibility.
2025-05-16 19:11:46 +08:00
MyPrototypeWhat
ef16558947 feat: enhance Prettier configuration and update store components
- Added Tailwind CSS support to Prettier configuration with new settings for styles and functions.
- Updated package.json to include the prettier-plugin-tailwindcss dependency.
- Refactored various store components for improved layout and organization, including adjustments to error handling and component structure.
- Enhanced CSS styles for better responsiveness and visual consistency across components.
2025-05-15 18:18:54 +08:00
MyPrototypeWhat
c799f15fcc feat: update store components and enhance assistant functionality
- Refactored store components to improve organization and user experience, including the introduction of new GridView and ListView components.
- Implemented a detail dialog for displaying item information and installation options.
- Enhanced the store sidebar with collapsible categories for better navigation.
- Updated data structures to support dynamic subcategory handling and improved filtering capabilities.
- Added utility functions for dialog and collapsible components to streamline UI interactions.
2025-05-14 17:17:24 +08:00
MyPrototypeWhat
802402e922 feat: add store categories and items with enhanced filtering functionality
- Introduced new JSON files for store categories and assistant items to improve organization and accessibility.
- Implemented a conversion script to dynamically generate the assistant items list from agents data.
- Refactored store components to utilize the new data structure, enhancing the store layout and user experience.
- Added loading states and error handling for category and item fetching processes.
- Created new GridView and ListView components for displaying store items in different formats.
2025-05-13 19:25:37 +08:00
lizhixuan
37482bca7b feat: refactor electron and vitest configuration for dynamic imports and improved structure
- Updated electron.vite.config.ts to use dynamic imports for Tailwind CSS.
- Refactored vitest.config.ts to asynchronously retrieve renderer configuration from electron.vite.config.
- Enhanced plugin and alias management for better maintainability and performance.
2025-05-12 23:37:11 +08:00
lizhixuan
184713dba8 feat: enhance store page with new components and functionality
- Updated component paths in components.json for better organization.
- Added 'motion' library to package.json for animations.
- Refactored TypeScript configuration to include new renderer paths.
- Implemented new StoreContent and StoreSidebar components for improved store layout.
- Integrated store categories and items with filtering capabilities.
- Enhanced UI with Tailwind CSS animations and styles for a better user experience.
2025-05-12 22:58:54 +08:00
MyPrototypeWhat
0a0956cfc4 feat: update store page and integrate new UI components
- Updated Tailwind CSS configuration and styles for the store page.
- Added new UI components including Card, Badge, DropdownMenu, and Sidebar for enhanced user experience.
- Implemented store categories and items with filtering functionality.
- Introduced mobile responsiveness with a custom hook for detecting mobile devices.
- Enhanced theme management to support dynamic theme changes.
- Added new utility functions for improved class name management.
2025-05-12 19:28:23 +08:00
MyPrototypeWhat
0a0bbad77f feat: integrate Tailwind CSS and add store page functionality
- Introduced Tailwind CSS for styling by adding a new configuration file and global styles.
- Created a new Store page with a button to test configuration success.
- Updated routing to include the Store page and added corresponding sidebar icon.
- Enhanced the settings store to include the new Store icon in the sidebar.
- Updated translations for the Store page in Chinese.
- Added utility functions for class name management using Tailwind CSS.
2025-05-09 16:36:50 +08:00
157 changed files with 7108 additions and 3730 deletions

View File

@@ -4,5 +4,8 @@
"printWidth": 120,
"trailingComma": "none",
"endOfLine": "lf",
"bracketSameLine": true
"bracketSameLine": true,
"tailwindStylesheet": "./src/renderer/src/assets/styles/tailwind.css",
"tailwindFunctions": ["clsx"],
"plugins": ["prettier-plugin-tailwindcss"],
}

View File

@@ -151,7 +151,7 @@ index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data && typeof response.data[0]?.embedding === 'string') {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
@@ -266,7 +266,7 @@ index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data && typeof response.data[0]?.embedding === 'string') {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);

View File

@@ -188,7 +188,7 @@ Thank you for your support and contributions!
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioHQ
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/renderer/src/assets/styles/tailwind.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@renderer/ui/third-party",
"utils": "@renderer/utils",
"ui": "@renderer/ui",
"lib": "@renderer/lib",
"hooks": "@renderer/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -190,7 +190,7 @@ Cherry Studio への貢献を歓迎します!以下の方法で貢献できま
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioHQ
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram

View File

@@ -202,7 +202,7 @@ https://docs.cherry-ai.com
[deepwiki-shield]: https://img.shields.io/badge/Deepwiki-CherryHQ-0088CC?style=plastic
[deepwiki-link]: https://deepwiki.com/CherryHQ/cherry-studio
[twitter-shield]: https://img.shields.io/badge/Twitter-CherryStudioApp-0088CC?style=plastic&logo=x
[twitter-link]: https://twitter.com/CherryStudioHQ
[twitter-link]: https://twitter.com/CherryStudioApp
[discord-shield]: https://img.shields.io/badge/Discord-@CherryStudio-0088CC?style=plastic&logo=discord
[discord-link]: https://discord.gg/wez8HtpxqQ
[telegram-shield]: https://img.shields.io/badge/Telegram-@CherryStudioAI-0088CC?style=plastic&logo=telegram

View File

@@ -107,11 +107,8 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
划词助手:支持文本选择快捷键、开关快捷键、思考块支持和引用功能
复制功能新增纯文本复制去除Markdown格式符号
知识库支持设置向量维度修复Ollama分数错误和维度编辑问题
多语言:增加模型名称多语言提示和翻译源语言手动选择
文件管理:修复主题/消息删除时文件未清理问题,优化文件选择流程
模型修复Gemini模型推理预算、Voyage AI嵌入问题和DeepSeek翻译模型更新
图像功能统一图片查看器支持Base64图片渲染修复图片预览相关问题
UI实现标签折叠/拖拽排序,修复气泡溢出,增加引文索引显示
新增划词助手
助手支持分组
支持主题颜色切换
划词助手支持应用过滤
翻译模块功能改进

View File

@@ -40,6 +40,7 @@ export default defineConfig({
},
renderer: {
plugins: [
(async () => (await import('@tailwindcss/vite')).default())(),
react({
plugins: [
[

View File

@@ -62,7 +62,8 @@ export default defineConfig([
'.yarn/**',
'.gitignore',
'scripts/cloudflare-worker.js',
'src/main/integration/nutstore/sso/lib/**'
'src/main/integration/nutstore/sso/lib/**',
'src/renderer/src/ui/**'
]
}
])

View File

@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.4.2",
"version": "1.4.1",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -68,11 +68,9 @@
"@cherrystudio/embedjs-loader-sitemap": "^0.1.31",
"@cherrystudio/embedjs-loader-web": "^0.1.31",
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
"@cherrystudio/embedjs-ollama": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31",
"@electron-toolkit/utils": "^3.0.0",
"@langchain/community": "^0.3.36",
"@langchain/ollama": "^0.2.1",
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tanstack/react-query": "^5.27.0",
"@types/react-infinite-scroll-component": "^5.0.0",
@@ -94,8 +92,7 @@
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"remove-markdown": "^0.6.2",
"selection-hook": "^0.9.23",
"selection-hook": "^0.9.22",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"webdav": "^5.8.0",
@@ -122,9 +119,18 @@
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@playwright/test": "^1.52.0",
"@radix-ui/react-collapsible": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.6",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.4.2",
"@swc/plugin-styled-components": "^7.1.5",
"@tailwindcss/vite": "^4.1.5",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
@@ -153,6 +159,8 @@
"antd": "^5.22.5",
"axios": "^1.7.3",
"browser-image-compression": "^2.0.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"color": "^5.0.0",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
@@ -176,15 +184,17 @@
"lint-staged": "^15.5.0",
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
"lucide-react": "^0.487.0",
"lucide-react": "^0.511.0",
"mermaid": "^11.6.0",
"mime": "^4.0.4",
"motion": "^12.10.5",
"motion": "^12.12.1",
"next-themes": "^0.4.6",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"p-queue": "^8.1.0",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"rc-virtual-list": "^3.18.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -208,11 +218,16 @@
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.88.0",
"shiki": "^3.4.2",
"sonner": "^2.0.3",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tailwind-merge": "^3.2.0",
"tailwindcss": "^4.1.5",
"tiny-pinyin": "^1.3.2",
"tokenx": "^0.4.1",
"tw-animate-css": "^1.2.9",
"typescript": "^5.6.2",
"usehooks-ts": "^3.1.1",
"uuid": "^10.0.0",
"vite": "6.2.6",
"vitest": "^3.1.4"

View File

@@ -11,13 +11,13 @@ if (isDev) {
export const DATA_PATH = getDataPath()
export const titleBarOverlayDark = {
height: 40,
height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#fff'
}
export const titleBarOverlayLight = {
height: 40,
height: 42,
color: 'rgba(255,255,255,0)',
symbolColor: '#000'
}

View File

@@ -5,15 +5,8 @@ import EmbeddingsFactory from './EmbeddingsFactory'
export default class Embeddings {
private sdk: BaseEmbeddings
constructor({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
this.sdk = EmbeddingsFactory.create({
model,
provider,
apiKey,
apiVersion,
baseURL,
dimensions
} as KnowledgeBaseParams)
constructor({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams) {
this.sdk = EmbeddingsFactory.create({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
}
public async init(): Promise<void> {
return this.sdk.init()

View File

@@ -1,49 +1,20 @@
import type { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { OllamaEmbeddings } from '@cherrystudio/embedjs-ollama'
import { OpenAiEmbeddings } from '@cherrystudio/embedjs-openai'
import { AzureOpenAiEmbeddings } from '@cherrystudio/embedjs-openai/src/azure-openai-embeddings'
import { getInstanceName } from '@main/utils'
import { KnowledgeBaseParams } from '@types'
import { SUPPORTED_DIM_MODELS as VOYAGE_SUPPORTED_DIM_MODELS, VoyageEmbeddings } from './VoyageEmbeddings'
import VoyageEmbeddings from './VoyageEmbeddings'
export default class EmbeddingsFactory {
static create({ model, provider, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
static create({ model, apiKey, apiVersion, baseURL, dimensions }: KnowledgeBaseParams): BaseEmbeddings {
const batchSize = 10
if (provider === 'voyageai') {
if (VOYAGE_SUPPORTED_DIM_MODELS.includes(model)) {
return new VoyageEmbeddings({
modelName: model,
apiKey,
outputDimension: dimensions,
batchSize: 8
})
} else {
return new VoyageEmbeddings({
modelName: model,
apiKey,
batchSize: 8
})
}
}
if (provider === 'ollama') {
if (baseURL.includes('v1/')) {
return new OllamaEmbeddings({
model: model,
baseUrl: baseURL.replace('v1/', ''),
requestOptions: {
// @ts-ignore expected
'encoding-format': 'float'
}
})
}
return new OllamaEmbeddings({
model: model,
baseUrl: baseURL,
requestOptions: {
// @ts-ignore expected
'encoding-format': 'float'
}
if (model.includes('voyage')) {
return new VoyageEmbeddings({
modelName: model,
apiKey,
outputDimension: dimensions,
batchSize: 8
})
}
if (apiVersion !== undefined) {

View File

@@ -1,20 +1,16 @@
import { BaseEmbeddings } from '@cherrystudio/embedjs-interfaces'
import { VoyageEmbeddings as _VoyageEmbeddings } from '@langchain/community/embeddings/voyage'
/**
* 支持设置嵌入维度的模型
*/
export const SUPPORTED_DIM_MODELS = ['voyage-3-large', 'voyage-3.5', 'voyage-3.5-lite', 'voyage-code-3']
export class VoyageEmbeddings extends BaseEmbeddings {
export default class VoyageEmbeddings extends BaseEmbeddings {
private model: _VoyageEmbeddings
constructor(private readonly configuration?: ConstructorParameters<typeof _VoyageEmbeddings>[0]) {
super()
if (!this.configuration) this.configuration = {}
if (!this.configuration.modelName) this.configuration.modelName = 'voyage-3'
if (!SUPPORTED_DIM_MODELS.includes(this.configuration.modelName) && this.configuration.outputDimension) {
throw new Error(`VoyageEmbeddings only supports ${SUPPORTED_DIM_MODELS.join(', ')}`)
}
if (!this.configuration.outputDimension) {
throw new Error('You need to pass in the optional dimensions parameter for this model')
}
this.model = new _VoyageEmbeddings(this.configuration)
}
override async getDimensions(): Promise<number> {

View File

@@ -34,26 +34,6 @@ if (isWin) {
app.commandLine.appendSwitch('wm-window-animations-disabled')
}
// Enable features for unresponsive renderer js call stacks
app.commandLine.appendSwitch('enable-features', 'DocumentPolicyIncludeJSCallStacksInCrashReports')
app.on('web-contents-created', (_, webContents) => {
webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Document-Policy': ['include-js-call-stacks-in-crash-reports']
}
})
})
webContents.on('unresponsive', async () => {
// Interrupt execution and collect call stack from unresponsive renderer
Logger.error('Renderer unresponsive start')
const callStack = await webContents.mainFrame.collectJavaScriptCallStack()
Logger.error('Renderer unresponsive js call stack\n', callStack)
})
})
// in production mode, handle uncaught exception and unhandled rejection globally
if (!isDev) {
// handle uncaught exception

View File

@@ -110,21 +110,13 @@ class KnowledgeService {
private getRagApplication = async ({
id,
model,
provider,
apiKey,
apiVersion,
baseURL,
dimensions
}: KnowledgeBaseParams): Promise<RAGApplication> => {
let ragApplication: RAGApplication
const embeddings = new Embeddings({
model,
provider,
apiKey,
apiVersion,
baseURL,
dimensions
} as KnowledgeBaseParams)
const embeddings = new Embeddings({ model, apiKey, apiVersion, baseURL, dimensions } as KnowledgeBaseParams)
try {
ragApplication = await new RAGApplicationBuilder()
.setModel('NO_MODEL')

View File

@@ -14,7 +14,6 @@ import type {
import type { ActionItem } from '../../renderer/src/types/selectionTypes'
import { ConfigKeys, configManager } from './ConfigManager'
import storeSyncService from './StoreSyncService'
let SelectionHook: SelectionHookConstructor | null = null
try {
@@ -40,8 +39,7 @@ type RelativeOrientation =
enum TriggerMode {
Selected = 'selected',
Ctrlkey = 'ctrlkey',
Shortcut = 'shortcut'
Ctrlkey = 'ctrlkey'
}
/** SelectionService is a singleton class that manages the selection hook and the toolbar window
@@ -316,8 +314,6 @@ export class SelectionService {
this.toolbarWindow.close()
this.toolbarWindow = null
}
this.closePreloadedActionWindows()
this.started = false
this.logInfo('SelectionService Stopped')
return true
@@ -338,21 +334,6 @@ export class SelectionService {
this.logInfo('SelectionService Quitted')
}
/**
* Toggle the enabled state of the selection service
* Will sync the new enabled store to all renderer windows
*/
public toggleEnabled(enabled: boolean | undefined = undefined) {
if (!this.selectionHook) return
const newEnabled = enabled === undefined ? !configManager.getSelectionAssistantEnabled() : enabled
configManager.setSelectionAssistantEnabled(newEnabled)
//sync the new enabled state to all renderer windows
storeSyncService.syncToRenderer('selectionStore/setSelectionEnabled', newEnabled)
}
/**
* Create and configure the toolbar window
* Sets up window properties, event handlers, and loads the toolbar UI
@@ -397,9 +378,6 @@ export class SelectionService {
// Clean up when closed
this.toolbarWindow.on('closed', () => {
if (!this.toolbarWindow?.isDestroyed()) {
this.toolbarWindow?.destroy()
}
this.toolbarWindow = null
})
@@ -585,21 +563,6 @@ export class SelectionService {
return startTop.y === endTop.y && startBottom.y === endBottom.y
}
/**
* Get the user selected text and process it (trigger by shortcut)
*
* it's a public method used by shortcut service
*/
public processSelectTextByShortcut(): void {
if (!this.selectionHook || !this.started || this.triggerMode !== TriggerMode.Shortcut) return
const selectionData = this.selectionHook.getCurrentSelection()
if (selectionData) {
this.processTextSelection(selectionData)
}
}
/**
* Determine if the text selection should be processed by filter mode&list
* @param selectionData Text selection information and coordinates
@@ -891,6 +854,7 @@ export class SelectionService {
this.lastCtrlkeyDownTime = -1
const selectionData = this.selectionHook!.getCurrentSelection()
if (selectionData) {
this.processTextSelection(selectionData)
}
@@ -994,17 +958,6 @@ export class SelectionService {
}
}
/**
* Close all preloaded action windows
*/
private closePreloadedActionWindows() {
for (const actionWindow of this.preloadedActionWindows) {
if (!actionWindow.isDestroyed()) {
actionWindow.destroy()
}
}
}
/**
* Preload a new action window asynchronously
* This method is called after popping a window to ensure we always have windows ready
@@ -1153,44 +1106,29 @@ export class SelectionService {
* Manages appropriate event listeners for each mode
*/
private processTriggerMode() {
switch (this.triggerMode) {
case TriggerMode.Selected:
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
if (this.triggerMode === TriggerMode.Selected) {
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false
}
this.isCtrlkeyListenerActive = false
}
this.selectionHook!.setSelectionPassiveMode(false)
break
case TriggerMode.Ctrlkey:
if (!this.isCtrlkeyListenerActive) {
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
this.selectionHook!.setSelectionPassiveMode(false)
} else if (this.triggerMode === TriggerMode.Ctrlkey) {
if (!this.isCtrlkeyListenerActive) {
this.selectionHook!.on('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.on('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = true
}
this.isCtrlkeyListenerActive = true
}
this.selectionHook!.setSelectionPassiveMode(true)
break
case TriggerMode.Shortcut:
//remove the ctrlkey listener, don't need any key listener for shortcut mode
if (this.isCtrlkeyListenerActive) {
this.selectionHook!.off('key-down', this.handleKeyDownCtrlkeyMode)
this.selectionHook!.off('key-up', this.handleKeyUpCtrlkeyMode)
this.isCtrlkeyListenerActive = false
}
this.selectionHook!.setSelectionPassiveMode(true)
break
this.selectionHook!.setSelectionPassiveMode(true)
}
}
public writeToClipboard(text: string): boolean {
if (!this.selectionHook || !this.started) return false
return this.selectionHook.writeToClipboard(text)
return this.selectionHook?.writeToClipboard(text) ?? false
}
/**

View File

@@ -4,16 +4,10 @@ import { BrowserWindow, globalShortcut } from 'electron'
import Logger from 'electron-log'
import { configManager } from './ConfigManager'
import selectionService from './SelectionService'
import { windowService } from './WindowService'
let showAppAccelerator: string | null = null
let showMiniWindowAccelerator: string | null = null
let selectionAssistantToggleAccelerator: string | null = null
let selectionAssistantSelectTextAccelerator: string | null = null
//indicate if the shortcuts are registered on app boot time
let isRegisterOnBoot = true
// store the focus and blur handlers for each window to unregister them later
const windowOnHandlers = new Map<BrowserWindow, { onFocusHandler: () => void; onBlurHandler: () => void }>()
@@ -34,18 +28,6 @@ function getShortcutHandler(shortcut: Shortcut) {
return () => {
windowService.toggleMiniWindow()
}
case 'selection_assistant_toggle':
return () => {
if (selectionService) {
selectionService.toggleEnabled()
}
}
case 'selection_assistant_select_text':
return () => {
if (selectionService) {
selectionService.processSelectTextByShortcut()
}
}
default:
return null
}
@@ -55,8 +37,9 @@ function formatShortcutKey(shortcut: string[]): string {
return shortcut.join('+')
}
// convert the shortcut recorded by keyboard event key value to electron global shortcut format
const convertShortcutFormat = (shortcut: string | string[]): string => {
const convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat = (
shortcut: string | string[]
): string => {
const accelerator = (() => {
if (Array.isArray(shortcut)) {
return shortcut
@@ -110,14 +93,11 @@ const convertShortcutFormat = (shortcut: string | string[]): string => {
}
export function registerShortcuts(window: BrowserWindow) {
if (isRegisterOnBoot) {
window.once('ready-to-show', () => {
if (configManager.getLaunchToTray()) {
registerOnlyUniversalShortcuts()
}
})
isRegisterOnBoot = false
}
window.once('ready-to-show', () => {
if (configManager.getLaunchToTray()) {
registerOnlyUniversalShortcuts()
}
})
//only for clearer code
const registerOnlyUniversalShortcuts = () => {
@@ -144,12 +124,7 @@ export function registerShortcuts(window: BrowserWindow) {
}
// only register universal shortcuts when needed
if (
onlyUniversalShortcuts &&
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
shortcut.key
)
) {
if (onlyUniversalShortcuts && !['show_app', 'mini_window'].includes(shortcut.key)) {
return
}
@@ -171,14 +146,6 @@ export function registerShortcuts(window: BrowserWindow) {
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_toggle':
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
break
case 'selection_assistant_select_text':
selectionAssistantSelectTextAccelerator = formatShortcutKey(shortcut.shortcut)
break
//the following ZOOMs will register shortcuts seperately, so will return
case 'zoom_in':
globalShortcut.register('CommandOrControl+=', () => handler(window))
@@ -195,7 +162,9 @@ export function registerShortcuts(window: BrowserWindow) {
return
}
const accelerator = convertShortcutFormat(shortcut.shortcut)
const accelerator = convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(
shortcut.shortcut
)
globalShortcut.register(accelerator, () => handler(window))
} catch (error) {
@@ -212,25 +181,15 @@ export function registerShortcuts(window: BrowserWindow) {
if (showAppAccelerator) {
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
const accelerator = convertShortcutFormat(showAppAccelerator)
const accelerator =
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showAppAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (showMiniWindowAccelerator) {
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
const accelerator = convertShortcutFormat(showMiniWindowAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantToggleAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
if (selectionAssistantSelectTextAccelerator) {
const handler = getShortcutHandler({ key: 'selection_assistant_select_text' } as Shortcut)
const accelerator = convertShortcutFormat(selectionAssistantSelectTextAccelerator)
const accelerator =
convertShortcutRecordedByKeyboardEventKeyValueToElectronGlobalShortcutFormat(showMiniWindowAccelerator)
handler && globalShortcut.register(accelerator, () => handler(window))
}
} catch (error) {
@@ -258,8 +217,6 @@ export function unregisterAllShortcuts() {
try {
showAppAccelerator = null
showMiniWindowAccelerator = null
selectionAssistantToggleAccelerator = null
selectionAssistantSelectTextAccelerator = null
windowOnHandlers.forEach((handlers, window) => {
window.off('focus', handlers.onFocusHandler)
window.off('blur', handlers.onBlurHandler)

View File

@@ -49,23 +49,6 @@ export class StoreSyncService {
this.windowIds = this.windowIds.filter((id) => id !== windowId)
}
/**
* Sync an action to all renderer windows
* @param type Action type, like 'settings/setTray'
* @param payload Action payload
*
* NOTICE: DO NOT use directly in ConfigManager, may cause infinite sync loop
*/
public syncToRenderer(type: string, payload: any): void {
const action: StoreSyncAction = {
type,
payload
}
//-1 means the action is from the main process, will be broadcast to all windows
this.broadcastToOtherWindows(-1, action)
}
/**
* Register IPC handlers for store sync communication
* Handles window subscription, unsubscription and action broadcasting

View File

@@ -56,14 +56,14 @@ export class WindowService {
minHeight: 600,
show: false,
autoHideMenuBar: true,
transparent: isMac,
transparent: false,
vibrancy: 'sidebar',
visualEffectState: 'active',
titleBarStyle: 'hidden',
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 12 },
trafficLightPosition: { x: 12, y: 12 },
...(isLinux ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
@@ -116,6 +116,12 @@ export class WindowService {
app.exit(1)
}
})
mainWindow.webContents.on('unresponsive', () => {
// 在升级到electron 34后可以获取具体js stack trace,目前只打个日志监控下
// https://www.electronjs.org/blog/electron-34-0#unresponsive-renderer-javascript-call-stacks
Logger.error('Renderer process unresponsive')
})
}
private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) {

View File

@@ -1,8 +1,12 @@
import path from 'node:path'
import { getConfigDir } from '@main/utils/file'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth'
import { OAuthClientInformation, OAuthClientInformationFull, OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
import {
OAuthClientInformation,
OAuthClientInformationFull,
OAuthTokens
} from '@modelcontextprotocol/sdk/shared/auth.js'
import Logger from 'electron-log'
import open from 'open'

View File

@@ -65,7 +65,7 @@ export function handleMcpProtocolUrl(url: URL) {
const mainWindow = windowService.getMainWindow()
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
mainWindow.webContents.executeJavaScript("window.navigate('/mcp-servers')")
}
break
}

View File

@@ -5,7 +5,7 @@ import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar'
import MainSidebar from './components/app/MainSidebar'
import TopViewContainer from './components/TopView'
import AntdProvider from './context/AntdProvider'
import { CodeStyleProvider } from './context/CodeStyleProvider'
@@ -13,14 +13,9 @@ import { NotificationProvider } from './context/NotificationProvider'
import StyleSheetManager from './context/StyleSheetManager'
import { ThemeProvider } from './context/ThemeProvider'
import NavigationHandler from './handler/NavigationHandler'
import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
import FilesPage from './pages/files/FilesPage'
import DiscoverPage from './pages/discover'
import HomePage from './pages/home/HomePage'
import KnowledgePage from './pages/knowledge/KnowledgePage'
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage'
function App(): React.ReactElement {
return (
@@ -34,16 +29,18 @@ function App(): React.ReactElement {
<TopViewContainer>
<HashRouter>
<NavigationHandler />
<Sidebar />
<MainSidebar />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} />
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
<Route path="/translate" element={<TranslatePage />} />
<Route path="/files" element={<FilesPage />} />
<Route path="/knowledge" element={<KnowledgePage />} />
<Route path="/apps" element={<AppsPage />} />
{/* <Route path="/agents" element={<AgentsPage />} /> */}
{/* <Route path="/paintings/*" element={<PaintingsRoutePage />} /> */}
{/* <Route path="/translate" element={<TranslatePage />} /> */}
{/* <Route path="/files" element={<FilesPage />} /> */}
{/* <Route path="/knowledge" element={<KnowledgePage />} /> */}
{/* <Route path="/apps" element={<AppsPage />} /> */}
{/* <Route path="/mcp-servers/*" element={<McpServersPage />} /> */}
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="/discover/*" element={<DiscoverPage />} />
</Routes>
</HashRouter>
</TopViewContainer>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -25,7 +25,6 @@
}
.minapp-drawer {
max-width: calc(100vw - var(--sidebar-width));
.ant-drawer-content-wrapper {
box-shadow: none;
}
@@ -33,7 +32,7 @@
position: absolute;
-webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px);
width: calc(100vw - var(--sidebar-width));
width: 100%;
margin-top: -0.5px;
border-bottom: none;
}

View File

@@ -29,7 +29,7 @@
--color-text-secondary: rgba(235, 235, 245, 0.7);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff19;
--color-border: #383838;
--color-border-soft: #ffffff10;
--color-border-mute: #ffffff05;
--color-error: #f44336;
@@ -44,8 +44,8 @@
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
--color-list-item: #222;
--color-list-item-hover: #1e1e1e;
--color-list-item: rgba(255, 255, 255, 0.1);
--color-list-item-hover: rgba(255, 255, 255, 0.05);
--modal-background: #1f1f1f;
@@ -56,7 +56,7 @@
--navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background: #1f1f1f;
--navbar-height: 40px;
--navbar-height: 42px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
@@ -71,7 +71,8 @@
--chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black);
--list-item-border-radius: 20px;
--list-item-border-radius: 8px;
--border-width: 0.5px;
}
[theme-mode='light'] {
@@ -120,8 +121,8 @@
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
--color-list-item: #eee;
--color-list-item-hover: #f5f5f5;
--color-list-item: rgba(255, 255, 255, 0.9);
--color-list-item-hover: rgba(255, 255, 255, 0.5);
--modal-background: var(--color-white);
@@ -136,4 +137,6 @@
--chat-background-user: #95ec69;
--chat-background-assistant: #ffffff;
--chat-text-user: var(--color-text);
--border-width: 0.5px;
}

View File

@@ -1,6 +1,14 @@
#content-container {
background-color: var(--color-background);
border-top: 0.5px solid var(--color-border);
border-top-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
}
.group-container {
.context-menu-container {
width: 100%;
}
}
.context-menu-container {
max-width: 100%;
}

View File

@@ -12,7 +12,7 @@
*::before,
*::after {
box-sizing: border-box;
margin: 0;
// margin: 0;
font-weight: normal;
}
@@ -129,29 +129,22 @@ ul {
.message-content-container {
margin: 5px 0;
border-radius: 8px;
padding: 0.5rem 1rem;
padding: 10px 15px 0 15px;
}
.block-wrapper {
display: flow-root;
}
.message-content-container > *:last-child {
margin-bottom: 0;
}
.message-thought-container {
margin-top: 8px;
}
.message-user {
color: var(--chat-text-user);
.message-content-container-user .anticon {
.markdown,
.anticon,
.iconfont,
.lucide,
.message-tokens {
color: var(--chat-text-user) !important;
}
.markdown {
color: var(--chat-text-user);
.message-action-button:hover {
background-color: var(--color-white-soft);
}
}
.group-grid-container.horizontal,
@@ -172,12 +165,6 @@ ul {
code {
color: var(--color-text);
}
.markdown {
display: flow-root;
*:last-child {
margin-bottom: 0;
}
}
}
.lucide {

View File

@@ -0,0 +1,146 @@
@import 'tailwindcss' source('../../../src');
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
/* 如需自定义:
1. 清晰地组织自定义 CSS 到相应的层中。
2. 基础样式(如全局重置、链接样式)放入 base 层;
3. 可复用的组件样式(如果仍使用 @apply 或原生 CSS 嵌套创建)放入 components 层;
4. 新的自定义工具类放入 utilities 层。
*/
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-marquee: marquee var(--duration) infinite linear;
--animate-marquee-vertical: marquee-vertical var(--duration) linear infinite;
@keyframes marquee {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(-100% - var(--gap)));
}
}
@keyframes marquee-vertical {
from {
transform: translateY(0);
}
to {
transform: translateY(calc(-100% - var(--gap)));
}
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -26,7 +26,6 @@ interface Props {
onSave?: (newContent: string) => void
onChange?: (newContent: string) => void
setTools?: (value: React.SetStateAction<CodeTool[]>) => void
height?: string
minHeight?: string
maxHeight?: string
/** 用于覆写编辑器的某些设置 */
@@ -55,7 +54,6 @@ const CodeEditor = ({
onSave,
onChange,
setTools,
height,
minHeight,
maxHeight,
options,
@@ -195,7 +193,6 @@ const CodeEditor = ({
value={initialContent.current}
placeholder={placeholder}
width="100%"
height={height}
minHeight={minHeight}
maxHeight={collapsible && !isExpanded ? (maxHeight ?? '350px') : 'none'}
editable={true}

View File

@@ -6,10 +6,9 @@ import styled from 'styled-components'
interface ContextMenuProps {
children: React.ReactNode
onContextMenu?: (e: React.MouseEvent) => void
style?: React.CSSProperties
}
const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu, style }) => {
const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu }) => {
const { t } = useTranslation()
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>(null)
const [selectedText, setSelectedText] = useState<string>('')
@@ -67,7 +66,7 @@ const ContextMenu: React.FC<ContextMenuProps> = ({ children, onContextMenu, styl
]
return (
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container" style={style}>
<ContextContainer onContextMenu={handleContextMenu} className="context-menu-container">
{contextMenuPosition && (
<Dropdown
overlayStyle={{ position: 'fixed', left: contextMenuPosition.x, top: contextMenuPosition.y, zIndex: 1000 }}

View File

@@ -395,10 +395,7 @@ const MinappPopupContainer: React.FC = () => {
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{
marginLeft: 'var(--sidebar-width)',
backgroundColor: window.root.style.background
}}>
style={{ backgroundColor: window.root.style.background }}>
{!isReady && (
<EmptyView>
<Avatar
@@ -418,7 +415,7 @@ const TitleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${isMac ? '20px' : '10px'};
padding-left: ${isMac ? '80px' : '10px'};
padding-right: 10px;
position: absolute;
top: 0;

View File

@@ -78,7 +78,7 @@ const WebviewContainer = memo(
)
const WebviewStyle: React.CSSProperties = {
width: 'calc(100vw - var(--sidebar-width))',
width: '100vw',
height: 'calc(100vh - var(--navbar-height))',
backgroundColor: 'var(--color-background)',
display: 'inline-flex'

View File

@@ -0,0 +1,90 @@
import { isMac } from '@renderer/config/constant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { MessageSquareDiff, Search } from 'lucide-react'
import { FC } from 'react'
import styled from 'styled-components'
import SearchPopup from '../Popups/SearchPopup'
interface Props {}
const HeaderNavbar: FC<Props> = () => {
return (
<Container>
<div>
{!isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
</div>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</Container>
)
}
const Container = styled.div`
display: flex;
width: var(--assistant-width);
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0;
padding-left: var(--sidebar-width);
height: var(--navbar-height);
min-height: var(--navbar-height);
background-color: transparent;
-webkit-app-region: drag;
padding-left: ${isMac ? '75px' : '0'};
`
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
padding: 0 7px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
-webkit-app-region: no-drag;
cursor: pointer;
.iconfont {
font-size: 18px;
color: var(--color-icon);
&.icon-a-addchat {
font-size: 20px;
}
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
color: var(--color-icon-white);
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default HeaderNavbar

View File

@@ -0,0 +1,379 @@
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
import { UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar'
import { useChat } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import { Avatar, Tooltip } from 'antd'
import {
Blocks,
Bot,
ChevronDown,
ChevronRight,
Compass,
FileSearch,
Folder,
Languages,
MessageSquare,
Moon,
Palette,
SquareTerminal,
Sun,
SunMoon
} from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Tabs from '../../pages/home/Tabs'
import MainNavbar from './MainNavbar'
type Tab = 'assistants' | 'topic' | 'settings'
const MainSidebar: FC = () => {
const { assistants } = useAssistants()
const navigate = useNavigate()
const [tab, setTab] = useState<Tab>('assistants')
const avatar = useAvatar()
const { userName, defaultPaintingProvider } = useSettings()
const { t } = useTranslation()
const { theme, settedTheme, toggleTheme } = useTheme()
const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false)
const location = useLocation()
const { pathname } = location
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
const { showAssistants, showTopics, topicPosition } = useSettings()
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, () => setTab('topic'))
return () => unsubscribe()
}, [])
useEffect(() => {
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
const newAssistant = assistants.find((a) => a.id === assistantId)
if (newAssistant) {
setActiveAssistant(newAssistant)
}
})
return () => {
unsubscribe()
}
}, [assistants, setActiveAssistant])
useEffect(() => {
const canMinimize = !showAssistants && !showTopics
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
return () => {
window.api.window.resetMinimumSize()
}
}, [showAssistants, showTopics, topicPosition])
useEffect(() => {
setIsAppMenuExpanded(false)
}, [activeAssistant.id, activeTopic.id])
const onAvatarClick = () => {
navigate('/settings/provider')
}
const onChageTheme = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
toggleTheme()
}
const appMenuItems = [
// { icon: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
{ icon: <Compass size={18} className="icon" />, text: t('discover.title'), path: '/discover' },
{ icon: <Languages size={18} className="icon" />, text: t('translate.title'), path: '/translate' },
{
icon: <Palette size={18} className="icon" />,
text: t('paintings.title'),
path: `/paintings/${defaultPaintingProvider}`
},
// { icon: <LayoutGrid size={18} className="icon" />, text: t('minapp.title'), path: '/apps' },
{ icon: <FileSearch size={18} className="icon" />, text: t('knowledge.title'), path: '/knowledge' },
{ icon: <SquareTerminal size={18} className="icon" />, text: t('common.mcp'), path: '/mcp-servers' },
{ icon: <Folder size={18} className="icon" />, text: t('files.title'), path: '/files' }
]
const isRoutes = (path: string): boolean => pathname.startsWith(path)
const onChageTab = (tab: Tab) => {
setTab(tab)
setIsAppMenuExpanded(false)
}
if (!showAssistants) {
return null
}
if (location.pathname !== '/') {
return null
}
return (
<Container id="main-sidebar">
<MainNavbar />
<MainMenu>
<MainMenuItem
active={tab === 'assistants' && location.pathname === '/'}
onClick={() => onChageTab('assistants')}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Bot size={18} />
</MainMenuItemIcon>
<MainMenuItemText>{t('assistants.title')}</MainMenuItemText>
</MainMenuItemLeft>
{tab === 'topic' && (
<MainMenuItemRight>
<MainMenuItemRightText>{activeAssistant.name}</MainMenuItemRightText>
</MainMenuItemRight>
)}
</MainMenuItem>
<MainMenuItem active={tab === 'topic' && location.pathname === '/'} onClick={() => onChageTab('topic')}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<MessageSquare size={18} />
</MainMenuItemIcon>
<MainMenuItemText>{t('common.topics')}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
<MainMenuItem
style={{ opacity: isAppMenuExpanded ? 0.5 : 1 }}
onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
<MainMenuItemLeft>
<MainMenuItemIcon>
<Blocks size={19} className="icon" />
</MainMenuItemIcon>
<MainMenuItemText>{t('common.apps')}</MainMenuItemText>
</MainMenuItemLeft>
<MainMenuItemRight>
{isAppMenuExpanded ? (
<ChevronDown size={18} color="var(--color-text-3)" />
) : (
<ChevronRight size={18} color="var(--color-text-3)" />
)}
</MainMenuItemRight>
</MainMenuItem>
{isAppMenuExpanded && (
<SubMenu>
{appMenuItems.map((item) => (
<MainMenuItem key={item.path} active={isRoutes(item.path)} onClick={() => navigate(item.path)}>
<MainMenuItemLeft>
<MainMenuItemIcon>{item.icon}</MainMenuItemIcon>
<MainMenuItemText>{item.text}</MainMenuItemText>
</MainMenuItemLeft>
</MainMenuItem>
))}
</SubMenu>
)}
</MainMenu>
<Tabs
tab={tab}
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
/>
<UserMenu onClick={onAvatarClick}>
<UserMenuLeft>
{isEmoji(avatar) ? (
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
</EmojiAvatar>
) : (
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
)}
<UserMenuText>{userName}</UserMenuText>
</UserMenuLeft>
<Tooltip
title={t('settings.theme.title') + ': ' + t(`settings.theme.${settedTheme}`)}
mouseEnterDelay={0.8}
placement="right">
<Icon theme={theme} onClick={onChageTheme}>
{settedTheme === ThemeMode.dark ? (
<Moon size={18} className="icon" />
) : settedTheme === ThemeMode.light ? (
<Sun size={18} className="icon" />
) : (
<SunMoon size={18} className="icon" />
)}
</Icon>
</Tooltip>
</UserMenu>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
width: var(--assistant-width);
max-width: var(--assistant-width);
border-right: 0.5px solid var(--color-border);
`
const MainMenu = styled.div`
display: flex;
flex-direction: column;
padding: 10px;
padding-top: 0;
gap: 8px;
`
const MainMenuItem = styled.div<{ active?: boolean }>`
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'transparent')};
padding: 5px 10px;
border-radius: 5px;
border-radius: 8px;
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')};
}
`
const MainMenuItemLeft = styled.div`
display: flex;
align-items: center;
gap: 5px;
`
const MainMenuItemRight = styled.div`
display: flex;
align-items: center;
gap: 5px;
`
const MainMenuItemRightText = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MainMenuItemIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
`
const MainMenuItemText = styled.div`
font-size: 14px;
font-weight: 500;
`
const UserMenu = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin: 10px;
margin-bottom: 10px;
padding: 4px 8px;
gap: 5px;
border-radius: 8px;
&:hover {
background-color: var(--color-list-item);
}
`
const UserMenuLeft = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
`
const AvatarImg = styled(Avatar)`
width: 28px;
height: 28px;
background-color: var(--color-background-soft);
border: none;
cursor: pointer;
`
const UserMenuText = styled.div`
font-size: 14px;
font-weight: 500;
`
const Icon = styled.div<{ theme: string }>`
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
box-sizing: border-box;
-webkit-app-region: none;
border: 0.5px solid transparent;
&:hover {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
opacity: 0.8;
cursor: pointer;
.icon {
color: var(--color-icon-white);
}
}
&.active {
background-color: ${({ theme }) => (theme === 'dark' ? 'var(--color-black)' : 'var(--color-white)')};
border: 0.5px solid var(--color-border);
.icon {
color: var(--color-primary);
}
}
@keyframes borderBreath {
0% {
opacity: 0.1;
}
50% {
opacity: 1;
}
100% {
opacity: 0.1;
}
}
&.opened-minapp {
position: relative;
}
&.opened-minapp::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border-radius: inherit;
opacity: 0.3;
border: 0.5px solid var(--color-primary);
}
`
const SubMenu = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
`
export default MainSidebar

View File

@@ -1,24 +1,16 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { Button } from 'antd'
import { ChevronDown, X } from 'lucide-react'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => {
const backgroundColor = useNavBackgroundColor()
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
}
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
return <NavbarContainer {...props}>{children}</NavbarContainer>
}
export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
@@ -36,41 +28,54 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
const isFullscreen = useFullscreen()
return (
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
<CloseIconWindows />
{children}
<CloseIconMac />
</NavbarMainContainer>
)
}
const CloseIconMac = () => {
const navigate = useNavigate()
if (!isMac) {
return null
}
return <Button type="text" icon={<X size={18} />} onClick={() => navigate('/')} className="nodrag" />
}
const CloseIconWindows = () => {
const navigate = useNavigate()
if (isMac) {
return null
}
return (
<Button
size="small"
type="default"
shape="circle"
icon={<ChevronDown size={16} />}
onClick={() => navigate('/')}
className="nodrag"
style={{ marginRight: 5 }}
/>
)
}
const NavbarContainer = styled.div`
min-width: 100%;
display: flex;
flex-direction: row;
min-height: var(--navbar-height);
max-height: var(--navbar-height);
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
-webkit-app-region: drag;
`
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
padding: 0 10px;
display: flex;
flex-direction: row;
align-items: center;
font-weight: bold;
color: var(--color-text-1);
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 ${isMac ? '20px' : 0};
font-weight: bold;
color: var(--color-text-1);
background-color: var(--color-background);
`
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
@@ -87,9 +92,26 @@ const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
display: flex;
flex-direction: row;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
justify-content: space-between;
padding: 0 ${isMac ? '20px' : 0};
padding-left: ${isMac ? '70px' : '10px'};
font-weight: bold;
color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
-webkit-app-region: drag;
`
const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
padding: 0 8px;
font-weight: bold;
justify-content: space-between;
color: var(--color-text-1);
`

View File

@@ -16,6 +16,7 @@ import type { MenuProps } from 'antd'
import { Avatar, Dropdown, Tooltip } from 'antd'
import {
CircleHelp,
Compass,
FileSearch,
Folder,
Languages,
@@ -155,7 +156,8 @@ const MainMenus: FC = () => {
translate: <Languages size={18} className="icon" />,
minapp: <LayoutGrid size={18} className="icon" />,
knowledge: <FileSearch size={18} className="icon" />,
files: <Folder size={17} className="icon" />
files: <Folder size={17} className="icon" />,
discover: <Compass size={18} className="icon" />
}
const pathMap = {
@@ -165,7 +167,8 @@ const MainMenus: FC = () => {
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
files: '/files',
discover: '/discover'
}
return sidebarIcons.visible.map((icon) => {

View File

@@ -140,8 +140,6 @@ import XirangModelLogo from '@renderer/assets/images/models/xirang.png'
import XirangModelLogoDark from '@renderer/assets/images/models/xirang_dark.png'
import YiModelLogo from '@renderer/assets/images/models/yi.png'
import YiModelLogoDark from '@renderer/assets/images/models/yi_dark.png'
import YoudaoLogo from '@renderer/assets/images/providers/netease-youdao.svg'
import NomicLogo from '@renderer/assets/images/providers/nomic.png'
import { getProviderByModel } from '@renderer/services/AssistantService'
import { Assistant, Model } from '@renderer/types'
import OpenAI from 'openai'
@@ -299,7 +297,7 @@ export function getModelLogo(modelId: string) {
'davinci-': isLight ? ChatGptModelLogo : ChatGptModelLogoDakr,
glm: isLight ? ChatGLMModelLogo : ChatGLMModelLogoDark,
deepseek: isLight ? DeepSeekModelLogo : DeepSeekModelLogoDark,
'(qwen|qwq|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark,
'(qwen|qwq-|qvq-)': isLight ? QwenModelLogo : QwenModelLogoDark,
gemma: isLight ? GemmaModelLogo : GemmaModelLogoDark,
'yi-': isLight ? YiModelLogo : YiModelLogoDark,
llama: isLight ? LlamaModelLogo : LlamaModelLogoDark,
@@ -378,14 +376,12 @@ export function getModelLogo(modelId: string) {
'google/': isLight ? GoogleModelLogo : GoogleModelLogoDark,
xirang: isLight ? XirangModelLogo : XirangModelLogoDark,
hugging: isLight ? HuggingfaceModelLogo : HuggingfaceModelLogoDark,
youdao: YoudaoLogo,
embedding: isLight ? EmbeddingModelLogo : EmbeddingModelLogoDark,
perplexity: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
sonar: isLight ? PerplexityModelLogo : PerplexityModelLogoDark,
'bge-': BgeModelLogo,
'voyage-': VoyageModelLogo,
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark,
'nomic-': NomicLogo
tokenflux: isLight ? TokenFluxModelLogo : TokenFluxModelLogoDark
}
for (const key in logoMap) {

View File

@@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
`
export const SUMMARIZE_PROMPT =
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks or other special symbols"
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks, markdown language markers, or other special symbols"
// https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
export const SEARCH_SUMMARY_PROMPT = `

View File

@@ -38,8 +38,22 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
setSettedTheme(nextTheme || ThemeMode.system)
}
const tailwindThemeChange = (theme) => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
root.classList.add(theme)
}
useEffect(() => {
window.api?.setTheme(settedTheme || actualTheme)
}, [settedTheme, actualTheme])
useEffect(() => {
document.body.setAttribute('theme-mode', settedTheme)
tailwindThemeChange(settedTheme)
}, [settedTheme])
useEffect(() => {
// Set initial theme and OS attributes on body
document.body.setAttribute('os', isMac ? 'mac' : 'windows')
document.body.setAttribute('theme-mode', actualTheme)

View File

@@ -1,3 +1,4 @@
import './assets/styles/tailwind.css'
import './assets/styles/index.scss'
import '@ant-design/v5-patch-for-react-19'

View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View File

@@ -0,0 +1,47 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setActiveAssistant, setActiveTopic } from '@renderer/store/runtime'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant } from '@renderer/types'
import { Topic } from '@renderer/types'
import { useEffect } from 'react'
import { useAssistants } from './useAssistant'
import { useSettings } from './useSettings'
export const useChat = () => {
const { assistants } = useAssistants()
const activeAssistant = useAppSelector((state) => state.runtime.chat.activeAssistant) || assistants[0]
const activeTopic = useAppSelector((state) => state.runtime.chat.activeTopic) || activeAssistant?.topics[0]!
const { clickAssistantToShowTopic } = useSettings()
const dispatch = useAppDispatch()
useEffect(() => {
if (activeTopic) {
dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic, dispatch])
useEffect(() => {
const firstTopic = activeAssistant.topics[0]
firstTopic && dispatch(setActiveTopic(firstTopic))
}, [activeAssistant, dispatch])
useEffect(() => {
if (clickAssistantToShowTopic) {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
}, [clickAssistantToShowTopic, activeAssistant])
return {
activeAssistant,
activeTopic,
setActiveAssistant: (assistant: Assistant) => {
dispatch(setActiveAssistant(assistant))
},
setActiveTopic: (topic: Topic) => {
dispatch(setActiveTopic(topic))
}
}
}

View File

@@ -1,5 +1,4 @@
import { createSelector } from '@reduxjs/toolkit'
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setTagsOrder, updateAssistants } from '@renderer/store/assistants'
import { flatMap, groupBy, uniq } from 'lodash'
import { useCallback, useMemo } from 'react'
@@ -7,19 +6,15 @@ import { useTranslation } from 'react-i18next'
import { useAssistants } from './useAssistant'
// 基础选择器
const selectAssistantsState = (state: RootState) => state.assistants
// 记忆化 tagsOrder 选择器(自动处理默认值)--- 这是一个选择器,用于从 store 中获取 tagsOrder 的值。因为之前的tagsOrder是后面新加的不这样做会报错所以这里需要处理一下默认值
const selectTagsOrder = createSelector([selectAssistantsState], (assistants) => assistants.tagsOrder ?? [])
// 定义useTags的返回类型包含所有标签和获取特定标签的助手函数
// 为了不增加新的概念,标签直接作为助手的属性,所以这里的标签是指助手的标签属性
// 但是为了方便管理,增加了一个获取特定标签的助手函数
export const useTags = () => {
const { assistants } = useAssistants()
const { t } = useTranslation()
const dispatch = useAppDispatch()
const savedTagsOrder = useAppSelector(selectTagsOrder)
const savedTagsOrder = useAppSelector((state) => state.assistants.tagsOrder || [])
// 计算所有标签
const allTags = useMemo(() => {

View File

@@ -1,47 +1,16 @@
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { deleteMessageFiles } from '@renderer/services/MessagesService'
import store from '@renderer/store'
import { updateTopic } from '@renderer/store/assistants'
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
import { Assistant, Topic } from '@renderer/types'
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
import { find, isEmpty } from 'lodash'
import { useEffect, useState } from 'react'
import { isEmpty } from 'lodash'
import { useAssistant } from './useAssistant'
import { getStoreSetting } from './useSettings'
const renamingTopics = new Set<string>()
let _activeTopic: Topic
let _setActiveTopic: (topic: Topic) => void
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
const { assistant } = useAssistant(_assistant.id)
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
_activeTopic = activeTopic
_setActiveTopic = setActiveTopic
useEffect(() => {
if (activeTopic) {
store.dispatch(loadTopicMessagesThunk(activeTopic.id))
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
}
}, [activeTopic])
useEffect(() => {
// activeTopic not in assistant.topics
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
setActiveTopic(assistant.topics[0])
}
}, [activeTopic?.id, assistant])
return { activeTopic, setActiveTopic }
}
export function useTopic(assistant: Assistant, topicId?: string) {
return assistant?.topics.find((topic) => topic.id === topicId)
}
@@ -86,7 +55,6 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
.substring(0, 50)
if (topicName) {
const data = { ...topic, name: topicName } as Topic
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
}
return
@@ -97,8 +65,9 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
if (summaryText) {
const data = { ...topic, name: summaryText }
_setActiveTopic(data)
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
} else {
window.message?.error(i18n.t('message.error.fetchTopicName'))
}
}
} finally {

View File

@@ -261,7 +261,6 @@
"topics.clear.title": "Clear Messages",
"topics.copy.image": "Copy as image",
"topics.copy.md": "Copy as markdown",
"topics.copy.plain_text": "Copy as plain text (remove Markdown)",
"topics.copy.title": "Copy",
"topics.delete.shortcut": "Hold {{key}} to delete directly",
"topics.edit.placeholder": "Enter new name",
@@ -422,7 +421,9 @@
"pinyin.asc": "Sort by Pinyin (A-Z)",
"pinyin.desc": "Sort by Pinyin (Z-A)"
},
"no_results": "No results"
"no_results": "No results",
"apps": "Apps",
"mcp": "Tools"
},
"docs": {
"title": "Docs"
@@ -566,12 +567,8 @@
"urls": "URLs",
"dimensions": "Embedding dimension",
"dimensions_size_tooltip": "The size of the embedding dimension; the larger the value, the larger the embedding dimension, but it also consumes more tokens.",
"dimensions_size_placeholder": " Embedding dimension size, e.g. 1024",
"dimensions_auto_set": "Auto-set embedding dimensions",
"dimensions_error_invalid": "Please enter embedding dimension size",
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}}).",
"dimensions_set_right": "⚠️ Please ensure the model supports the set embedding dimension size",
"dimensions_default": "The model will use default embedding dimensions"
"dimensions_size_placeholder": "Default value (modification not recommended)",
"dimensions_size_too_large": "The embedding dimension cannot exceed the model's context limit ({{max_context}})."
},
"languages": {
"arabic": "Arabic",
@@ -1517,7 +1514,6 @@
"messages.prompt": "Show prompt",
"messages.tokens": "Show token usage",
"messages.divider": "Show divider between messages",
"messages.divider.tooltip": "Not applicable to bubble-style message",
"messages.grid_columns": "Message grid display columns",
"messages.grid_popover_trigger": "Grid detail trigger",
"messages.grid_popover_trigger.click": "Click to display",
@@ -1550,7 +1546,6 @@
"models.add.model_id.select.placeholder": "Select Model",
"models.add.model_id.tooltip": "Example: gpt-3.5-turbo",
"models.add.model_name": "Model Name",
"models.add.model_name.tooltip": "Optional e.g. GPT-4",
"models.add.model_name.placeholder": "Optional e.g. GPT-4",
"models.check.all": "All",
"models.check.all_models_passed": "All models check passed",
@@ -1702,8 +1697,6 @@
"exit_fullscreen": "Exit Fullscreen",
"key": "Key",
"mini_window": "Quick Assistant",
"selection_assistant_toggle": "Toggle Selection Assistant",
"selection_assistant_select_text": "Selection Assistant: Select Text",
"new_topic": "New Topic",
"press_shortcut": "Press Shortcut",
"reset_defaults": "Reset Defaults",
@@ -1829,7 +1822,7 @@
"close": "Close",
"closed": "Translation closed",
"copied": "Translation content copied",
"detected.language": "Auto Detect",
"detected.language": "Detected Language",
"empty": "Translation content is empty",
"not.found": "Translation content not found",
"confirm": {
@@ -1920,15 +1913,10 @@
"title": "Toolbar",
"trigger_mode": {
"title": "Trigger Mode",
"description": "The way to trigger the selection assistant and show the toolbar",
"description_note": "Some applications do not support selecting text with the Ctrl key. If you have remapped the Ctrl key using tools like AHK, it may cause some applications to fail to select text.",
"description": "Show toolbar immediately when text is selected, or show only when Ctrl key is held after selection.",
"description_note": "The Ctrl key may not work in some apps. If you use AHK or other tools to remap the Ctrl key, it may not work.",
"selected": "Selection",
"selected_note": "Show toolbar immediately when text is selected",
"ctrlkey": "Ctrl Key",
"ctrlkey_note": "After selection, hold down the Ctrl key to show the toolbar",
"shortcut": "Shortcut",
"shortcut_note": "After selection, use shortcut to show the toolbar. Please set the shortcut in the shortcut settings page and enable it. ",
"shortcut_link": "Go to Shortcut Settings"
"ctrlkey": "Ctrl Key"
},
"compact_mode": {
"title": "Compact Mode",

View File

@@ -261,7 +261,6 @@
"topics.clear.title": "メッセージをクリア",
"topics.copy.image": "画像としてコピー",
"topics.copy.md": "Markdownとしてコピー",
"topics.copy.plain_text": "プレーンテキストとしてコピーMarkdownを除去",
"topics.copy.title": "コピー",
"topics.delete.shortcut": "{{key}}キーを押しながらで直接削除",
"topics.edit.placeholder": "新しい名前を入力",
@@ -422,7 +421,9 @@
"pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート"
},
"no_results": "検索結果なし"
"no_results": "検索結果なし",
"apps": "アプリ",
"mcp": "ツール"
},
"docs": {
"title": "ドキュメント"
@@ -566,12 +567,8 @@
"urls": "URL",
"dimensions": "埋め込み次元",
"dimensions_size_tooltip": "埋め込み次元のサイズは、数値が大きいほど埋め込み次元も大きくなりますが、消費するトークンも増えます。",
"dimensions_size_placeholder": " 埋め込み次元のサイズ1024",
"dimensions_auto_set": "埋め込み次元を自動設定",
"dimensions_error_invalid": "埋め込み次元のサイズを入力してください",
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。",
"dimensions_set_right": "⚠️ モデルが設定した埋め込み次元のサイズをサポートしていることを確認してください",
"dimensions_default": "モデルはデフォルトの埋め込み次元を使用します"
"dimensions_size_placeholder": "デフォルト値(変更はお勧めしません",
"dimensions_size_too_large": "埋め込み次元はモデルのコンテキスト制限({{max_context}})を超えてはなりません。"
},
"languages": {
"arabic": "アラビア語",
@@ -633,6 +630,7 @@
"error.enter.api.key": "APIキーを入力してください",
"error.enter.model": "モデルを選択してください",
"error.enter.name": "ナレッジベース名を入力してください",
"error.fetchTopicName": "トピックの命名に失敗しました",
"error.get_embedding_dimensions": "埋込み次元を取得できませんでした",
"error.invalid.api.host": "無効なAPIアドレスです",
"error.invalid.api.key": "無効なAPIキーです",
@@ -1510,7 +1508,6 @@
"messages.prompt": "プロンプト表示",
"messages.tokens": "トークン使用量を表示",
"messages.divider": "メッセージ間に区切り線を表示",
"messages.divider.tooltip": "バブルスタイルのメッセージには適用されません",
"messages.grid_columns": "メッセージグリッドの表示列数",
"messages.grid_popover_trigger": "グリッド詳細トリガー",
"messages.grid_popover_trigger.click": "クリックで表示",
@@ -1543,8 +1540,7 @@
"models.add.model_id.select.placeholder": "モデルを選択",
"models.add.model_id.tooltip": "例gpt-3.5-turbo",
"models.add.model_name": "モデル名",
"models.add.model_name.tooltip": "例GPT-4",
"models.add.model_name.placeholder": "例GPT-4",
"models.add.model_name.placeholder": "例GPT-3.5",
"models.check.all": "すべて",
"models.check.all_models_passed": "すべてのモデルチェックが成功しました",
"models.check.button_caption": "健康チェック",
@@ -1689,8 +1685,6 @@
"exit_fullscreen": "フルスクリーンを終了",
"key": "キー",
"mini_window": "クイックアシスタント",
"selection_assistant_toggle": "選択アシスタントを切り替え",
"selection_assistant_select_text": "選択アシスタント:テキストを選択",
"new_topic": "新しいトピック",
"press_shortcut": "ショートカットを押す",
"reset_defaults": "デフォルトのショートカットをリセット",
@@ -1861,7 +1855,7 @@
"menu": {
"description": "對當前輸入框內容進行翻譯"
},
"detected.language": "自動検出"
"detected.language": "検出された言語"
},
"tray": {
"quit": "終了",
@@ -1918,16 +1912,11 @@
"toolbar": {
"title": "ツールバー",
"trigger_mode": {
"title": "単語の取り出し方",
"description": "テキスト選択後、取詞ツールバーを表示する方法",
"description_note": "一部のアプリケーションでは、Ctrl キーでテキスト選択できません。AHK などのツールを使用して Ctrl キーをマップした場合、一部のアプリケーションでテキスト選択が失敗する可能性があります。",
"title": "表示方法",
"description": "テキスト選択時に即時表示、またはCtrlキー押下時のみ表示",
"description_note": "一部のアプリCtrlキーでテキスト選択に対応していません。AHKなどCtrlキーをマップすると、選択できなくなる場合があります。",
"selected": "選択時",
"selected_note": "テキスト選択時に即時表示",
"ctrlkey": "Ctrlキー",
"ctrlkey_note": "テキスト選択後、Ctrlキーを押下して表示",
"shortcut": "ショートカットキー",
"shortcut_note": "テキスト選択後、ショートカットキーを押下して表示。ショートカットキーを設定するには、ショートカット設定ページで有効にしてください。",
"shortcut_link": "ショートカット設定ページに移動"
"ctrlkey": "Ctrlキー"
},
"compact_mode": {
"title": "コンパクトモード",

View File

@@ -261,7 +261,6 @@
"topics.clear.title": "Очистить сообщения",
"topics.copy.image": "Скопировать как изображение",
"topics.copy.md": "Скопировать как Markdown",
"topics.copy.plain_text": "Копировать как обычный текст (удалить Markdown)",
"topics.copy.title": "Скопировать",
"topics.delete.shortcut": "Удерживайте {{key}} для мгновенного удаления",
"topics.edit.placeholder": "Введите новый заголовок",
@@ -422,7 +421,9 @@
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
},
"no_results": "Результатов не найдено"
"no_results": "Результатов не найдено",
"apps": "Приложения",
"mcp": "Инструменты"
},
"docs": {
"title": "Документация"
@@ -566,12 +567,8 @@
"urls": "URL-адреса",
"dimensions": "векторное пространство",
"dimensions_size_tooltip": "Размерность вложения, чем больше значение, тем больше размерность вложения, но и потребляемых токенов также становится больше.",
"dimensions_size_placeholder": " Размерность эмбеддинга, например 1024",
"dimensions_auto_set": "Автоматическая установка размерности эмбеддинга",
"dimensions_error_invalid": "Пожалуйста, введите размерность эмбеддинга",
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})",
"dimensions_set_right": "⚠️ Убедитесь, что модель поддерживает заданный размер эмбеддинга",
"dimensions_default": "Модель будет использовать размер эмбеддинга по умолчанию"
"dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)",
"dimensions_size_too_large": "Размерность вложения не может превышать ограничение контекста модели ({{max_context}})"
},
"languages": {
"arabic": "Арабский",
@@ -633,6 +630,7 @@
"error.enter.api.key": "Пожалуйста, введите ваш API ключ",
"error.enter.model": "Пожалуйста, выберите модель",
"error.enter.name": "Пожалуйста, введите название базы знаний",
"error.fetchTopicName": "Не удалось назвать тему",
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания",
"error.invalid.api.host": "Неверный API адрес",
"error.invalid.api.key": "Неверный API ключ",
@@ -1510,7 +1508,6 @@
"messages.prompt": "Показывать подсказки",
"messages.tokens": "Показать использование токенов",
"messages.divider": "Показывать разделитель между сообщениями",
"messages.divider.tooltip": "Не применимо к сообщениям в стиле пузырей",
"messages.grid_columns": "Количество столбцов сетки сообщений",
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
"messages.grid_popover_trigger.click": "Нажатие для отображения",
@@ -1543,7 +1540,6 @@
"models.add.model_id.select.placeholder": "Выберите модель",
"models.add.model_id.tooltip": "Пример: gpt-3.5-turbo",
"models.add.model_name": "Имя модели",
"models.add.model_name.tooltip": "Необязательно, например, GPT-4",
"models.add.model_name.placeholder": "Необязательно, например, GPT-4",
"models.check.all": "Все",
"models.check.all_models_passed": "Все модели прошли проверку",
@@ -1689,8 +1685,6 @@
"exit_fullscreen": "Выйти из полноэкранного режима",
"key": "Клавиша",
"mini_window": "Быстрый помощник",
"selection_assistant_toggle": "Переключить помощник выделения",
"selection_assistant_select_text": "Помощник выделения: выделить текст",
"new_topic": "Новый топик",
"press_shortcut": "Нажмите сочетание клавиш",
"reset_defaults": "Сбросить настройки по умолчанию",
@@ -1861,7 +1855,7 @@
"menu": {
"description": "Перевести содержимое текущего ввода"
},
"detected.language": "Автоматическое обнаружение"
"detected.language": "Обнаруженный язык"
},
"tray": {
"quit": "Выйти",
@@ -1919,15 +1913,10 @@
"title": "Панель инструментов",
"trigger_mode": {
"title": "Режим активации",
"description": "Показывать панель сразу при выделении, или только при удержании Ctrl, или только при нажатии на сочетание клавиш",
"description": "Показывать панель сразу при выделении или только при удержании Ctrl.",
"description_note": "В некоторых приложениях Ctrl может не работать. Если вы используете AHK или другие инструменты для переназначения Ctrl, это может привести к тому, что некоторые приложения не смогут выделить текст.",
"selected": "При выделении",
"selected_note": "После выделения",
"ctrlkey": "По Ctrl",
"ctrlkey_note": "После выделения, удерживайте Ctrl для показа панели. Пожалуйста, установите Ctrl в настройках клавиатуры и активируйте его.",
"shortcut": "По сочетанию клавиш",
"shortcut_note": "После выделения, используйте сочетание клавиш для показа панели. Пожалуйста, установите сочетание клавиш в настройках клавиатуры и активируйте его.",
"shortcut_link": "Перейти к настройкам клавиатуры"
"ctrlkey": "По Ctrl"
},
"compact_mode": {
"title": "Компактный режим",

View File

@@ -279,7 +279,6 @@
"topics.clear.title": "清空消息",
"topics.copy.image": "复制为图片",
"topics.copy.md": "复制为 Markdown",
"topics.copy.plain_text": "复制为纯文本(去除 Markdown",
"topics.copy.title": "复制",
"topics.delete.shortcut": "按住 {{key}} 可直接删除",
"topics.edit.placeholder": "输入新名称",
@@ -422,7 +421,9 @@
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
},
"no_results": "无结果"
"no_results": "无结果",
"apps": "应用",
"mcp": "工具"
},
"docs": {
"title": "帮助文档"
@@ -519,11 +520,7 @@
"delete_confirm": "确定要删除此知识库吗?",
"dimensions": "嵌入维度",
"dimensions_size_tooltip": "嵌入维度大小,数值越大,嵌入维度越大,但消耗的 Token 也越多",
"dimensions_set_right": "⚠️ 请确保模型支持所设置的嵌入维度大小",
"dimensions_default": "模型将使用默认嵌入维度",
"dimensions_size_placeholder": " 嵌入维度大小,如 1024",
"dimensions_auto_set": "自动设置嵌入维度",
"dimensions_error_invalid": "请输入嵌入维度大小",
"dimensions_size_placeholder": " 默认值(不建议修改)",
"dimensions_size_too_large": "嵌入维度不能超过模型上下文限制({{max_context}}",
"directories": "目录",
"directory_placeholder": "请输入目录路径",
@@ -1517,7 +1514,6 @@
"messages.prompt": "显示提示词",
"messages.tokens": "显示Token用量",
"messages.divider": "消息分割线",
"messages.divider.tooltip": "不适用于气泡样式消息",
"messages.grid_columns": "消息网格展示列数",
"messages.grid_popover_trigger": "网格详情触发",
"messages.grid_popover_trigger.click": "点击显示",
@@ -1550,8 +1546,7 @@
"models.add.model_id.select.placeholder": "选择模型",
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
"models.add.model_name": "模型名称",
"models.add.model_name.placeholder": "例如 GPT-4",
"models.add.model_name.tooltip": "例如 GPT-4",
"models.add.model_name.placeholder": "例如 GPT-3.5",
"models.check.all": "所有",
"models.check.all_models_passed": "所有模型检测通过",
"models.check.button_caption": "健康检测",
@@ -1702,8 +1697,6 @@
"exit_fullscreen": "退出全屏",
"key": "按键",
"mini_window": "快捷助手",
"selection_assistant_toggle": "开关划词助手",
"selection_assistant_select_text": "划词助手:取词",
"new_topic": "新建话题",
"press_shortcut": "按下快捷键",
"reset_defaults": "重置默认快捷键",
@@ -1821,6 +1814,13 @@
"service_tier.flex": "灵活"
}
},
"discover": {
"title": "发现",
"install": "安装",
"uninstall": "卸载",
"update": "更新",
"update_all": "全部更新"
},
"translate": {
"any.language": "任意语言",
"target_language": "目标语言",
@@ -1862,7 +1862,7 @@
},
"title": "翻译",
"tooltip.newline": "换行",
"detected.language": "自动检测"
"detected.language": "检测到的语言"
},
"tray": {
"quit": "退出",
@@ -1919,16 +1919,11 @@
"toolbar": {
"title": "工具栏",
"trigger_mode": {
"title": "取词方式",
"description": "划词后,触发取词并显示工具栏的方式",
"title": "触发方式",
"description": "划词立即显示工具栏,或者划词后按住 Ctrl 键才显示工具栏",
"description_note": "少数应用不支持通过 Ctrl 键划词。若使用了AHK等工具对 Ctrl 键进行了重映射,可能导致部分应用无法划词。",
"selected": "划词",
"selected_note": "划词后立即显示工具栏",
"ctrlkey": "Ctrl 键",
"ctrlkey_note": "划词后,再 按住 Ctrl键才显示工具栏",
"shortcut": "快捷键",
"shortcut_note": "划词后,使用快捷键显示工具栏。请在快捷键设置页面中设置取词快捷键并启用。",
"shortcut_link": "前往快捷键设置"
"ctrlkey": "Ctrl 键"
},
"compact_mode": {
"title": "紧凑模式",

View File

@@ -261,7 +261,6 @@
"topics.clear.title": "清空訊息",
"topics.copy.image": "複製為圖片",
"topics.copy.md": "複製為 Markdown",
"topics.copy.plain_text": "複製為純文字(移除 Markdown",
"topics.copy.title": "複製",
"topics.delete.shortcut": "按住 {{key}} 可直接刪除",
"topics.edit.placeholder": "輸入新名稱",
@@ -422,7 +421,9 @@
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
},
"no_results": "沒有結果"
"no_results": "沒有結果",
"apps": "應用",
"mcp": "工具"
},
"docs": {
"title": "說明文件"
@@ -566,12 +567,8 @@
"urls": "網址",
"dimensions": "嵌入維度",
"dimensions_size_tooltip": "嵌入維度大小,數值越大,嵌入維度越大,但消耗的 Token 也越多",
"dimensions_size_placeholder": " 嵌入維度大小,例如 1024",
"dimensions_auto_set": "自動設定嵌入維度",
"dimensions_error_invalid": "請輸入嵌入維度大小",
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}}",
"dimensions_set_right": "⚠️ 請確保模型支援所設置的嵌入維度大小",
"dimensions_default": "模型將使用預設嵌入維度"
"dimensions_size_placeholder": "預設值(不建議修改)",
"dimensions_size_too_large": "嵌入維度不能超過模型上下文限制({{max_context}}"
},
"languages": {
"arabic": "阿拉伯文",
@@ -1514,7 +1511,6 @@
"messages.prompt": "提示詞顯示",
"messages.tokens": "Token用量顯示",
"messages.divider": "訊息間顯示分隔線",
"messages.divider.tooltip": "不適用於氣泡樣式消息",
"messages.grid_columns": "訊息網格展示列數",
"messages.grid_popover_trigger": "網格詳細資訊觸發",
"messages.grid_popover_trigger.click": "點選顯示",
@@ -1548,7 +1544,6 @@
"models.add.model_id.tooltip": "例如 gpt-3.5-turbo",
"models.add.model_name": "模型名稱",
"models.add.model_name.placeholder": "選填,例如 GPT-4",
"models.add.model_name.tooltip": "例如 GPT-4",
"models.check.all": "所有",
"models.check.all_models_passed": "所有模型檢查通過",
"models.check.button_caption": "健康檢查",
@@ -1692,8 +1687,6 @@
"copy_last_message": "複製上一則訊息",
"key": "按鍵",
"mini_window": "快捷助手",
"selection_assistant_toggle": "開關劃詞助手",
"selection_assistant_select_text": "劃詞助手:取词",
"new_topic": "新增話題",
"press_shortcut": "按下快捷鍵",
"reset_defaults": "重設預設快捷鍵",
@@ -1862,7 +1855,7 @@
"menu": {
"description": "對當前輸入框內容進行翻譯"
},
"detected.language": "自動檢測"
"detected.language": "檢測到的語言"
},
"tray": {
"quit": "結束",
@@ -1919,16 +1912,11 @@
"toolbar": {
"title": "工具列",
"trigger_mode": {
"title": "取詞方式",
"description": "劃詞後,觸發取詞並顯示工具列的方式",
"title": "觸發方式",
"description": "劃詞立即顯示工具列,或者劃詞後按住 Ctrl 鍵才顯示工具列",
"description_note": "在某些應用中可能無法透過 Ctrl 鍵劃詞。若使用了AHK等工具對Ctrl鍵進行了重新對應可能導致部分應用程式無法劃詞。",
"selected": "劃詞",
"selected_note": "劃詞後,立即顯示工具列",
"ctrlkey": "Ctrl 鍵",
"ctrlkey_note": "劃詞後,再 按住 Ctrl鍵才顯示工具列",
"shortcut": "快捷鍵",
"shortcut_note": "劃詞後,使用快捷鍵顯示工具列。請在快捷鍵設定頁面中設置取詞快捷鍵並啟用。",
"shortcut_link": "前往快捷鍵設定"
"ctrlkey": "Ctrl 鍵"
},
"compact_mode": {
"title": "緊湊模式",

View File

@@ -198,7 +198,6 @@
"topics.clear.title": "Καθαρισμός μηνυμάτων",
"topics.copy.image": "Αντιγραφή ως εικόνα",
"topics.copy.md": "Αντιγραφή ως Markdown",
"topics.copy.plain_text": "Αντιγραφή ως απλό κείμενο (αφαίρεση Markdown)",
"topics.copy.title": "Αντιγραφή",
"topics.delete.shortcut": "Πατήστε {{key}} για να διαγράψετε αμέσως",
"topics.edit.placeholder": "Εισαγάγετε το νέο όνομα",
@@ -491,12 +490,8 @@
"urls": "Διευθύνσεις",
"dimensions": "Διαστάσεις ενσωμάτωσης",
"dimensions_size_tooltip": "Το μέγεθος των διαστάσεων ενσωμάτωσης. Όσο μεγαλύτερη η τιμή, τόσο περισσότερες οι διαστάσεις ενσωμάτωσης, αλλά και οι απαιτούμενες μονάδες (Tokens).",
"dimensions_size_placeholder": " Μέγεθος διαστάσεων ενσωμάτωσης, π.χ. 1024",
"dimensions_auto_set": "Αυτόματη ρύθμιση διαστάσεων ενσωμάτωσης",
"dimensions_error_invalid": "Παρακαλώ εισάγετε μέγεθος διαστάσεων ενσωμάτωσης",
"dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})",
"dimensions_set_right": "⚠️ Βεβαιωθείτε ότι το μοντέλο υποστηρίζει το καθορισμένο μέγεθος διαστάσεων ενσωμάτωσης",
"dimensions_default": "Το μοντέλο θα χρησιμοποιήσει τις προεπιλεγμένες διαστάσεις ενσωμάτωσης"
"dimensions_size_placeholder": "Προεπιλεγμένη τιμή (δεν συνιστάται να τροποποιηθεί)",
"dimensions_size_too_large": "Οι διαστάσεις ενσωμάτωσης δεν μπορούν να υπερβούν το όριο περιεχομένου του μοντέλου ({{max_context}})"
},
"languages": {
"arabic": "Αραβικά",
@@ -556,6 +551,7 @@
"error.enter.api.key": "Παρακαλώ εισάγετε το κλειδί API σας",
"error.enter.model": "Παρακαλώ επιλέξτε ένα μοντέλο",
"error.enter.name": "Παρακαλώ εισάγετε ένα όνομα για τη βάση γνώσεων",
"error.fetchTopicName": "Αποτυχία ονοματοδοσίας θέματος",
"error.get_embedding_dimensions": "Απέτυχε η πρόσληψη διαστάσεων ενσωμάτωσης",
"error.invalid.api.host": "Μη έγκυρη διεύθυνση API",
"error.invalid.api.key": "Μη έγκυρο κλειδί API",
@@ -1304,7 +1300,6 @@
"advancedSettings": "Προχωρημένες Ρυθμίσεις"
},
"messages.divider": "Διαχωριστική γραμμή μηνυμάτων",
"messages.divider.tooltip": "Δεν ισχύει για μηνύματα με στυλ φυσαλίδας",
"messages.grid_columns": "Αριθμός στήλων γριλ μηνυμάτων",
"messages.grid_popover_trigger": "Καταγραφή στοιχείων στο grid",
"messages.grid_popover_trigger.click": "Εμφάνιση κλικ",

View File

@@ -199,7 +199,6 @@
"topics.clear.title": "Limpiar mensajes",
"topics.copy.image": "Copiar como imagen",
"topics.copy.md": "Copiar como Markdown",
"topics.copy.plain_text": "Copiar como texto sin formato (eliminar Markdown)",
"topics.copy.title": "Copiar",
"topics.delete.shortcut": "Mantén presionada {{key}} para eliminar directamente",
"topics.edit.placeholder": "Introduce nuevo nombre",
@@ -492,12 +491,8 @@
"urls": "URLs",
"dimensions": "Dimensión de incrustación",
"dimensions_size_tooltip": "Tamaño de la dimensión de incrustación, cuanto mayor sea el valor, mayor será la dimensión de incrustación, pero también consumirá más Tokens",
"dimensions_size_placeholder": " Tamaño de dimensión de incrustación, ej. 1024",
"dimensions_auto_set": "Configuración automática de dimensiones de incrustación",
"dimensions_error_invalid": "Por favor ingrese el tamaño de dimensión de incrustación",
"dimensions_size_too_large": "La dimensión de incrustación no puede exceder el límite del contexto del modelo ({{max_context}})",
"dimensions_set_right": "⚠️ Asegúrese de que el modelo admita el tamaño de dimensión de incrustación establecido",
"dimensions_default": "El modelo utilizará las dimensiones de incrustación predeterminadas"
"dimensions_size_placeholder": "Valor predeterminado (no recomendado modificar)",
"dimensions_size_too_large": "La dimensión de incrustación no puede exceder el límite del contexto del modelo ({{max_context}})"
},
"languages": {
"arabic": "Árabe",
@@ -557,6 +552,7 @@
"error.enter.api.key": "Ingrese su clave API",
"error.enter.model": "Seleccione un modelo",
"error.enter.name": "Ingrese el nombre de la base de conocimiento",
"error.fetchTopicName": "Error al nombrar el tema",
"error.get_embedding_dimensions": "Fallo al obtener las dimensiones de incrustación",
"error.invalid.api.host": "Dirección API inválida",
"error.invalid.api.key": "Clave API inválida",
@@ -1303,7 +1299,6 @@
"advancedSettings": "Configuración avanzada"
},
"messages.divider": "Separador de mensajes",
"messages.divider.tooltip": "No aplicable para mensajes de estilo burbuja",
"messages.grid_columns": "Número de columnas en la cuadrícula de mensajes",
"messages.grid_popover_trigger": "Desencadenante de detalles de cuadrícula",
"messages.grid_popover_trigger.click": "Mostrar al hacer clic",

View File

@@ -198,7 +198,6 @@
"topics.clear.title": "Effacer le message",
"topics.copy.image": "Copier sous forme d'image",
"topics.copy.md": "Copier sous forme de Markdown",
"topics.copy.plain_text": "Copier en tant que texte brut (supprimer Markdown)",
"topics.copy.title": "Copier",
"topics.delete.shortcut": "Maintenez {{key}} pour supprimer directement",
"topics.edit.placeholder": "Entrez un nouveau nom",
@@ -491,12 +490,8 @@
"urls": "URLs",
"dimensions": "Размерность встраивания",
"dimensions_size_tooltip": "Размерность встраивания. Чем больше значение, тем выше размерность, но тем больше токенов требуется",
"dimensions_size_placeholder": " Taille de dimension d'incorporation, ex. 1024",
"dimensions_auto_set": "Réglage automatique des dimensions d'incorporation",
"dimensions_error_invalid": "Veuillez saisir la taille de dimension d'incorporation",
"dimensions_size_too_large": "Размерность встраивания не может превышать ограничение контекста модели ({{max_context}})",
"dimensions_set_right": "⚠️ Assurez-vous que le modèle prend en charge la taille de dimension d'incorporation définie",
"dimensions_default": "Le modèle utilisera les dimensions d'incorporation par défaut"
"dimensions_size_placeholder": "Значение по умолчанию (не рекомендуется изменять)",
"dimensions_size_too_large": "Размерность встраивания не может превышать ограничение контекста модели ({{max_context}})"
},
"languages": {
"arabic": "Arabe",
@@ -556,6 +551,7 @@
"error.enter.api.key": "Veuillez entrer votre clé API",
"error.enter.model": "Veuillez sélectionner un modèle",
"error.enter.name": "Veuillez entrer le nom de la base de connaissances",
"error.fetchTopicName": "Échec de la dénomination du sujet",
"error.get_embedding_dimensions": "Impossible d'obtenir les dimensions d'encodage",
"error.invalid.api.host": "Adresse API invalide",
"error.invalid.api.key": "Clé API invalide",
@@ -1304,7 +1300,6 @@
"advancedSettings": "Расширенные настройки"
},
"messages.divider": "Séparateur de messages",
"messages.divider.tooltip": "Non applicable aux messages de style bulle",
"messages.grid_columns": "Nombre de colonnes de la grille de messages",
"messages.grid_popover_trigger": "Déclencheur de popover de la grille",
"messages.grid_popover_trigger.click": "Afficher au clic",

View File

@@ -199,7 +199,6 @@
"topics.clear.title": "Limpar mensagens",
"topics.copy.image": "Copiar como imagem",
"topics.copy.md": "Copiar como Markdown",
"topics.copy.plain_text": "Copiar como texto simples (remover Markdown)",
"topics.copy.title": "Copiar",
"topics.delete.shortcut": "Pressione {{key}} para deletar diretamente",
"topics.edit.placeholder": "Digite novo nome",
@@ -493,12 +492,8 @@
"urls": "URLs",
"dimensions": "Dimensão de incorporação",
"dimensions_size_tooltip": "Tamanho da dimensão de incorporação, quanto maior o valor, maior a dimensão de incorporação, mas também maior o consumo de tokens",
"dimensions_size_placeholder": " Tamanho da dimensão de incorporação, ex. 1024",
"dimensions_auto_set": "Definição automática de dimensões de incorporação",
"dimensions_error_invalid": "Por favor insira o tamanho da dimensão de incorporação",
"dimensions_size_too_large": "A dimensão de incorporação não pode exceder o limite do contexto do modelo ({{max_context}})",
"dimensions_set_right": "⚠️ Certifique-se de que o modelo suporta o tamanho da dimensão de incorporação definido",
"dimensions_default": "O modelo utilizará as dimensões de incorporação padrão"
"dimensions_size_placeholder": "Valor padrão (não recomendado alterar)",
"dimensions_size_too_large": "A dimensão de incorporação não pode exceder o limite do contexto do modelo ({{max_context}})"
},
"languages": {
"arabic": "Árabe",
@@ -558,6 +553,7 @@
"error.enter.api.key": "Insira sua chave API",
"error.enter.model": "Selecione um modelo",
"error.enter.name": "Insira o nome da base de conhecimento",
"error.fetchTopicName": "Falha ao nomear o tópico",
"error.get_embedding_dimensions": "Falha ao obter dimensões de incorporação",
"error.invalid.api.host": "Endereço API inválido",
"error.invalid.api.key": "Chave API inválida",
@@ -1306,7 +1302,6 @@
"advancedSettings": "Configurações Avançadas"
},
"messages.divider": "Divisor de mensagens",
"messages.divider.tooltip": "Não aplicável a mensagens de estilo bolha",
"messages.grid_columns": "Número de colunas da grade de mensagens",
"messages.grid_popover_trigger": "Disparador de detalhes da grade",
"messages.grid_popover_trigger.click": "Clique para mostrar",

View File

@@ -1,5 +1,4 @@
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CustomTag from '@renderer/components/CustomTag'
import ListItem from '@renderer/components/ListItem'
import Scrollbar from '@renderer/components/Scrollbar'
@@ -152,27 +151,23 @@ const AgentsPage: FC = () => {
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('agents.title')}
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
size="small"
variant="filled"
allowClear
onClear={handleSearchClear}
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
value={searchInput}
maxLength={50}
onChange={(e) => setSearchInput(e.target.value)}
onPressEnter={handleSearch}
/>
<div style={{ width: 80 }} />
</NavbarCenter>
</Navbar>
<div className="p-4">
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: '30%', height: 28, borderRadius: 15, paddingLeft: 12 }}
size="small"
variant="filled"
allowClear
onClear={handleSearchClear}
suffix={<Search size={14} color="var(--color-icon)" onClick={handleSearch} />}
value={searchInput}
maxLength={50}
onChange={(e) => setSearchInput(e.target.value)}
onPressEnter={handleSearch}
/>
<div style={{ width: 80 }} />
</div>
<Main id="content-container">
<AgentsGroupList>
{Object.entries(agentGroups).map(([group]) => (

View File

@@ -1,4 +1,3 @@
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { Button, Input } from 'antd'
import { Search, SettingsIcon, X } from 'lucide-react'
@@ -41,35 +40,37 @@ const AppsPage: FC = () => {
return (
<Container onContextMenu={handleContextMenu}>
<Navbar>
<NavbarMain>
{t('minapp.title')}
<Input
placeholder={t('common.search')}
className="nodrag"
style={{
width: '30%',
height: 28,
borderRadius: 15,
position: 'absolute',
left: '50vw',
transform: 'translateX(-50%)'
}}
size="small"
variant="filled"
suffix={<Search size={18} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
disabled={isSettingsOpen}
/>
<Button
type="text"
className="nodrag"
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
/>
</NavbarMain>
</Navbar>
{/* <Navbar> */}
{/* <NavbarMain> */}
{/* {t('minapp.title')} */}
<div className="p-2">
<Input
placeholder={t('common.search')}
className="nodrag"
style={{
width: '30%',
height: 28,
borderRadius: 15,
position: 'absolute',
left: '50vw',
transform: 'translateX(-50%)'
}}
size="small"
variant="filled"
suffix={<Search size={18} />}
value={search}
onChange={(e) => setSearch(e.target.value)}
disabled={isSettingsOpen}
/>
<Button
type="text"
className="nodrag"
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
/>
</div>
{/* </NavbarMain> */}
{/* </Navbar> */}
<ContentContainer id="content-container">
{isSettingsOpen && <MiniAppSettings />}
{!isSettingsOpen && (

View File

@@ -0,0 +1,47 @@
import { Category } from '@renderer/types/cherryStore'
import React, { Suspense } from 'react'
import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
// 实际的 AgentsPage 组件 - 请确保路径正确
import { discoverRouters } from '../routers'
// import AssistantDetailsPage from '../../agents/AssistantDetailsPage'; // 示例详情页
// 其他分类的页面组件 (如果需要)
// const MiniAppPagePlaceholder = ({ categoryId, subcategoryId }: { categoryId?: string; subcategoryId?: string }) => (
// <div className="p-4">
// MiniApp Placeholder for Category: {categoryId || 'N/A'}, Subcategory: {subcategoryId || 'N/A'}
// </div>
// )
export interface DiscoverContentProps {
activeTabId: string // This should be one of the CherryStoreType values, e.g., "Assistant"
// selectedSubcategoryId: string
currentCategory: Category | undefined
}
const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => {
const location = useLocation() // To see the current path for debugging or more complex logic
if (!currentCategory || !activeTabId) {
return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div>
}
if (!activeTabId && !location.pathname.startsWith('/discover/')) {
return <Navigate to="/discover/assistant?subcategory=all" replace /> // Fallback redirect, adjust as needed
}
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{discoverRouters.map((_Route) => {
if (!_Route.component) return null
return <Route key={_Route.path} path={`/${_Route.path}`} element={<_Route.component />} />
})}
<Route path="*" element={<div>Discover Feature Not Found at {location.pathname}</div>} />
</Routes>
</Suspense>
)
}
export default DiscoverContent

View File

@@ -0,0 +1,64 @@
import { SubCategoryItem } from '@renderer/types/cherryStore'
import { Badge } from '@renderer/ui/badge'
import {
Sidebar,
SidebarContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuSubItem,
SidebarProvider
} from '@renderer/ui/sidebar'
import { InternalCategory } from '../hooks/useDiscoverCategories'
interface DiscoverSidebarProps {
activeCategory: InternalCategory | undefined
selectedSubcategory: string
onSelectSubcategory: (subcategoryId: string, row?: SubCategoryItem) => void
}
export default function DiscoverSidebar({
activeCategory,
selectedSubcategory,
onSelectSubcategory
}: DiscoverSidebarProps) {
if (!activeCategory) {
return (
<Sidebar className="absolute top-0 left-0 h-full border-r">
<SidebarContent>
<p className="p-4 text-sm text-gray-500">No active category selected.</p>
</SidebarContent>
</Sidebar>
)
}
return (
<SidebarProvider className="relative h-full min-h-full w-full">
<Sidebar className="absolute top-0 left-0 h-full border-r">
<SidebarContent>
<SidebarMenu>
{activeCategory.items &&
activeCategory.items.length > 0 &&
activeCategory.items.map((subItem) => (
<SidebarMenuSubItem key={subItem.id}>
<SidebarMenuButton
isActive={subItem.id === selectedSubcategory}
onClick={() => {
onSelectSubcategory(subItem.id, subItem)
}}
size="sm">
<span className="truncate">{subItem.name}</span>
{typeof subItem.count === 'number' && (
<Badge variant="secondary" className="ml-auto shrink-0">
{subItem.count}
</Badge>
)}
</SidebarMenuButton>
</SidebarMenuSubItem>
))}
</SidebarMenu>
</SidebarContent>
</Sidebar>
</SidebarProvider>
)
}

View File

@@ -0,0 +1,158 @@
import { Category } from '@renderer/types/cherryStore'
import { useEffect, useMemo, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { discoverRouters } from '../routers'
// Extended Category type for internal use in hook, including path and sidebar flag
// Export this interface so other files can import it
export interface InternalCategory extends Category {
path: string
hasSidebar?: boolean // Optional: defaults to true if not specified, or handle explicitly
}
// Initial category data with path and hasSidebar
const initialCategories: InternalCategory[] = discoverRouters.map((router) => ({
id: router.id,
title: router.title,
path: router.path,
hasSidebar: !router.component,
// 目前没有需要二级分类的分类
items: []
}))
// Helper to find category by path
const findCategoryByPath = (path: string | undefined): InternalCategory | undefined =>
initialCategories.find((cat) => cat.path === path)
// Helper to find category by id (activeTab)
const findCategoryById = (id: string | undefined): InternalCategory | undefined =>
initialCategories.find((cat) => cat.id === id)
export function useDiscoverCategories() {
const [categories, setCategories] = useState<InternalCategory[]>(initialCategories)
const [activeTab, setActiveTab] = useState<string>('')
const [selectedSubcategory, setSelectedSubcategory] = useState<string>('all')
const navigate = useNavigate()
const location = useLocation()
// Effect to initialize activeTab from URL path segment or navigate to default
useEffect(() => {
const pathSegments = location.pathname.split('/').filter(Boolean) // e.g., ["discover", "assistant"]
// Expects URL like /discover/:categoryPathSegment/...
const currentCategoryPath = pathSegments.length >= 2 && pathSegments[0] === 'discover' ? pathSegments[1] : undefined
const categoryFromPath = findCategoryByPath(currentCategoryPath)
// Synchronize active tab with the category determined from the URL path.
// If a category is found from the path, update the active tab to match its ID.
if (categoryFromPath) {
if (activeTab !== categoryFromPath.id) {
setActiveTab(categoryFromPath.id)
}
} else if (location.pathname === '/discover' || location.pathname === '/discover/') {
// Handle the case where the URL is the base /discover path.
// Redirect to the first category's path to ensure a category is always selected.
if (categories.length > 0) {
const firstCategory = categories[0]
if (firstCategory?.path) {
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
}
}
} else if (!currentCategoryPath && categories.length > 0 && !activeTab) {
// Fallback for invalid or unmatched /discover/xxx URLs.
// If the URL contains a path segment that doesn't correspond to a known category,
// and no tab is active, redirect to the first valid category.
const firstCategory = categories[0]
if (firstCategory?.path) {
navigate(`/discover/${firstCategory.path}?subcategory=all`, { replace: true })
}
}
// If categoryFromPath is undefined, and it's not /discover, it means it's an invalid path like /discover/unknown
// In this case, we don't navigate from here; ideally App.tsx has a NotFound route, or DiscoverContent shows a message.
}, [location.pathname, categories, activeTab, navigate])
// Effect to initialize selectedSubcategory from URL query param or default to 'all'
useEffect(() => {
const searchParams = new URLSearchParams(location.search)
const subcategoryIdFromQuery = searchParams.get('subcategory')
const currentCatDetails = findCategoryById(activeTab) // Use the helper here
if (subcategoryIdFromQuery && currentCatDetails) {
// Check if the subcategory from query is valid for the current active category
if (currentCatDetails.items.some((item) => item.id === subcategoryIdFromQuery)) {
if (selectedSubcategory !== subcategoryIdFromQuery) {
setSelectedSubcategory(subcategoryIdFromQuery)
}
return // Valid subcategory from URL is set, no further action needed in this effect iteration
}
}
// If no valid subcategory in query, or if activeTab has changed and subcategory needs reset/defaulting
if (activeTab && currentCatDetails) {
const defaultSub = currentCatDetails.items.find((item) => item.id === 'all') || currentCatDetails.items[0]
if (defaultSub) {
// Ensure defaultSub exists
// Set selectedSubcategory state first
if (selectedSubcategory !== defaultSub.id) {
setSelectedSubcategory(defaultSub.id)
}
// Then, if URL doesn't match this default, update URL to reflect the default subcategory
// This ensures the URL is the source of truth / always consistent.
if (!subcategoryIdFromQuery || subcategoryIdFromQuery !== defaultSub.id) {
const newSearchParams = new URLSearchParams() // Start with clean params for this path
newSearchParams.set('subcategory', defaultSub.id)
// Ensure we use the current actual path from currentCatDetails if available for navigation
// This avoids issues if location.pathname is briefly out of sync during transitions.
const basePath = currentCatDetails.path
? `/discover/${currentCatDetails.path}`
: location.pathname.split('?')[0]
navigate(`${basePath}?${newSearchParams.toString()}`, { replace: true })
}
}
}
}, [activeTab, location.search, categories, navigate, selectedSubcategory]) // location.pathname removed as basePath logic handles path part
const currentCategory = useMemo(() => {
return findCategoryById(activeTab) // Use the helper here
}, [activeTab]) // categories removed from deps as findCategoryById uses stable initialCategories
const handleSelectTab = (tabId: string) => {
const categoryToSelect = findCategoryById(tabId)
if (categoryToSelect && categoryToSelect.path && activeTab !== tabId) {
navigate(`/discover/${categoryToSelect.path}?subcategory=all`)
}
}
const handleSelectSubcategory = (subcategoryId: string) => {
const currentCatDetails = findCategoryById(activeTab)
if (selectedSubcategory !== subcategoryId && currentCatDetails?.path) {
const newSearchParams = new URLSearchParams()
newSearchParams.set('subcategory', subcategoryId)
navigate(`/discover/${currentCatDetails.path}?${newSearchParams.toString()}`, { replace: false })
}
}
// Ensure each category has an "All" subcategory (runs once on mount)
useEffect(() => {
setCategories((prev) =>
prev.map((cat) => {
if (!cat.items.some((item) => item.id === 'all')) {
return { ...cat, items: [{ id: 'all', name: `All ${cat.title}` }, ...cat.items] }
}
return cat
})
)
}, [])
return {
categories,
activeTab,
selectedSubcategory,
currentCategory,
handleSelectTab,
handleSelectSubcategory,
setActiveTab
}
}

View File

@@ -0,0 +1,83 @@
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
// import { useRuntime } from '@renderer/hooks/useRuntime' // No longer needed if resourcesPath is not used
import { Tabs as VercelTabs } from '@renderer/ui/vercel-tabs'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom'
// Import Context and the main Dialog Manager component
import DiscoverContent from './components/DiscoverContent' // Removed DiscoverContent import
import DiscoverSidebar from './components/DiscoverSidebar'
import { InternalCategory, useDiscoverCategories } from './hooks/useDiscoverCategories'
// Function to adapt categories for VercelTabs
const adaptCategoriesForVercelTabs = (categories: InternalCategory[]) => {
return categories.map((category) => ({
id: category.id, // VercelTabs expects `id`
label: category.title // VercelTabs expects `label`
}))
}
export default function DiscoverPage() {
const { t } = useTranslation()
const {
categories,
activeTab,
selectedSubcategory,
currentCategory,
handleSelectTab,
handleSelectSubcategory,
setActiveTab
} = useDiscoverCategories()
// Path like /discover/:categoryIdFromUrl. categoryIdFromUrl is lowercase from URL.
const { categoryIdFromUrl } = useParams<{ categoryIdFromUrl: string }>()
useEffect(() => {
const matchedCategory = categories.find((cat) => cat.id.toLowerCase() === categoryIdFromUrl?.toLowerCase())
if (matchedCategory && activeTab !== matchedCategory.id) {
setActiveTab(matchedCategory.id)
}
}, [categoryIdFromUrl, categories, activeTab, setActiveTab])
const vercelTabsData = adaptCategoriesForVercelTabs(categories)
return (
<div className="h-full w-full">
<div className="flex h-full w-full flex-col overflow-hidden">
<NavbarMain className="h-auto flex-shrink-0">
<NavbarCenter>{t('discover.title')}</NavbarCenter>
</NavbarMain>
{categories.length > 0 && (
<div className="px-4 py-2">
<VercelTabs tabs={vercelTabsData} onTabChange={handleSelectTab} />
</div>
)}
<div className="flex flex-grow flex-row overflow-auto">
{currentCategory?.hasSidebar && (
<div className="w-64 flex-shrink-0 border-r">
<DiscoverSidebar
activeCategory={currentCategory}
selectedSubcategory={selectedSubcategory}
onSelectSubcategory={handleSelectSubcategory}
/>
</div>
)}
{/* {!currentCategory && categories.length > 0 && (
<div className="w-64 flex-shrink-0 border-r p-4 text-muted-foreground">Select a category...</div>
)} */}
<main className="flex-grow overflow-hidden">
<DiscoverContent
activeTabId={activeTab}
// selectedSubcategoryId={selectedSubcategory}
currentCategory={currentCategory}
/>
</main>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import i18n from '@renderer/i18n'
import { CherryStoreType } from '@renderer/types/cherryStore'
import { lazy } from 'react'
export const discoverRouters = [
{
id: CherryStoreType.ASSISTANT,
title: i18n.t('assistants.title'),
path: 'assistant',
component: lazy(() => import('../agents/AgentsPage'))
},
{
id: CherryStoreType.MINI_APP,
title: i18n.t('minapp.title'),
path: 'mini-app',
component: lazy(() => import('../apps/AppsPage'))
},
{
id: CherryStoreType.TRANSLATE,
title: i18n.t('translate.title'),
path: 'translate',
component: lazy(() => import('../translate/TranslatePage'))
},
{
id: CherryStoreType.FILES,
title: i18n.t('files.title'),
path: 'files',
component: lazy(() => import('../files/FilesPage'))
},
{
id: CherryStoreType.PAINTINGS,
title: i18n.t('paintings.title'),
path: 'paintings/*',
isPrefix: true,
component: lazy(() => import('../paintings/PaintingsRoutePage'))
},
{
id: CherryStoreType.MCP_SERVER,
title: i18n.t('common.mcp'),
path: 'mcp-servers/*',
isPrefix: true,
component: lazy(() => import('../mcp-servers'))
}
]

View File

@@ -0,0 +1,7 @@
import { Category } from '@renderer/types/cherryStore'
export interface DiscoverContextType {
selectedSubcategory: string
activeTabId: string
currentCategory?: Category // currentCategory might be undefined initially
}

View File

@@ -5,7 +5,6 @@ import {
SortAscendingOutlined,
SortDescendingOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import ListItem from '@renderer/components/ListItem'
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
import Logger from '@renderer/config/logger'
@@ -207,9 +206,9 @@ const FilesPage: FC = () => {
return (
<Container>
<Navbar>
{/* <NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
</Navbar>
</NavbarMain> */}
<ContentContainer id="content-container">
<SideNav>
{menuItems.map((item) => (

View File

@@ -15,7 +15,6 @@ import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages/Messages'
import Tabs from './Tabs'
interface Props {
assistant: Assistant
@@ -38,7 +37,7 @@ const Chat: FC<Props> = (props) => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
return `calc(100vw - ${minusAssistantsWidth} ${minusRightTopicsWidth})`
}, [showAssistants, showTopics, topicPosition])
useHotkeys('esc', () => {
@@ -128,15 +127,6 @@ const Chat: FC<Props> = (props) => {
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
</QuickPanelProvider>
</Main>
{topicPosition === 'right' && showTopics && (
<Tabs
activeAssistant={assistant}
activeTopic={props.activeTopic}
setActiveAssistant={props.setActiveAssistant}
setActiveTopic={props.setActiveTopic}
position="right"
/>
)}
</Container>
)
}

View File

@@ -0,0 +1,167 @@
import { Navbar } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { LayoutGrid, PanelLeft, PanelRight, Search } from 'lucide-react'
import { FC, useCallback } from 'react'
import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
activeAssistant: Assistant
position: 'left' | 'right'
}
const ChatNavbar: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const isFullscreen = useFullscreen()
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
// Function to toggle assistants with cooldown
const handleToggleShowAssistants = useCallback(() => {
if (showAssistants) {
// When hiding sidebar, set cooldown
toggleShowAssistants()
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowAssistants()
}
}, [showAssistants, toggleShowAssistants])
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
useShortcut('toggle_show_topics', () => {
if (topicPosition === 'right') {
toggleShowTopics()
} else {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
})
useShortcut('search_message', () => {
SearchPopup.show()
})
const handleNarrowModeToggle = async () => {
await modelGenerating()
dispatch(setNarrowMode(!narrowMode))
}
return (
<Navbar className="home-navbar">
<NavbarContainer $isFullscreen={isFullscreen} $showSidebar={showAssistants} className="home-navbar-right">
<HStack alignItems="center">
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
</NavbarIcon>
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
{isMac && (
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
)}
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{sidebarIcons.visible.includes('minapp') && (
<MinAppsPopover>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
<NarrowIcon>
<LayoutGrid size={18} />
</NarrowIcon>
</Tooltip>
</MinAppsPopover>
)}
</HStack>
</NavbarContainer>
</Navbar>
)
}
const NavbarContainer = styled.div<{ $isFullscreen: boolean; $showSidebar: boolean }>`
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
height: var(--navbar-height);
max-height: var(--navbar-height);
min-height: var(--navbar-height);
justify-content: space-between;
padding-left: ${({ $showSidebar }) => (isMac ? ($showSidebar ? '10px' : '75px') : '15px')};
font-weight: bold;
color: var(--color-text-1);
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
-webkit-app-region: drag;
`
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
padding: 0 7px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
cursor: pointer;
.iconfont {
font-size: 18px;
color: var(--color-icon);
&.icon-a-addchat {
font-size: 20px;
}
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
color: var(--color-icon-white);
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default ChatNavbar

View File

@@ -1,18 +1,14 @@
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useChat } from '@renderer/hooks/useChat'
import { useSettings } from '@renderer/hooks/useSettings'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import NavigationService from '@renderer/services/NavigationService'
import { Assistant } from '@renderer/types'
import { FC, useEffect, useState } from 'react'
import { FC, useEffect } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import Chat from './Chat'
import Navbar from './Navbar'
import HomeTabs from './Tabs'
let _activeAssistant: Assistant
import ChatNavbar from './ChatNavbar'
const HomePage: FC = () => {
const { assistants } = useAssistants()
@@ -21,12 +17,9 @@ const HomePage: FC = () => {
const location = useLocation()
const state = location.state
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
const { activeAssistant, activeTopic, setActiveAssistant, setActiveTopic } = useChat()
const { showAssistants, showTopics, topicPosition } = useSettings()
_activeAssistant = activeAssistant
useEffect(() => {
NavigationService.setNavigate(navigate)
}, [navigate])
@@ -61,23 +54,8 @@ const HomePage: FC = () => {
return (
<Container id="home-page">
<Navbar
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
setActiveAssistant={setActiveAssistant}
position="left"
/>
<ChatNavbar activeAssistant={activeAssistant} position="left" />
<ContentContainer id="content-container">
{showAssistants && (
<HomeTabs
activeAssistant={activeAssistant}
activeTopic={activeTopic}
setActiveAssistant={setActiveAssistant}
setActiveTopic={setActiveTopic}
position="left"
/>
)}
<Chat
assistant={activeAssistant}
activeTopic={activeTopic}
@@ -93,7 +71,6 @@ const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
max-width: calc(100vw - var(--sidebar-width));
`
const ContentContainer = styled.div`

View File

@@ -14,7 +14,7 @@ import { useAssistant } from '@renderer/hooks/useAssistant'
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
@@ -52,6 +52,7 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools'
import KnowledgeBaseInput from './KnowledgeBaseInput'
import MentionModelsInput from './MentionModelsInput'
import SendMessageButton from './SendMessageButton'
import SettingButton from './SettingButton'
import TokenCount from './TokenCount'
interface Props {
@@ -405,8 +406,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
}
const addNewTopic = useCallback(async () => {
await modelGenerating()
const topic = getDefaultTopic(assistant.id)
await db.topics.add({ id: topic.id, messages: [] })
@@ -858,6 +857,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
ToolbarButton={ToolbarButton}
onClick={onNewContext}
/>
<SettingButton assistant={assistant} ToolbarButton={ToolbarButton} />
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
{loading && (
<Tooltip placement="top" title={t('chat.input.pause')} arrow>

View File

@@ -176,7 +176,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
newList.push({
label: t('settings.mcp.addServer') + '...',
icon: <Plus />,
action: () => navigate('/settings/mcp')
action: () => navigate('/mcp-servers')
})
newList.unshift({

View File

@@ -0,0 +1,43 @@
import { Assistant } from '@renderer/types'
import { Popover } from 'antd'
import { Settings } from 'lucide-react'
import { FC, useState } from 'react'
import SettingsTab from '../Tabs/SettingsTab'
interface Props {
assistant: Assistant
ToolbarButton: any
}
const SettingButton: FC<Props> = ({ assistant, ToolbarButton }) => {
const [open, setOpen] = useState(false)
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen)
}
const handleClose = () => {
setOpen(false)
}
return (
<Popover
placement="topLeft"
content={<SettingsTab assistant={assistant} onClose={handleClose} />}
trigger="click"
open={open}
onOpenChange={handleOpenChange}
styles={{
body: {
padding: '4px 2px 4px 2px'
}
}}>
<ToolbarButton type="text">
<Settings size={18} />
</ToolbarButton>
</Popover>
)
}
export default SettingButton

View File

@@ -93,15 +93,19 @@ const Markdown: FC<Props> = ({ block }) => {
} as Partial<Components>
}, [onSaveCodeBlock])
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
const urlTransform = useCallback((value: string) => {
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
return defaultUrlTransform(value)
}, [])
// if (role === 'user' && !renderInputMessageAsMarkdown) {
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
// }
if (messageContent.includes('<style>')) {
components.style = MarkdownShadowDOMRenderer as any
}
return (
<ReactMarkdown
rehypePlugins={rehypePlugins}

View File

@@ -31,7 +31,7 @@ const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock }> = ({ block }) =>
}
const Alert = styled(AntdAlert)`
margin: 0.5rem 0;
margin: 15px 0 8px;
padding: 10px;
font-size: 12px;
`

View File

@@ -151,7 +151,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
</Flex>
)}
{role === 'user' && !renderInputMessageAsMarkdown ? (
<p className="markdown" style={{ whiteSpace: 'pre-wrap' }}>
<p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>
{block.content}
</p>
) : (

View File

@@ -42,7 +42,6 @@ const blockWrapperVariants = {
const AnimatedBlockWrapper: React.FC<AnimatedBlockWrapperProps> = ({ children, enableAnimation }) => {
return (
<motion.div
className="block-wrapper"
variants={blockWrapperVariants}
initial={enableAnimation ? 'hidden' : 'static'}
animate={enableAnimation ? 'visible' : 'static'}>

View File

@@ -139,7 +139,6 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
<WebSearchCard>
<ContextMenu>
<WebSearchCardHeader>
<CitationIndex>{citation.number}</CitationIndex>
{citation.showFavicon && citation.url && (
<Favicon hostname={new URL(citation.url).hostname} alt={citation.title || citation.hostname || ''} />
)}
@@ -163,7 +162,6 @@ const KnowledgeCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
<WebSearchCard>
<ContextMenu>
<WebSearchCardHeader>
<CitationIndex>{citation.number}</CitationIndex>
{citation.showFavicon && <FileSearch width={16} />}
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
{citation.title}
@@ -212,13 +210,6 @@ const PreviewIcon = styled.div`
}
`
const CitationIndex = styled.div`
font-size: 14px;
line-height: 1.6;
color: var(--color-text-2);
margin-right: 8px;
`
const CitationLink = styled.a`
font-size: 14px;
line-height: 1.6;

View File

@@ -21,6 +21,7 @@ import MessageEditor from './MessageEditor'
import MessageErrorBoundary from './MessageErrorBoundary'
import MessageHeader from './MessageHeader'
import MessageMenubar from './MessageMenubar'
import MessageTokens from './MessageTokens'
interface Props {
message: Message
@@ -98,7 +99,7 @@ const MessageItem: FC<Props> = ({
const isAssistantMessage = message.role === 'assistant'
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
const messageBorder = !isBubbleStyle && showMessageDivider ? '1px dotted var(--color-border)' : 'none'
const messageBorder = showMessageDivider ? undefined : 'none'
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
@@ -129,6 +130,22 @@ const MessageItem: FC<Props> = ({
)
}
if (isEditing) {
return (
<MessageContainer style={{ paddingTop: 15 }}>
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
<MessageEditor
message={message}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
</div>
</MessageContainer>
)
}
return (
<MessageContainer
key={message.id}
@@ -138,100 +155,35 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage
})}
ref={messageContainerRef}
style={{
...style,
justifyContent: isBubbleStyle ? (isAssistantMessage ? 'flex-start' : 'flex-end') : undefined,
flex: isBubbleStyle ? undefined : 1
}}>
{isEditing && (
<ContextMenu
style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
<ContextMenu>
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<MessageContentContainer
className={
message.role === 'user'
? 'message-content-container message-content-container-user'
: message.role === 'assistant'
? 'message-content-container message-content-container-assistant'
: 'message-content-container'
}
style={{
display: 'flex',
flexDirection: 'column',
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
width: isBubbleStyle ? '70%' : '100%'
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
fontSize,
background: messageBackground,
overflowY: 'visible',
maxWidth: narrowMode ? 760 : undefined
}}>
<MessageHeader
message={message}
assistant={assistant}
model={model}
key={getModelUniqId(model)}
index={index}
/>
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
<MessageEditor
message={message}
onSave={handleEditSave}
onResend={handleEditResend}
onCancel={handleEditCancel}
/>
</div>
</ContextMenu>
)}
{!isEditing && (
<ContextMenu
style={{
display: 'flex',
flexDirection: 'column',
alignSelf: isAssistantMessage ? 'flex-start' : 'flex-end',
flex: 1,
maxWidth: '100%'
}}>
<MessageHeader
message={message}
assistant={assistant}
model={model}
key={getModelUniqId(model)}
index={index}
/>
<MessageContentContainer
className={
message.role === 'user'
? 'message-content-container message-content-container-user'
: message.role === 'assistant'
? 'message-content-container message-content-container-assistant'
: 'message-content-container'
}
style={{
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
fontSize,
background: messageBackground,
overflowY: 'visible',
maxWidth: narrowMode ? 760 : undefined,
alignSelf: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined
}}>
<MessageErrorBoundary>
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && !isBubbleStyle && (
<MessageFooter
className="MessageFooter"
style={{
borderTop: messageBorder,
flexDirection: !isLastMessage ? 'row-reverse' : undefined
}}>
<MessageMenubar
message={message}
assistant={assistant}
model={model}
index={index}
topic={topic}
isLastMessage={isLastMessage}
isAssistantMessage={isAssistantMessage}
isGrouped={isGrouped}
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
setModel={setModel}
/>
</MessageFooter>
)}
</MessageContentContainer>
{showMenubar && isBubbleStyle && (
<MessageErrorBoundary>
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && (
<MessageFooter
className="MessageFooter"
style={{
borderTop: messageBorder,
flexDirection: !isAssistantMessage ? 'row-reverse' : undefined
border: messageBorder,
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined
}}>
<MessageTokens message={message} isLastMessage={isLastMessage} />
<MessageMenubar
message={message}
assistant={assistant}
@@ -246,8 +198,8 @@ const MessageItem: FC<Props> = ({
/>
</MessageFooter>
)}
</ContextMenu>
)}
</MessageContentContainer>
</ContextMenu>
</MessageContainer>
)
}
@@ -262,7 +214,7 @@ const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolea
const MessageContainer = styled.div`
display: flex;
width: 100%;
flex-direction: column;
position: relative;
transition: background-color 0.3s ease;
padding: 0 20px;
@@ -305,12 +257,12 @@ const MessageFooter = styled.div`
align-items: center;
padding: 2px 0;
margin-top: 2px;
border-top: 0.5px dotted var(--color-border);
gap: 20px;
`
const NewContextMessage = styled.div`
cursor: pointer;
flex: 1;
`
export default memo(MessageItem)

View File

@@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
return (
<>
{!isEmpty(message.mentions) && (
<Flex gap="8px" wrap>
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex>
)}

View File

@@ -180,8 +180,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
$isGrouped={isGrouped}
key={message.id}
className={classNames({
// 加个卡片布局
'group-message-wrapper': message.role === 'assistant' && (isHorizontal || isGrid) && isGrouped,
'group-message-wrapper': message.role === 'assistant' && isHorizontal && isGrouped,
[multiModelMessageStyle]: isGrouped,
selected: message.id === selectedMessageId
})}>
@@ -316,7 +315,6 @@ interface MessageWrapperProps {
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
width: 100%;
display: flex;
&.horizontal {
display: inline-block;

View File

@@ -17,13 +17,10 @@ import { CSSProperties, FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import MessageTokens from './MessageTokens'
interface Props {
message: Message
assistant: Assistant
model?: Model
index: number | undefined
}
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
@@ -31,7 +28,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
return modelId ? getModelLogo(modelId) : undefined
}
const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) => {
const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const avatar = useAvatar()
const { theme } = useTheme()
const { userName, sidebarIcons } = useSettings()
@@ -55,11 +52,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
const isAssistantMessage = message.role === 'assistant'
const showMinappIcon = sidebarIcons.visible.includes('minapp')
const { showTokens } = useSettings()
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const isLastMessage = index === 0
const showMiniApp = useCallback(() => {
showMinappIcon && model?.provider && openMinappById(model.provider)
@@ -116,14 +111,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, index }) =>
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
{username}
</UserName>
<InfoWrap
style={{
flexDirection: !isAssistantMessage && isBubbleStyle ? 'row-reverse' : undefined
}}>
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
{showTokens && <DividerContainer style={{ color: 'var(--color-text-3)' }}> | </DividerContainer>}
<MessageTokens message={message} isLastMessage={isLastMessage} />
</InfoWrap>
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
</Container>
@@ -152,19 +140,6 @@ const UserWrap = styled.div`
justify-content: space-between;
`
const InfoWrap = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
`
const DividerContainer = styled.div`
font-size: 10px;
color: var(--color-text-3);
margin: 0 2px;
`
const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>`
font-size: 14px;
font-weight: 600;

View File

@@ -5,7 +5,6 @@ import { TranslateLanguageOptions } from '@renderer/config/translate'
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
import { useChatContext } from '@renderer/hooks/useChatContext'
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
import { useMessageStyle } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getMessageTitle } from '@renderer/services/MessagesService'
import { translateText } from '@renderer/services/TranslateService'
@@ -23,7 +22,6 @@ import {
exportMessageToNotion,
messageToMarkdown
} from '@renderer/utils/export'
import { copyMessageAsPlainText } from '@renderer/utils/copy'
// import { withMessageThought } from '@renderer/utils/formats'
import { removeTrailingDoubleSpaces } from '@renderer/utils/markdown'
import { findMainTextBlocks, findTranslationBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
@@ -68,9 +66,6 @@ const MessageMenubar: FC<Props> = (props) => {
appendAssistantResponse,
removeMessageBlock
} = useMessageOperations(topic)
const { isBubbleStyle } = useMessageStyle()
const loading = useTopicLoading(topic)
const isUserMessage = message.role === 'user'
@@ -202,11 +197,6 @@ const MessageMenubar: FC<Props> = (props) => {
key: 'export',
icon: <Share size={16} color="var(--color-icon)" style={{ marginTop: 3 }} />,
children: [
{
label: t('chat.topics.copy.plain_text'),
key: 'copy_message_plain_text',
onClick: () => copyMessageAsPlainText(message)
},
exportMenuOptions.image && {
label: t('chat.topics.copy.image'),
key: 'img',
@@ -342,29 +332,24 @@ const MessageMenubar: FC<Props> = (props) => {
return translationBlocks.length > 0
}, [message])
const softHoverBg = isBubbleStyle && !isLastMessage
return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton
className="message-action-button"
onClick={() => handleResendUserMessage()}
$softHoverBg={isBubbleStyle}>
<ActionButton className="message-action-button" onClick={() => handleResendUserMessage()}>
<SyncOutlined />
</ActionButton>
</Tooltip>
)}
{message.role === 'user' && (
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={onEdit}>
<EditOutlined />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onCopy} $softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={onCopy}>
{!copied && <Copy size={16} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton>
@@ -381,7 +366,7 @@ const MessageMenubar: FC<Props> = (props) => {
mouseEnterDelay={0.8}
open={showRegenerateTooltip}
onOpenChange={setShowRegenerateTooltip}>
<ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
<ActionButton className="message-action-button">
<RefreshCw size={16} />
</ActionButton>
</Tooltip>
@@ -389,7 +374,7 @@ const MessageMenubar: FC<Props> = (props) => {
)}
{isAssistantMessage && (
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={onMentionModel}>
<AtSign size={16} />
</ActionButton>
</Tooltip>
@@ -459,10 +444,7 @@ const MessageMenubar: FC<Props> = (props) => {
placement="top"
arrow>
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<Languages size={16} />
</ActionButton>
</Tooltip>
@@ -470,7 +452,7 @@ const MessageMenubar: FC<Props> = (props) => {
)}
{isAssistantMessage && isGrouped && (
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={onUseful}>
{message.useful ? (
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
) : (
@@ -485,7 +467,7 @@ const MessageMenubar: FC<Props> = (props) => {
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
onConfirm={() => deleteMessage(message.id)}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()} $softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<Tooltip
title={t('common.delete')}
mouseEnterDelay={1}
@@ -501,10 +483,7 @@ const MessageMenubar: FC<Props> = (props) => {
trigger={['click']}
placement="topRight"
arrow>
<ActionButton
className="message-action-button"
onClick={(e) => e.stopPropagation()}
$softHoverBg={softHoverBg}>
<ActionButton className="message-action-button" onClick={(e) => e.stopPropagation()}>
<Menu size={19} />
</ActionButton>
</Dropdown>
@@ -521,7 +500,7 @@ const MenusBar = styled.div`
gap: 6px;
`
const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
const ActionButton = styled.div`
cursor: pointer;
border-radius: 8px;
display: flex;
@@ -532,11 +511,8 @@ const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
height: 30px;
transition: all 0.2s ease;
&:hover {
background-color: ${(props) =>
props.$softHoverBg ? 'var(--color-background-soft)' : 'var(--color-background-mute)'};
color: var(--color-text-1);
.anticon,
.lucide {
background-color: var(--color-background-mute);
.anticon {
color: var(--color-text-1);
}
}
@@ -546,6 +522,9 @@ const ActionButton = styled.div<{ $softHoverBg?: boolean }>`
font-size: 14px;
color: var(--color-icon);
}
&:hover {
color: var(--color-text-1);
}
.icon-at {
font-size: 16px;
}

View File

@@ -69,14 +69,19 @@ const MessgeTokens: React.FC<MessageTokensProps> = ({ message }) => {
}
const MessageMetadata = styled.div`
font-size: 10px;
color: var(--color-text-3);
font-size: 11px;
color: var(--color-text-2);
user-select: text;
margin: 2px 0;
cursor: pointer;
text-align: right;
.tokens span {
padding: 0 2px;
.tokens {
display: block;
span {
padding: 0 2px;
}
}
`

View File

@@ -1,231 +0,0 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import FloatingSidebar from '@renderer/components/Popups/FloatingSidebar'
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd'
import { t } from 'i18next'
import { LayoutGrid, MessageSquareDiff, PanelLeftClose, PanelRightClose, Search } from 'lucide-react'
import { FC, useCallback, useState } from 'react'
import styled from 'styled-components'
import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton'
interface Props {
activeAssistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
setActiveAssistant: (assistant: Assistant) => void
position: 'left' | 'right'
}
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const isFullscreen = useFullscreen()
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
const [sidebarHideCooldown, setSidebarHideCooldown] = useState(false)
// Function to toggle assistants with cooldown
const handleToggleShowAssistants = useCallback(() => {
if (showAssistants) {
// When hiding sidebar, set cooldown
toggleShowAssistants()
setSidebarHideCooldown(true)
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowAssistants()
}
}, [showAssistants, toggleShowAssistants])
const handleToggleShowTopics = useCallback(() => {
if (showTopics) {
// When hiding sidebar, set cooldown
toggleShowTopics()
setSidebarHideCooldown(true)
// setTimeout(() => {
// setSidebarHideCooldown(false)
// }, 10000) // 10 seconds cooldown
} else {
// When showing sidebar, no cooldown needed
toggleShowTopics()
}
}, [showTopics, toggleShowTopics])
useShortcut('toggle_show_assistants', handleToggleShowAssistants)
useShortcut('toggle_show_topics', () => {
if (topicPosition === 'right') {
toggleShowTopics()
} else {
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
}
})
useShortcut('search_message', () => {
SearchPopup.show()
})
const handleNarrowModeToggle = async () => {
await modelGenerating()
dispatch(setNarrowMode(!narrowMode))
}
return (
<Navbar className="home-navbar">
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={handleToggleShowAssistants} style={{ marginLeft: isMac && !isFullscreen ? 16 : 0 }}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} style={{ marginRight: 5 }}>
<MessageSquareDiff size={18} />
</NavbarIcon>
</Tooltip>
</NavbarLeft>
)}
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right">
<HStack alignItems="center">
{!showAssistants && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'left'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{!showAssistants && sidebarHideCooldown && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
<NavbarIcon
onClick={() => toggleShowAssistants()}
style={{ marginRight: 8, marginLeft: isMac && !isFullscreen ? 4 : -12 }}
onMouseOut={() => setSidebarHideCooldown(false)}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
<SelectModelButton assistant={assistant} />
</HStack>
<HStack alignItems="center" gap={8}>
<UpdateAppButton />
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{sidebarIcons.visible.includes('minapp') && (
<MinAppsPopover>
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8}>
<NarrowIcon>
<LayoutGrid size={18} />
</NarrowIcon>
</Tooltip>
</MinAppsPopover>
)}
{topicPosition === 'right' && !showTopics && !sidebarHideCooldown && (
<FloatingSidebar
activeAssistant={assistant}
setActiveAssistant={setActiveAssistant}
activeTopic={activeTopic}
setActiveTopic={setActiveTopic}
position={'right'}>
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
</FloatingSidebar>
)}
{topicPosition === 'right' && !showTopics && sidebarHideCooldown && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => toggleShowTopics()} onMouseOut={() => setSidebarHideCooldown(false)}>
<PanelLeftClose size={18} />
</NavbarIcon>
</Tooltip>
)}
{topicPosition === 'right' && showTopics && (
<Tooltip title={t('navbar.hide_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={() => handleToggleShowTopics()}>
<PanelRightClose size={18} />
</NavbarIcon>
</Tooltip>
)}
</HStack>
</NavbarRight>
</Navbar>
)
}
export const NavbarIcon = styled.div`
-webkit-app-region: none;
border-radius: 8px;
height: 30px;
padding: 0 7px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
cursor: pointer;
.iconfont {
font-size: 18px;
color: var(--color-icon);
&.icon-a-addchat {
font-size: 20px;
}
&.icon-a-darkmode {
font-size: 20px;
}
&.icon-appstore {
font-size: 20px;
}
}
.anticon {
color: var(--color-icon);
font-size: 16px;
}
&:hover {
background-color: var(--color-background-mute);
color: var(--color-icon-white);
}
`
const NarrowIcon = styled(NavbarIcon)`
@media (max-width: 1000px) {
display: none;
}
`
export default HeaderNavbar

View File

@@ -6,7 +6,7 @@ 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 { Divider, Tooltip } from 'antd'
import { Tooltip } from 'antd'
import { FC, useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -80,7 +80,7 @@ const Assistants: FC<AssistantsTabProps> = ({
if (assistantsTabSortType === 'tags') {
return (
<Container className="assistants-tab" ref={containerRef}>
<div style={{ marginBottom: '8px' }}>
<div style={{ display: 'flex', flexDirection: 'column', marginBottom: 4, gap: 10 }}>
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
{group.tag !== t('assistants.tags.untagged') && (
@@ -95,7 +95,7 @@ const Assistants: FC<AssistantsTabProps> = ({
{group.tag}
</GroupTitleName>
</Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider>
<GroupTitleDivider />
</GroupTitle>
)}
{!collapsedTags[group.tag] && (
@@ -197,23 +197,20 @@ const AssistantAddItem = styled.div`
cursor: pointer;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
background-color: var(--color-list-item-hover);
}
`
const GroupTitle = styled.div`
padding: 8px 0;
position: relative;
color: var(--color-text-2);
font-size: 12px;
font-weight: 500;
margin-bottom: -8px;
cursor: pointer;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 24px;
`
const GroupTitleName = styled.div`
@@ -221,13 +218,18 @@ const GroupTitleName = styled.div`
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
background-color: var(--color-background);
box-sizing: border-box;
padding: 0 4px;
color: var(--color-text);
position: absolute;
transform: translateY(2px);
font-size: 13px;
line-height: 24px;
margin-right: 5px;
display: flex;
`
const GroupTitleDivider = styled.div`
flex: 1;
border-top: 1px solid var(--color-border);
`
const AssistantName = styled.div`

View File

@@ -69,6 +69,7 @@ import OpenAISettingsGroup from './components/OpenAISettingsGroup'
interface Props {
assistant: Assistant
onClose: () => void
}
const SettingsTab: FC<Props> = (props) => {
@@ -197,7 +198,10 @@ const SettingsTab: FC<Props> = (props) => {
type="text"
size="small"
icon={<Settings2 size={16} />}
onClick={() => AssistantSettingsPopup.show({ assistant, tab: 'model' })}
onClick={() => {
AssistantSettingsPopup.show({ assistant, tab: 'model' })
props.onClose()
}}
/>
</HStack>
}>
@@ -318,12 +322,7 @@ const SettingsTab: FC<Props> = (props) => {
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>
{t('settings.messages.divider')}
<Tooltip title={t('settings.messages.divider.tooltip')}>
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
</Tooltip>
</SettingRowTitleSmall>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
@@ -686,8 +685,10 @@ const SettingsTab: FC<Props> = (props) => {
}
const Container = styled(Scrollbar)`
min-width: 300px;
max-width: 40vw;
max-height: 70vh;
display: flex;
flex: 1;
flex-direction: column;
padding: 0 8px;
padding-right: 0;

View File

@@ -26,7 +26,7 @@ import { RootState } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types'
import { removeSpecialCharactersForFileName } from '@renderer/utils'
import { copyTopicAsMarkdown, copyTopicAsPlainText } from '@renderer/utils/copy'
import { copyTopicAsMarkdown } from '@renderer/utils/copy'
import {
exportMarkdownToJoplin,
exportMarkdownToSiyuan,
@@ -280,11 +280,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
label: t('chat.topics.copy.md'),
key: 'md',
onClick: () => copyTopicAsMarkdown(topic)
},
{
label: t('chat.topics.copy.plain_text'),
key: 'plain_text',
onClick: () => copyTopicAsPlainText(topic)
}
]
},

View File

@@ -20,11 +20,11 @@ import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { getDefaultModel, getDefaultTopic } from '@renderer/services/AssistantService'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { getLeadingEmoji, uuid } from '@renderer/utils'
import { classNames, getLeadingEmoji, uuid } from '@renderer/utils'
import { hasTopicPendingRequests } from '@renderer/utils/queue'
import { Dropdown, MenuProps } from 'antd'
import { Button, Dropdown, MenuProps } from 'antd'
import { omit } from 'lodash'
import { AlignJustify, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { AlignJustify, EllipsisVertical, Plus, Settings2, Tag, Tags } from 'lucide-react'
import { FC, memo, startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -63,6 +63,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
const { assistants, updateAssistants } = useAssistants()
const [isPending, setIsPending] = useState(false)
const [isMenuOpen, setIsMenuOpen] = useState(false)
useEffect(() => {
if (isActive) {
@@ -141,7 +142,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
<Container onClick={handleSwitch} className={classNames({ active: isActive, 'is-menu-open': isMenuOpen })}>
<AssistantNameRow className="name" title={fullAssistantName}>
{assistantIconType === 'model' ? (
<ModelAvatar
@@ -159,11 +160,15 @@ const AssistantItem: FC<AssistantItemProps> = ({
)}
<AssistantName className="text-nowrap">{assistantName}</AssistantName>
</AssistantNameRow>
{isActive && (
<MenuButton onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
<TopicCount className="topics-count">{assistant.topics.length}</TopicCount>
</MenuButton>
)}
<Dropdown menu={{ items: menuItems }} trigger={['click']} onOpenChange={setIsMenuOpen}>
<Button
className="item-menu-button"
type="text"
size="small"
icon={<EllipsisVertical size={16} color="var(--color-text-3)" />}
onClick={(e) => e.stopPropagation()}
/>
</Dropdown>
</Container>
</Dropdown>
)
@@ -382,6 +387,7 @@ const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 8px;
height: 37px;
position: relative;
@@ -389,12 +395,23 @@ const Container = styled.div`
border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);
cursor: pointer;
&.is-menu-open {
.item-menu-button {
display: block;
}
}
&:hover {
background-color: var(--color-list-item-hover);
.item-menu-button {
display: block;
}
}
&.active {
background-color: var(--color-list-item);
}
.item-menu-button {
display: none;
}
`
const AssistantNameRow = styled.div`
@@ -410,31 +427,4 @@ const AssistantName = styled.div`
font-size: 13px;
`
const MenuButton = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
min-width: 22px;
height: 22px;
min-height: 22px;
border-radius: 11px;
position: absolute;
background-color: var(--color-background);
right: 9px;
top: 6px;
padding: 0 5px;
border: 0.5px solid var(--color-border);
`
const TopicCount = styled.div`
color: var(--color-text);
font-size: 10px;
border-radius: 10px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`
export default memo(AssistantItem)

View File

@@ -1,13 +1,8 @@
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Assistant, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Segmented as AntSegmented, SegmentedProps } from 'antd'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FC } from 'react'
import styled from 'styled-components'
import Assistants from './AssistantsTab'
@@ -15,51 +10,19 @@ import Settings from './SettingsTab'
import Topics from './TopicsTab'
interface Props {
tab: Tab
activeAssistant: Assistant
activeTopic: Topic
setActiveAssistant: (assistant: Assistant) => void
setActiveTopic: (topic: Topic) => void
position: 'left' | 'right'
forceToSeeAllTab?: boolean
style?: React.CSSProperties
}
type Tab = 'assistants' | 'topic' | 'settings'
let _tab: any = ''
const HomeTabs: FC<Props> = ({
activeAssistant,
activeTopic,
setActiveAssistant,
setActiveTopic,
position,
forceToSeeAllTab,
style
}) => {
const HomeTabs: FC<Props> = ({ tab, activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, style }) => {
const { addAssistant } = useAssistants()
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
const { topicPosition } = useSettings()
const { defaultAssistant } = useDefaultAssistant()
const { showTopics, toggleShowTopics } = useShowTopics()
const { t } = useTranslation()
const borderStyle = '0.5px solid var(--color-border)'
const border =
position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle, borderTopLeftRadius: 0 }
if (position === 'left' && topicPosition === 'left') {
_tab = tab
}
const showTab = !(position === 'left' && topicPosition === 'right')
const assistantTab = {
label: t('assistants.abbr'),
value: 'assistants'
// icon: <BotIcon size={16} />
}
const onCreateAssistant = async () => {
const assistant = await AddAssistantPopup.show()
@@ -72,68 +35,8 @@ const HomeTabs: FC<Props> = ({
setActiveAssistant(assistant)
}
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => {
showTab && setTab('assistants')
}),
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
showTab && setTab('topic')
}),
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
showTab && setTab('settings')
}),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
showTab && setTab('topic')
if (position === 'left' && topicPosition === 'right') {
toggleShowTopics()
}
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [position, showTab, tab, toggleShowTopics, topicPosition])
useEffect(() => {
if (position === 'right' && topicPosition === 'right' && tab === 'assistants') {
setTab('topic')
}
if (position === 'left' && topicPosition === 'right' && forceToSeeAllTab != true && tab !== 'assistants') {
setTab('assistants')
}
}, [position, tab, topicPosition, forceToSeeAllTab])
return (
<Container style={{ ...border, ...style }} className="home-tabs">
{(showTab || (forceToSeeAllTab == true && !showTopics)) && (
<>
<Segmented
value={tab}
style={{ borderRadius: 50 }}
shape="round"
options={
[
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
? assistantTab
: undefined,
{
label: t('common.topics'),
value: 'topic'
// icon: <MessageSquareQuote size={16} />
},
{
label: t('settings.title'),
value: 'settings'
// icon: <SettingsIcon size={16} />
}
].filter(Boolean) as SegmentedProps['options']
}
onChange={(value) => setTab(value as 'topic' | 'settings')}
block
/>
<Divider />
</>
)}
<Container style={{ ...style }} className="home-tabs">
<TabContent className="home-tabs-content">
{tab === 'assistants' && (
<Assistants
@@ -154,6 +57,7 @@ const HomeTabs: FC<Props> = ({
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
max-width: var(--assistants-width);
min-width: var(--assistants-width);
@@ -173,68 +77,4 @@ const TabContent = styled.div`
overflow-x: hidden;
`
const Divider = styled.div`
border-top: 0.5px solid var(--color-border);
margin-top: 10px;
margin-left: 10px;
margin-right: 10px;
`
const Segmented = styled(AntSegmented)`
font-family: var(--font-family);
&.ant-segmented {
background-color: transparent;
margin: 0 10px;
margin-top: 10px;
padding: 0;
}
.ant-segmented-item {
overflow: hidden;
transition: none !important;
height: 34px;
line-height: 34px;
background-color: transparent;
user-select: none;
border-radius: var(--list-item-border-radius);
box-shadow: none;
}
.ant-segmented-item-selected,
.ant-segmented-item-selected:active {
transition: none !important;
background-color: var(--color-list-item);
}
.ant-segmented-item-label {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
font-size: 13px;
height: 100%;
}
.ant-segmented-item-label[aria-selected='true'] {
color: var(--color-text);
}
.icon-business-smart-assistant {
margin-right: -2px;
}
.ant-segmented-thumb {
transition: none !important;
background-color: var(--color-list-item);
border-radius: var(--list-item-border-radius);
box-shadow: none;
&:hover {
background-color: transparent;
}
}
.ant-segmented-item-label,
.ant-segmented-item-icon {
display: flex;
align-items: center;
}
/* These styles ensure the same appearance as before */
border-radius: 0;
box-shadow: none;
`
export default HomeTabs

View File

@@ -1,4 +1,5 @@
import { CopyOutlined, DeleteOutlined, EditOutlined, RedoOutlined } from '@ant-design/icons'
import { NavbarIcon } from '@renderer/components/app/MainNavbar'
import CustomTag from '@renderer/components/CustomTag'
import Ellipsis from '@renderer/components/Ellipsis'
import { HStack } from '@renderer/components/Layout'
@@ -22,7 +23,6 @@ import styled from 'styled-components'
import CustomCollapse from '../../components/CustomCollapse'
import FileItem from '../files/FileItem'
import { NavbarIcon } from '../home/Navbar'
import KnowledgeSearchPopup from './components/KnowledgeSearchPopup'
import KnowledgeSettingsPopup from './components/KnowledgeSettingsPopup'
import StatusIcon from './components/StatusIcon'

View File

@@ -1,5 +1,5 @@
import { DeleteOutlined, EditOutlined, SettingOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
import DragableList from '@renderer/components/DragableList'
import ListItem from '@renderer/components/ListItem'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
@@ -92,9 +92,9 @@ const KnowledgePage: FC = () => {
return (
<Container>
<Navbar>
<NavbarMain>
<NavbarCenter style={{ borderRight: 'none' }}>{t('knowledge.title')}</NavbarCenter>
</Navbar>
</NavbarMain>
<ContentContainer id="content-container">
<SideNav>
<ScrollContainer>

View File

@@ -9,9 +9,9 @@ import { SettingHelpText } from '@renderer/pages/settings'
import AiProvider from '@renderer/providers/AiProvider'
import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService'
import { getModelUniqId } from '@renderer/services/ModelService'
import { KnowledgeBase, Model } from '@renderer/types'
import { Model } from '@renderer/types'
import { getErrorMessage } from '@renderer/utils/error'
import { Flex, Form, Input, InputNumber, Modal, Select, Slider, Switch } from 'antd'
import { Form, Input, Modal, Select, Slider } from 'antd'
import { find, sortBy } from 'lodash'
import { nanoid } from 'nanoid'
import { useMemo, useRef, useState } from 'react'
@@ -24,8 +24,6 @@ interface ShowParams {
interface FormData {
name: string
model: string
autoDims: boolean | undefined
dimensions: number | undefined
rerankModel: string | undefined
documentCount: number | undefined
}
@@ -37,7 +35,6 @@ interface Props extends ShowParams {
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
const [open, setOpen] = useState(true)
const [loading, setLoading] = useState(false)
const [autoDims, setAutoDims] = useState(true)
const [form] = Form.useForm<FormData>()
const { t } = useTranslation()
const { providers } = useProviders()
@@ -70,8 +67,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
.map((m) => ({
label: m.name,
value: getModelUniqId(m),
providerId: p.id,
modelId: m.id
key: `${p.id}-${m.id}`
}))
}))
.filter((group) => group.options.length > 0)
@@ -111,27 +107,24 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
return
}
if (autoDims || typeof values.dimensions === 'undefined') {
try {
const aiProvider = new AiProvider(provider)
values.dimensions = await aiProvider.getEmbeddingDimensions(selectedEmbeddingModel)
} catch (error) {
console.error('Error getting embedding dimensions:', error)
window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error))
setLoading(false)
return
}
} else if (typeof values.dimensions === 'string') {
// 按理来说不应该是string的但是确实是string
values.dimensions = parseInt(values.dimensions)
const aiProvider = new AiProvider(provider)
let dimensions = 0
try {
dimensions = await aiProvider.getEmbeddingDimensions(selectedEmbeddingModel)
} catch (error) {
console.error('Error getting embedding dimensions:', error)
window.message.error(t('message.error.get_embedding_dimensions') + '\n' + getErrorMessage(error))
setLoading(false)
return
}
const newBase: KnowledgeBase = {
const newBase = {
id: nanoid(),
name: values.name,
model: selectedEmbeddingModel,
rerankModel: selectedRerankModel,
dimensions: values.dimensions,
dimensions,
documentCount: values.documentCount || DEFAULT_KNOWLEDGE_DOCUMENT_COUNT,
items: [],
created_at: Date.now(),
@@ -141,7 +134,7 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
await window.api.knowledgeBase.create(getKnowledgeBaseParams(newBase))
addKnowledgeBase(newBase)
addKnowledgeBase(newBase as any)
setOpen(false)
resolve(newBase)
}
@@ -210,59 +203,11 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
marks={{ 1: '1', 6: t('knowledge.document_count_default'), 30: '30' }}
/>
</Form.Item>
<Form.Item
name="autoDims"
colon={false}
initialValue={true}
layout="horizontal"
label={t('knowledge.dimensions_auto_set')}
tooltip={t('knowledge.dimensions_default')}
style={{ marginBottom: 0, justifyContent: 'space-between' }}>
<Flex justify="flex-end" style={{ marginBottom: '1rem' }}>
<Switch
checked={autoDims}
onClick={() => {
form.setFieldValue('autoDims', !autoDims)
if (!autoDims) {
form.validateFields(['dimensions'])
}
setAutoDims(!autoDims)
}}></Switch>
</Flex>
</Form.Item>
<Form.Item
name="dimensions"
colon={false}
layout="horizontal"
initialValue={undefined}
label={t('knowledge.dimensions')}
tooltip={{ title: t('knowledge.dimensions_size_tooltip') }}
dependencies={['model']}
style={{ display: autoDims ? 'none' : 'block' }}
rules={[
({ getFieldValue }) => ({
validator(_, value) {
if (getFieldValue('autoDims') || value > 0) {
return Promise.resolve()
} else {
return Promise.reject(t('knowledge.dimensions_error_invalid'))
}
}
})
]}>
<InputNumber min={1} style={{ width: '100%' }} placeholder={t('knowledge.dimensions_size_placeholder')} />
</Form.Item>
{!autoDims && (
<SettingHelpText style={{ marginTop: -15, marginBottom: 20 }}>
{t('knowledge.dimensions_set_right')}
</SettingHelpText>
)}
</Form>
</Modal>
)
}
export default class AddKnowledgePopup {
static hide() {
TopView.hide('AddKnowledgePopup')

View File

@@ -187,6 +187,32 @@ const PopupContainer: React.FC<Props> = ({ base: _base, resolve }) => {
</AdvancedSettingsButton>
<div style={{ display: showAdvanced ? 'block' : 'none' }}>
<Form.Item
name="dimensions"
label={t('knowledge.dimensions')}
layout="horizontal"
initialValue={base.dimensions}
tooltip={{ title: t('knowledge.dimensions_size_tooltip') }}
rules={[
{
validator(_, value) {
const maxContext = getEmbeddingMaxContext(base.model.id)
if (value && maxContext && value > maxContext) {
return Promise.reject(
new Error(t('knowledge.dimensions_size_too_large', { max_context: maxContext }))
)
}
return Promise.resolve()
}
}
]}>
<InputNumber
style={{ width: '100%' }}
defaultValue={base.dimensions}
placeholder={t('knowledge.dimensions_size_placeholder')}
disabled={base.model.id !== 'voyage-3-large'}
/>
</Form.Item>
<Form.Item
name="chunkSize"
label={t('knowledge.chunk_size')}

View File

@@ -1,5 +1,6 @@
import { CheckCircleOutlined, QuestionCircleOutlined, WarningOutlined } from '@ant-design/icons'
import { Center, VStack } from '@renderer/components/Layout'
import { SettingDescription, SettingRow, SettingSubtitle } from '@renderer/pages/settings'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setIsBunInstalled, setIsUvInstalled } from '@renderer/store/mcp'
import { Alert, Button } from 'antd'
@@ -8,8 +9,6 @@ import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingDescription, SettingRow, SettingSubtitle } from '..'
interface Props {
mini?: boolean
}
@@ -82,7 +81,7 @@ const InstallNpxUv: FC<Props> = ({ mini = false }) => {
icon={installed ? <CheckCircleOutlined /> : <WarningOutlined />}
className="nodrag"
color={installed ? 'green' : 'danger'}
onClick={() => navigate('/settings/mcp/mcp-install')}
onClick={() => navigate('mcp-install')}
/>
)
}

View File

@@ -3,6 +3,7 @@ import { nanoid } from '@reduxjs/toolkit'
import DragableList from '@renderer/components/DragableList'
import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
import { SettingTitle } from '@renderer/pages/settings'
import { MCPServer } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error'
import { Button, Dropdown, Empty, Switch, Tag } from 'antd'
@@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingTitle } from '..'
import AddMcpServerModal from './AddMcpServerModal'
import EditMcpJsonPopup from './EditMcpJsonPopup'
import SyncServersPopup from './SyncServersPopup'
@@ -36,7 +36,7 @@ const McpServersList: FC = () => {
isActive: false
}
addMCPServer(newServer)
navigate(`/settings/mcp/settings`, { state: { server: newServer } })
navigate(`settings`, { state: { server: newServer } })
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-list' })
}, [addMCPServer, navigate, t])
@@ -50,7 +50,7 @@ const McpServersList: FC = () => {
setIsAddModalVisible(false)
window.message.success({ content: t('settings.mcp.addSuccess'), key: 'mcp-quick-add' })
// Optionally navigate to the new server's settings page
// navigate(`/settings/mcp/settings`, { state: { server } })
// navigate(`/mcp-servers/settings`, { state: { server } })
},
[addMCPServer, t]
)
@@ -119,7 +119,7 @@ const McpServersList: FC = () => {
</ListHeader>
<DragableList style={{ width: '100%' }} list={mcpServers} onUpdate={updateMcpServers}>
{(server: MCPServer) => (
<ServerCard key={server.id} onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}>
<ServerCard key={server.id} onClick={() => navigate(`settings`, { state: { server } })}>
<ServerHeader>
<ServerName>
{server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />}
@@ -148,7 +148,7 @@ const McpServersList: FC = () => {
<Button
icon={<Settings2 size={16} />}
type="text"
onClick={() => navigate(`/settings/mcp/settings`, { state: { server } })}
onClick={() => navigate(`settings`, { state: { server } })}
/>
</StatusIndicator>
</ServerHeader>

View File

@@ -1,7 +1,8 @@
import { DeleteOutlined, SaveOutlined } from '@ant-design/icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
import MCPDescription from '@renderer/pages/settings/MCPSettings/McpDescription'
import MCPDescription from '@renderer/pages/mcp-servers/McpDescription'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '@renderer/pages/settings'
import { MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
import { formatMcpError } from '@renderer/utils/error'
import { Button, Flex, Form, Input, Radio, Select, Switch, Tabs } from 'antd'
@@ -12,7 +13,6 @@ import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingTitle } from '..'
import MCPPromptsSection from './McpPrompt'
import MCPResourcesSection from './McpResource'
import MCPToolsSection from './McpTool'
@@ -319,7 +319,7 @@ const McpSettings: React.FC = () => {
await window.api.mcp.removeServer(server)
deleteMCPServer(server.id)
window.message.success({ content: t('settings.mcp.deleteSuccess'), key: 'mcp-list' })
navigate('/settings/mcp')
navigate('/mcp-servers')
}
})
} catch (error: any) {
@@ -608,7 +608,7 @@ const McpSettings: React.FC = () => {
}
return (
<SettingContainer theme={theme} style={{ width: '100%', paddingTop: 55, backgroundColor: 'transparent' }}>
<SettingContainer theme={theme} style={{ width: '100%', paddingTop: 20, backgroundColor: 'transparent' }}>
<SettingGroup style={{ marginBottom: 0, borderRadius: 'var(--list-item-border-radius)' }}>
<SettingTitle>
<Flex justify="space-between" align="center" gap={5} style={{ marginRight: 10 }}>

View File

@@ -1,7 +1,4 @@
import { NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import { isLinux, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { Button, Dropdown, Menu, type MenuProps } from 'antd'
import { ChevronDown, Search } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@@ -74,29 +71,27 @@ export const McpSettingsNavbar = () => {
}))
return (
<NavbarRight style={{ paddingRight: useFullscreen() ? '12px' : isWindows ? 150 : isLinux ? 120 : 12 }}>
<HStack alignItems="center" gap={5}>
<HStack alignItems="center" gap={5}>
<Button
size="small"
type="text"
onClick={() => navigate('npx-search')}
icon={<Search size={14} />}
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.searchNpx')}
</Button>
<Dropdown menu={{ items: resourceMenuItems }} trigger={['click']}>
<Button
size="small"
type="text"
onClick={() => navigate('/settings/mcp/npx-search')}
icon={<Search size={14} />}
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20 }}>
{t('settings.mcp.searchNpx')}
style={{ fontSize: 13, height: 28, borderRadius: 20, display: 'flex', alignItems: 'center' }}>
{t('settings.mcp.findMore')}
<ChevronDown size={16} />
</Button>
<Dropdown menu={{ items: resourceMenuItems }} trigger={['click']}>
<Button
size="small"
type="text"
className="nodrag"
style={{ fontSize: 13, height: 28, borderRadius: 20, display: 'flex', alignItems: 'center' }}>
{t('settings.mcp.findMore')}
<ChevronDown size={16} />
</Button>
</Dropdown>
<InstallNpxUv mini />
</HStack>
</NavbarRight>
</Dropdown>
<InstallNpxUv mini />
</HStack>
)
}

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