Compare commits

...

19 Commits

Author SHA1 Message Date
dev 50d1342680 fix: add anthropic model to temp and top p check 2025-10-11 20:45:48 +08:00
kangfenmao ee4c310725 feat: update migration logic and increment version for store
- Incremented version in the store configuration from 161 to 162.
- Updated migration logic to handle new provider integration and state adjustments.
- Removed deprecated migration logic for version 161.
2025-10-11 16:17:00 +08:00
kangfenmao a000ff2a1a fix: adjust overflow properties in MessageGroup component
- Changed overflow properties in the GridContainer styled component to improve layout handling. Overflow is now set to hidden for vertical alignment.
2025-10-11 16:07:49 +08:00
kangfenmao 2f9576b2ae feat: remove some minapp and update related configurations
- Introduced new app icon for Stepfun.
- Updated minapps configuration to include Stepfun with its logo and URL.
- Removed Yuewen app from configurations and translations.
- Updated translations for multiple languages to reflect the addition of Stepfun and removal of Yuewen.
- Incremented version in the store configuration and added migration logic for new provider integration.
2025-10-11 16:07:49 +08:00
kangfenmao 92554dd398 chore: update @ai-sdk/google to version 2.0.17 and add corresponding patch 2025-10-11 16:07:49 +08:00
defi-failure 9473ddc762 feature: unified assistant tab (#10590)
* feature: unified assistant tab

* refactor(TagGroup): make TagsContainer component internal by removing export

* refactor(components): migrate styled-components to cn utility classes

Replace styled-components with cn utility classes from @heroui/react for better maintainability and performance

* refactor(AssistantsTab): split AssistantsTab into smaller hooks and components

* fix: click agent item should jump to topic tab

* feat: add AddButton component and refactor usage across tabs

- Introduced a new AddButton component for consistent UI across different tabs.
- Replaced existing button implementations with AddButton in Sessions, Topics, and UnifiedAddButton components.
- Removed unnecessary margin from AssistantsTab's container for improved layout.

---------

Co-authored-by: icarus <eurfelux@gmail.com>
Co-authored-by: kangfenmao <kangfenmao@qq.com>
2025-10-11 16:07:35 +08:00
SuYao 5f469a71f3 fix: update ai-sdk dependencies to latest versions (#10643) 2025-10-11 15:53:30 +08:00
defi-failure 87bac60afc fix: long dir breaks edit agent layout (#10644) 2025-10-11 14:47:45 +08:00
SuYao 704339e835 fix: increase tool call maxCount (#10642) 2025-10-11 14:21:18 +08:00
ABucket c8ab7180ba fix: Provider icons are not displayed after selecting SiliconFlow in the "images" page (#10620) 2025-10-11 12:48:26 +08:00
ABucket 11757546c3 fix: Quick Assistant fails to correctly inject variables in prompts (#10617) 2025-10-11 12:45:25 +08:00
ABucket 420b9ec2f2 fix: AI_TypeValidationError when calling Ling-1T model (#10622) 2025-10-11 12:45:00 +08:00
beyondkmp 1c73271e33 fix: support gpt-5-codex for github copilot (#10587)
* fix: support gpt-5-codex for github copilot

- Added patch for @ai-sdk/openai to version 2.0.42 in package.json and yarn.lock.
- Updated editor version for Copilot from v1.97.2 to v1.104.1 in OpenAIBaseClient and providerConfig.
- Enhanced provider configuration to support new model options for Copilot.

* fix: streamline Copilot header management

- Replaced individual header assignments for Copilot with centralized constants in OpenAIBaseClient and providerConfig.
- Enhanced provider configuration to conditionally set response mode for Copilot models, improving routing logic.

* update aisdk

* delete patch

* 🤖 chore: integrate Copilot SDK provider

* use a plugin

* udpate dependency

* fix: remove unused Copilot default headers from OpenAIBaseClient

- Eliminated the import and usage of COPILOT_DEFAULT_HEADERS to streamline header management in the OpenAIBaseClient class.

* update yarn

* fix lint

* format code

* feat: enhance web search tool types in webSearchPlugin

- Added type normalization for web search tools to improve type safety and clarity.
- Updated WebSearchToolInputSchema and WebSearchToolOutputSchema to use normalized types for better consistency across the plugin.
2025-10-11 10:18:09 +08:00
ABucket acdbe6b9ed feat: allow right click to create note and folder (#10523)
* feat: allow right click to create note and folder

* fix: duplicate menu for notes or folder

* fix: create notes in folder when a folder is selected
2025-10-10 16:58:14 +01:00
beyondkmp 6c201228d9 feat: support search in mini app page (#10609)
*  feat: add webview find-in-page overlay

* 🐛 fix: reset webview search on tab change

* fix clear search issue

* 🐛 fix: rebind webview search events

* 🐛 fix: disable spellcheck in search input

* fix spellcheck

* 🐛 fix: webview search can now reopen after closing

Fixed an issue where the search overlay couldn't be reopened after closing.
The openSearch callback was unnecessarily depending on webviewRef.current,
causing event listener rebinding issues. Removed the redundant webviewRef
check as isWebviewReady is sufficient to ensure webview readiness.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Payne Fu <payne@Paynes-Mac-mini.rcoffice.ringcentral.com>
Co-authored-by: Payne Fu <payne@Paynes-MBP.rcoffice.ringcentral.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-10 07:00:45 -07:00
Tristan Zhang 73b2a375ad fix: insert reasoning block before the content block (#10545)
fix: always insert reasoning block before the content block
2025-10-09 22:32:13 +08:00
Chen Tao 89bb830b60 fix: knowledge base not delete and websearch rag error (#10595)
* fix: knowledge base not  delete

* fix: websearch rag error

* chore: add comment
2025-10-09 22:29:52 +08:00
Tristan Zhang 2399db4944 fix: adding multiple keys to the zhipu model service is not detected properly (#10583) 2025-10-09 20:40:46 +08:00
beyondkmp 62774b34d3 feat: add updating dialog in render (#10569)
* feat: replace update dialog handling with quit and install functionality

* refactor: remove App_ShowUpdateDialog and implement App_QuitAndInstall in IpcChannel
* update ipc.ts to handle quit and install action
* modify AppUpdater to include quitAndInstall method
* adjust preload index to invoke new quit and install action
* enhance AboutSettings to manage update dialog state and trigger quit and install

* fix(AboutSettings): handle null update info in update dialog state management

* fix(UpdateDialog): improve error handling during update installation and enhance release notes processing

* fix(AppUpdater): remove redundant assignment of releaseInfo after update download

* fix(IpcChannel): remove UpdateDownloadedCancelled enum value

* format code

* fix(UpdateDialog): enhance installation process with loading state and error handling

* update i18n

* fix(i18n): Auto update translations for PR #10569

* feat(UpdateAppButton): integrate UpdateDialog and update button functionality for better user experience

* fix(UpdateDialog): update installation handler to support async operation and ensure modal closes after installation

* refactor(AppUpdater.test): remove deprecated formatReleaseNotes tests to streamline test suite

* refactor(update-dialog): simplify dialog close handling

Replace onOpenChange with onClose prop to directly handle dialog closing
Remove redundant handleClose function and simplify button onPress handler

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: icarus <eurfelux@gmail.com>
2025-10-09 15:58:24 +08:00
74 changed files with 2384 additions and 680 deletions
@@ -1,8 +1,8 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..17e109b7778cbebb904f1919e768d21a2833d965 100644
index b957cb824faa79cf01ba3a504f221870bd8e306a..4d71d30f655775d61537d9d8b73f6e17d41fa67e 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -448,7 +448,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
@@ -452,7 +452,7 @@ function convertToGoogleGenerativeAIMessages(prompt, options) {
// src/get-model-path.ts
function getModelPath(modelId) {
+7 -6
View File
@@ -99,10 +99,10 @@
"@agentic/exa": "^7.3.3",
"@agentic/searxng": "^7.3.3",
"@agentic/tavily": "^7.3.3",
"@ai-sdk/amazon-bedrock": "^3.0.29",
"@ai-sdk/google-vertex": "^3.0.33",
"@ai-sdk/mistral": "^2.0.17",
"@ai-sdk/perplexity": "^2.0.11",
"@ai-sdk/amazon-bedrock": "^3.0.35",
"@ai-sdk/google-vertex": "^3.0.40",
"@ai-sdk/mistral": "^2.0.19",
"@ai-sdk/perplexity": "^2.0.13",
"@ant-design/v5-patch-for-react-19": "^1.0.3",
"@anthropic-ai/sdk": "^0.41.0",
"@anthropic-ai/vertex-sdk": "patch:@anthropic-ai/vertex-sdk@npm%3A0.11.4#~/.yarn/patches/@anthropic-ai-vertex-sdk-npm-0.11.4-c19cb41edb.patch",
@@ -152,6 +152,7 @@
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/sdk-trace-node": "^2.0.0",
"@opentelemetry/sdk-trace-web": "^2.0.0",
"@opeoginni/github-copilot-openai-compatible": "0.1.18",
"@playwright/test": "^1.52.0",
"@radix-ui/react-context-menu": "^2.2.16",
"@reduxjs/toolkit": "^2.2.5",
@@ -219,7 +220,7 @@
"@viz-js/lang-dot": "^1.0.5",
"@viz-js/viz": "^3.14.0",
"@xyflow/react": "^12.4.4",
"ai": "^5.0.59",
"ai": "^5.0.68",
"antd": "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
@@ -380,7 +381,7 @@
"undici": "6.21.2",
"vite": "npm:rolldown-vite@latest",
"tesseract.js@npm:*": "patch:tesseract.js@npm%3A6.0.1#~/.yarn/patches/tesseract.js-npm-6.0.1-2562a7e46d.patch",
"@ai-sdk/google@npm:2.0.14": "patch:@ai-sdk/google@npm%3A2.0.14#~/.yarn/patches/@ai-sdk-google-npm-2.0.14-376d8b03cc.patch"
"@ai-sdk/google@npm:2.0.17": "patch:@ai-sdk/google@npm%3A2.0.17#~/.yarn/patches/@ai-sdk-google-npm-2.0.17-fd88491de4.patch"
},
"packageManager": "yarn@4.9.1",
"lint-staged": {
+7 -7
View File
@@ -36,14 +36,14 @@
"ai": "^5.0.26"
},
"dependencies": {
"@ai-sdk/anthropic": "^2.0.22",
"@ai-sdk/azure": "^2.0.42",
"@ai-sdk/deepseek": "^1.0.20",
"@ai-sdk/openai": "^2.0.42",
"@ai-sdk/openai-compatible": "^1.0.19",
"@ai-sdk/anthropic": "^2.0.27",
"@ai-sdk/azure": "^2.0.49",
"@ai-sdk/deepseek": "^1.0.23",
"@ai-sdk/openai": "^2.0.48",
"@ai-sdk/openai-compatible": "^1.0.22",
"@ai-sdk/provider": "^2.0.0",
"@ai-sdk/provider-utils": "^3.0.10",
"@ai-sdk/xai": "^2.0.23",
"@ai-sdk/provider-utils": "^3.0.12",
"@ai-sdk/xai": "^2.0.26",
"zod": "^4.1.5"
},
"devDependencies": {
@@ -1,7 +1,7 @@
import { anthropic } from '@ai-sdk/anthropic'
import { google } from '@ai-sdk/google'
import { openai } from '@ai-sdk/openai'
import { InferToolInput, InferToolOutput } from 'ai'
import { InferToolInput, InferToolOutput, type Tool } from 'ai'
import { ProviderOptionsMap } from '../../../options/types'
import { OpenRouterSearchConfig } from './openrouter'
@@ -15,6 +15,13 @@ export type AnthropicSearchConfig = NonNullable<Parameters<typeof anthropic.tool
export type GoogleSearchConfig = NonNullable<Parameters<typeof google.tools.googleSearch>[0]>
export type XAISearchConfig = NonNullable<ProviderOptionsMap['xai']['searchParameters']>
type NormalizeTool<T> = T extends Tool<infer INPUT, infer OUTPUT> ? Tool<INPUT, OUTPUT> : Tool<any, any>
type AnthropicWebSearchTool = NormalizeTool<ReturnType<typeof anthropic.tools.webSearch_20250305>>
type OpenAIWebSearchTool = NormalizeTool<ReturnType<typeof openai.tools.webSearch>>
type OpenAIChatWebSearchTool = NormalizeTool<ReturnType<typeof openai.tools.webSearchPreview>>
type GoogleWebSearchTool = NormalizeTool<ReturnType<typeof google.tools.googleSearch>>
/**
* 插件初始化时接收的完整配置对象
*
@@ -59,7 +66,7 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchPluginConfig = {
export type WebSearchToolOutputSchema = {
// Anthropic 工具 - 手动定义
anthropic: InferToolOutput<ReturnType<typeof anthropic.tools.webSearch_20250305>>
anthropic: InferToolOutput<AnthropicWebSearchTool>
// OpenAI 工具 - 基于实际输出
// TODO: 上游定义不规范,是unknown
@@ -82,8 +89,8 @@ export type WebSearchToolOutputSchema = {
}
export type WebSearchToolInputSchema = {
anthropic: InferToolInput<ReturnType<typeof anthropic.tools.webSearch_20250305>>
openai: InferToolInput<ReturnType<typeof openai.tools.webSearch>>
google: InferToolInput<ReturnType<typeof google.tools.googleSearch>>
'openai-chat': InferToolInput<ReturnType<typeof openai.tools.webSearchPreview>>
anthropic: InferToolInput<AnthropicWebSearchTool>
openai: InferToolInput<OpenAIWebSearchTool>
google: InferToolInput<GoogleWebSearchTool>
'openai-chat': InferToolInput<OpenAIChatWebSearchTool>
}
+1 -2
View File
@@ -5,8 +5,8 @@ export enum IpcChannel {
App_SetLanguage = 'app:set-language',
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
App_ShowUpdateDialog = 'app:show-update-dialog',
App_CheckForUpdate = 'app:check-for-update',
App_QuitAndInstall = 'app:quit-and-install',
App_Reload = 'app:reload',
App_Quit = 'app:quit',
App_Info = 'app:info',
@@ -234,7 +234,6 @@ export enum IpcChannel {
// events
BackupProgress = 'backup-progress',
ThemeUpdated = 'theme:updated',
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
RestoreProgress = 'restore-progress',
UpdateError = 'update-error',
UpdateAvailable = 'update-available',
+1 -1
View File
@@ -142,7 +142,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
// Update
ipcMain.handle(IpcChannel.App_ShowUpdateDialog, () => appUpdater.showUpdateDialog(mainWindow))
ipcMain.handle(IpcChannel.App_QuitAndInstall, () => appUpdater.quitAndInstall())
// language
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
+4 -65
View File
@@ -1,17 +1,15 @@
import { loggerService } from '@logger'
import { isWin } from '@main/constant'
import { getIpCountry } from '@main/utils/ipService'
import { locales } from '@main/utils/locales'
import { generateUserAgent } from '@main/utils/systemInfo'
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
import { app, BrowserWindow, dialog, net } from 'electron'
import { app, net } from 'electron'
import { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater'
import path from 'path'
import semver from 'semver'
import icon from '../../../build/icon.png?asset'
import { configManager } from './ConfigManager'
import { windowService } from './WindowService'
@@ -26,7 +24,6 @@ const LANG_MARKERS = {
export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater
private releaseInfo: UpdateInfo | undefined
private cancellationToken: CancellationToken = new CancellationToken()
private updateCheckResult: UpdateCheckResult | null = null
@@ -66,7 +63,6 @@ export default class AppUpdater {
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo)
this.releaseInfo = processedReleaseInfo
logger.info('update downloaded', processedReleaseInfo)
})
@@ -247,37 +243,9 @@ export default class AppUpdater {
}
}
public async showUpdateDialog(mainWindow: BrowserWindow) {
if (!this.releaseInfo) {
return
}
const locale = locales[configManager.getLanguage()]
const { update: updateLocale } = locale.translation
let detail = this.formatReleaseNotes(this.releaseInfo.releaseNotes)
if (detail === '') {
detail = updateLocale.noReleaseNotes
}
dialog
.showMessageBox({
type: 'info',
title: updateLocale.title,
icon,
message: updateLocale.message.replace('{{version}}', this.releaseInfo.version),
detail,
buttons: [updateLocale.later, updateLocale.install],
defaultId: 1,
cancelId: 0
})
.then(({ response }) => {
if (response === 1) {
app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall())
} else {
mainWindow.webContents.send(IpcChannel.UpdateDownloadedCancelled)
}
})
public quitAndInstall() {
app.isQuitting = true
setImmediate(() => autoUpdater.quitAndInstall())
}
/**
@@ -349,38 +317,9 @@ export default class AppUpdater {
return processedInfo
}
/**
* Format release notes for display
* @param releaseNotes - Release notes in various formats
* @returns Formatted string for display
*/
private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string {
if (!releaseNotes) {
return ''
}
if (typeof releaseNotes === 'string') {
// Check if it contains multi-language markers
if (this.hasMultiLanguageMarkers(releaseNotes)) {
return this.parseMultiLangReleaseNotes(releaseNotes)
}
return releaseNotes
}
if (Array.isArray(releaseNotes)) {
return releaseNotes.map((note) => note.note).join('\n')
}
return ''
}
}
interface GithubReleaseInfo {
draft: boolean
prerelease: boolean
tag_name: string
}
interface ReleaseNoteInfo {
readonly version: string
readonly note: string | null
}
+9 -3
View File
@@ -29,7 +29,7 @@ import Reranker from '@main/knowledge/reranker/Reranker'
import { fileStorage } from '@main/services/FileStorage'
import { windowService } from '@main/services/WindowService'
import { getDataPath } from '@main/utils'
import { getAllFiles } from '@main/utils/file'
import { getAllFiles, sanitizeFilename } from '@main/utils/file'
import { TraceMethod } from '@mcp-trace/trace-core'
import { MB } from '@shared/config/constant'
import type { LoaderReturn } from '@shared/config/types'
@@ -147,11 +147,16 @@ class KnowledgeService {
}
}
private getDbPath = (id: string): string => {
// 消除网络搜索requestI d中的特殊字符
return path.join(this.storageDir, sanitizeFilename(id, '_'))
}
/**
* Delete knowledge base file
*/
private deleteKnowledgeFile = (id: string): boolean => {
const dbPath = path.join(this.storageDir, id)
const dbPath = this.getDbPath(id)
if (fs.existsSync(dbPath)) {
try {
fs.rmSync(dbPath, { recursive: true })
@@ -244,7 +249,8 @@ class KnowledgeService {
dimensions
})
try {
const libSqlDb = new LibSqlDb({ path: path.join(this.storageDir, id) })
const dbPath = this.getDbPath(id)
const libSqlDb = new LibSqlDb({ path: dbPath })
// Save database instance for later closing
this.dbInstances.set(id, libSqlDb)
@@ -274,46 +274,4 @@ describe('AppUpdater', () => {
expect(result.releaseNotes).toBeNull()
})
})
describe('formatReleaseNotes', () => {
it('should format string release notes with markers', () => {
vi.mocked(configManager.getLanguage).mockReturnValue('en-US')
const notes = `<!--LANG:en-->English<!--LANG:zh-CN-->中文<!--LANG:END-->`
const result = (appUpdater as any).formatReleaseNotes(notes)
expect(result).toBe('English')
})
it('should format string release notes without markers', () => {
const notes = 'Simple notes'
const result = (appUpdater as any).formatReleaseNotes(notes)
expect(result).toBe('Simple notes')
})
it('should format array release notes', () => {
const notes = [
{ version: '1.0.0', note: 'Note 1' },
{ version: '1.0.1', note: 'Note 2' }
]
const result = (appUpdater as any).formatReleaseNotes(notes)
expect(result).toBe('Note 1\nNote 2')
})
it('should handle null release notes', () => {
const result = (appUpdater as any).formatReleaseNotes(null)
expect(result).toBe('')
})
it('should handle undefined release notes', () => {
const result = (appUpdater as any).formatReleaseNotes(undefined)
expect(result).toBe('')
})
})
})
+2 -2
View File
@@ -51,7 +51,7 @@ const api = {
setProxy: (proxy: string | undefined, bypassRules?: string) =>
ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules),
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog),
quitAndInstall: () => ipcRenderer.invoke(IpcChannel.App_QuitAndInstall),
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable),
setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages),
@@ -223,7 +223,7 @@ const api = {
create: (base: KnowledgeBaseParams, context?: SpanContext) =>
tracedInvoke(IpcChannel.KnowledgeBase_Create, context, base),
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Reset, base),
delete: (base: KnowledgeBaseParams, id: string) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Delete, base, id),
delete: (id: string) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Delete, id),
add: ({
base,
item,
@@ -166,9 +166,7 @@ export abstract class OpenAIBaseClient<
baseURL: this.getBaseURL(),
defaultHeaders: {
...this.defaultHeaders(),
...this.provider.extra_headers,
...(this.provider.id === 'copilot' ? { 'editor-version': 'vscode/1.97.2' } : {}),
...(this.provider.id === 'copilot' ? { 'copilot-vision-request': 'true' } : {})
...this.provider.extra_headers
}
}) as TSdkInstance
}
@@ -23,6 +23,7 @@ import { CherryWebSearchConfig } from '@renderer/store/websearch'
import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
import { replacePromptVariables } from '@renderer/utils/prompt'
import type { ModelMessage, Tool } from 'ai'
import { stepCountIs } from 'ai'
@@ -159,14 +160,14 @@ export async function buildStreamTextParams(
abortSignal: options.requestOptions?.signal,
headers: options.requestOptions?.headers,
providerOptions,
stopWhen: stepCountIs(10),
stopWhen: stepCountIs(20),
maxRetries: 0
}
if (tools) {
params.tools = tools
}
if (assistant.prompt) {
params.system = assistant.prompt
params.system = await replacePromptVariables(assistant.prompt, model.name)
}
logger.debug('params', params)
return {
@@ -0,0 +1,89 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@renderer/services/LoggerService', () => ({
loggerService: {
withContext: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn()
})
}
}))
vi.mock('@renderer/services/AssistantService', () => ({
getProviderByModel: vi.fn()
}))
vi.mock('@renderer/store', () => ({
default: {
getState: () => ({ copilot: { defaultHeaders: {} } })
}
}))
import type { Model, Provider } from '@renderer/types'
import { COPILOT_DEFAULT_HEADERS, COPILOT_EDITOR_VERSION, isCopilotResponsesModel } from '../constants'
import { providerToAiSdkConfig } from '../providerConfig'
const createWindowKeyv = () => {
const store = new Map<string, string>()
return {
get: (key: string) => store.get(key),
set: (key: string, value: string) => {
store.set(key, value)
}
}
}
const createCopilotProvider = (): Provider => ({
id: 'copilot',
type: 'openai',
name: 'GitHub Copilot',
apiKey: 'test-key',
apiHost: 'https://api.githubcopilot.com',
models: [],
isSystem: true
})
const createModel = (id: string, name = id): Model => ({
id,
name,
provider: 'copilot',
group: 'copilot'
})
describe('Copilot responses routing', () => {
beforeEach(() => {
;(globalThis as any).window = {
...(globalThis as any).window,
keyv: createWindowKeyv()
}
})
it('detects official GPT-5 Codex identifiers case-insensitively', () => {
expect(isCopilotResponsesModel(createModel('gpt-5-codex', 'gpt-5-codex'))).toBe(true)
expect(isCopilotResponsesModel(createModel('GPT-5-CODEX', 'GPT-5-CODEX'))).toBe(true)
expect(isCopilotResponsesModel(createModel('gpt-5-codex', 'custom-name'))).toBe(true)
expect(isCopilotResponsesModel(createModel('custom-id', 'custom-name'))).toBe(false)
})
it('configures gpt-5-codex with the Copilot provider', () => {
const provider = createCopilotProvider()
const config = providerToAiSdkConfig(provider, createModel('gpt-5-codex', 'GPT-5-CODEX'))
expect(config.providerId).toBe('github-copilot-openai-compatible')
expect(config.options.headers?.['Editor-Version']).toBe(COPILOT_EDITOR_VERSION)
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
expect(config.options.headers?.['copilot-vision-request']).toBe('true')
})
it('uses the Copilot provider for other models and keeps headers', () => {
const provider = createCopilotProvider()
const config = providerToAiSdkConfig(provider, createModel('gpt-4'))
expect(config.providerId).toBe('github-copilot-openai-compatible')
expect(config.options.headers?.['Editor-Version']).toBe(COPILOT_DEFAULT_HEADERS['Editor-Version'])
expect(config.options.headers?.['Copilot-Integration-Id']).toBe(COPILOT_DEFAULT_HEADERS['Copilot-Integration-Id'])
})
})
@@ -0,0 +1,25 @@
import type { Model } from '@renderer/types'
export const COPILOT_EDITOR_VERSION = 'vscode/1.104.1'
export const COPILOT_PLUGIN_VERSION = 'copilot-chat/0.26.7'
export const COPILOT_INTEGRATION_ID = 'vscode-chat'
export const COPILOT_USER_AGENT = 'GitHubCopilotChat/0.26.7'
export const COPILOT_DEFAULT_HEADERS = {
'Copilot-Integration-Id': COPILOT_INTEGRATION_ID,
'User-Agent': COPILOT_USER_AGENT,
'Editor-Version': COPILOT_EDITOR_VERSION,
'Editor-Plugin-Version': COPILOT_PLUGIN_VERSION,
'editor-version': COPILOT_EDITOR_VERSION,
'editor-plugin-version': COPILOT_PLUGIN_VERSION,
'copilot-vision-request': 'true'
} as const
// Models that require the OpenAI Responses endpoint when routed through GitHub Copilot (#10560)
const COPILOT_RESPONSES_MODEL_IDS = ['gpt-5-codex']
export function isCopilotResponsesModel(model: Model): boolean {
const normalizedId = model.id?.trim().toLowerCase()
const normalizedName = model.name?.trim().toLowerCase()
return COPILOT_RESPONSES_MODEL_IDS.some((target) => normalizedId === target || normalizedName === target)
}
+2 -1
View File
@@ -28,7 +28,8 @@ const STATIC_PROVIDER_MAPPING: Record<string, ProviderId> = {
gemini: 'google', // Google Gemini -> google
'azure-openai': 'azure', // Azure OpenAI -> azure
'openai-response': 'openai', // OpenAI Responses -> openai
grok: 'xai' // Grok -> xai
grok: 'xai', // Grok -> xai
copilot: 'github-copilot-openai-compatible'
}
/**
@@ -21,6 +21,7 @@ import { formatApiHost } from '@renderer/utils/api'
import { cloneDeep, trim } from 'lodash'
import { aihubmixProviderCreator, newApiResolverCreator, vertexAnthropicProviderCreator } from './config'
import { COPILOT_DEFAULT_HEADERS } from './constants'
import { getAiSdkProviderId } from './factory'
const logger = loggerService.withContext('ProviderConfigProcessor')
@@ -109,6 +110,9 @@ function formatProviderApiHost(provider: Provider): Provider {
if (!formatted.anthropicApiHost) {
formatted.anthropicApiHost = formatted.apiHost
}
} else if (formatted.id === 'copilot') {
const trimmed = trim(formatted.apiHost)
formatted.apiHost = trimmed.endsWith('/') ? trimmed.slice(0, -1) : trimmed
} else if (formatted.type === 'gemini') {
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
} else {
@@ -151,6 +155,26 @@ export function providerToAiSdkConfig(
baseURL: trim(actualProvider.apiHost),
apiKey: getRotatedApiKey(actualProvider)
}
const isCopilotProvider = actualProvider.id === 'copilot'
if (isCopilotProvider) {
const storedHeaders = store.getState().copilot.defaultHeaders ?? {}
const options = ProviderConfigFactory.fromProvider('github-copilot-openai-compatible', baseConfig, {
headers: {
...COPILOT_DEFAULT_HEADERS,
...storedHeaders,
...actualProvider.extra_headers
},
name: actualProvider.id,
includeUsage: true
})
return {
providerId: 'github-copilot-openai-compatible',
options
}
}
// 处理OpenAI模式
const extraOptions: any = {}
if (actualProvider.type === 'openai-response' && !isOpenAIChatCompletionOnlyModel(model)) {
@@ -172,15 +196,6 @@ export function providerToAiSdkConfig(
}
}
}
// copilot
if (actualProvider.id === 'copilot') {
extraOptions.headers = {
...extraOptions.headers,
'editor-version': 'vscode/1.97.2',
'copilot-vision-request': 'true'
}
}
// azure
if (aiSdkProviderId === 'azure' || actualProvider.type === 'azure-openai') {
extraOptions.apiVersion = actualProvider.apiVersion
@@ -229,7 +244,6 @@ export function providerToAiSdkConfig(
}
}
// 如果AI SDK支持该provider,使用原生配置
if (hasProviderConfig(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
const options = ProviderConfigFactory.fromProvider(aiSdkProviderId, baseConfig, extraOptions)
return {
@@ -277,9 +291,17 @@ export async function prepareSpecialProviderConfig(
) {
switch (provider.id) {
case 'copilot': {
const defaultHeaders = store.getState().copilot.defaultHeaders
const { token } = await window.api.copilot.getToken(defaultHeaders)
const defaultHeaders = store.getState().copilot.defaultHeaders ?? {}
const headers = {
...COPILOT_DEFAULT_HEADERS,
...defaultHeaders
}
const { token } = await window.api.copilot.getToken(headers)
config.options.apiKey = token
config.options.headers = {
...headers,
...config.options.headers
}
break
}
case 'cherryai': {
@@ -32,6 +32,14 @@ export const NEW_PROVIDER_CONFIGS: ProviderConfig[] = [
supportsImageGeneration: true,
aliases: ['vertexai-anthropic']
},
{
id: 'github-copilot-openai-compatible',
name: 'GitHub Copilot OpenAI Compatible',
import: () => import('@opeoginni/github-copilot-openai-compatible'),
creatorFunctionName: 'createGitHubCopilotOpenAICompatible',
supportsImageGeneration: false,
aliases: ['copilot', 'github-copilot']
},
{
id: 'bedrock',
name: 'Amazon Bedrock',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

@@ -549,7 +549,7 @@ const MinappPopupContainer: React.FC = () => {
{/* 在所有小程序中显示GoogleLoginTip */}
<GoogleLoginTip isReady={isReady} currentUrl={currentUrl} currentAppId={currentMinappId} />
{!isReady && (
<EmptyView>
<EmptyView style={{ backgroundColor: 'var(--color-background-soft)' }}>
<Avatar
src={currentAppInfo?.logo}
size={80}
@@ -10,6 +10,7 @@ interface ShowParams {
providerId: string
title?: string
showHealthCheck?: boolean
providerType?: 'llm' | 'webSearch' | 'preprocess'
}
interface Props extends ShowParams {
@@ -19,7 +20,7 @@ interface Props extends ShowParams {
/**
* API Key 列表弹窗容器组件
*/
const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealthCheck = true }) => {
const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealthCheck = true, providerType }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
@@ -32,14 +33,20 @@ const PopupContainer: React.FC<Props> = ({ providerId, title, resolve, showHealt
}
const ListComponent = useMemo(() => {
if (isWebSearchProviderId(providerId)) {
return <WebSearchApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
const type =
providerType ||
(isWebSearchProviderId(providerId) ? 'webSearch' : isPreprocessProviderId(providerId) ? 'preprocess' : 'llm')
switch (type) {
case 'webSearch':
return <WebSearchApiKeyList providerId={providerId as any} showHealthCheck={showHealthCheck} />
case 'preprocess':
return <DocPreprocessApiKeyList providerId={providerId as any} showHealthCheck={showHealthCheck} />
case 'llm':
default:
return <LlmApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}
if (isPreprocessProviderId(providerId)) {
return <DocPreprocessApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}
return <LlmApiKeyList providerId={providerId} showHealthCheck={showHealthCheck} />
}, [providerId, showHealthCheck])
}, [providerId, showHealthCheck, providerType])
return (
<Modal
@@ -0,0 +1,101 @@
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ScrollShadow } from '@heroui/react'
import { loggerService } from '@logger'
import { handleSaveData } from '@renderer/store'
import { ReleaseNoteInfo, UpdateInfo } from 'builder-util-runtime'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
const logger = loggerService.withContext('UpdateDialog')
interface UpdateDialogProps {
isOpen: boolean
onClose: () => void
releaseInfo: UpdateInfo | null
}
const UpdateDialog: React.FC<UpdateDialogProps> = ({ isOpen, onClose, releaseInfo }) => {
const { t } = useTranslation()
const [isInstalling, setIsInstalling] = useState(false)
useEffect(() => {
if (isOpen && releaseInfo) {
logger.info('Update dialog opened', { version: releaseInfo.version })
}
}, [isOpen, releaseInfo])
const handleInstall = async () => {
setIsInstalling(true)
try {
await handleSaveData()
await window.api.quitAndInstall()
} catch (error) {
logger.error('Failed to save data before update', error as Error)
setIsInstalling(false)
window.toast.error(t('update.saveDataError'))
}
}
const releaseNotes = releaseInfo?.releaseNotes
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="2xl"
scrollBehavior="inside"
classNames={{
base: 'max-h-[85vh]',
header: 'border-b border-divider',
footer: 'border-t border-divider'
}}>
<ModalContent>
{(onModalClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
<h3 className="font-semibold text-lg">{t('update.title')}</h3>
<p className="text-default-500 text-small">
{t('update.message').replace('{{version}}', releaseInfo?.version || '')}
</p>
</ModalHeader>
<ModalBody>
<ScrollShadow className="max-h-[450px]" hideScrollBar>
<div className="markdown rounded-lg bg-default-50 p-4">
<Markdown>
{typeof releaseNotes === 'string'
? releaseNotes
: Array.isArray(releaseNotes)
? releaseNotes
.map((note: ReleaseNoteInfo) => note.note)
.filter(Boolean)
.join('\n\n')
: t('update.noReleaseNotes')}
</Markdown>
</div>
</ScrollShadow>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onModalClose} isDisabled={isInstalling}>
{t('update.later')}
</Button>
<Button
color="primary"
onPress={async () => {
await handleInstall()
onModalClose()
}}
isLoading={isInstalling}>
{t('update.install')}
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
)
}
export default UpdateDialog
+4 -28
View File
@@ -22,8 +22,6 @@ import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?
import GoogleAppLogo from '@renderer/assets/images/apps/google.svg?url'
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
import GrokXAppLogo from '@renderer/assets/images/apps/grok-x.png?url'
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.webp?url'
import LambdaChatLogo from '@renderer/assets/images/apps/lambdachat.webp?url'
import LeChatLogo from '@renderer/assets/images/apps/lechat.png?url'
@@ -32,13 +30,13 @@ import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
import MonicaLogo from '@renderer/assets/images/apps/monica.webp?url'
import n8nLogo from '@renderer/assets/images/apps/n8n.svg?url'
import NamiAiLogo from '@renderer/assets/images/apps/nm.png?url'
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm-search.webp?url'
import NotebookLMAppLogo from '@renderer/assets/images/apps/notebooklm.svg?url'
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?url'
import StepfunAppLogo from '@renderer/assets/images/apps/stepfun.png?url'
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
@@ -46,7 +44,6 @@ import WPSLingXiLogo from '@renderer/assets/images/apps/wpslingxi.webp?url'
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
import YouLogo from '@renderer/assets/images/apps/you.jpg?url'
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.webp?url'
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
import ZaiAppLogo from '@renderer/assets/images/apps/zai.png?url'
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
@@ -150,9 +147,9 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
},
{
id: 'stepfun',
name: i18n.t('minapps.yuewen'),
url: 'https://yuewen.cn/chats/new',
logo: YuewenAppLogo,
name: i18n.t('minapps.stepfun'),
url: 'https://stepfun.com',
logo: StepfunAppLogo,
bodered: true
},
{
@@ -263,13 +260,6 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
url: 'https://www.tiangong.cn/',
bodered: true
},
{
id: 'hugging-chat',
name: 'HuggingChat',
logo: HuggingChatLogo,
url: 'https://huggingface.co/chat/',
bodered: true
},
{
id: 'Felo',
name: 'Felo',
@@ -297,13 +287,6 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
url: 'https://bot.n.cn/',
bodered: true
},
{
id: 'nm-search',
name: i18n.t('minapps.nami-ai-search'),
logo: NamiAiSearchLogo,
url: 'https://www.n.cn/',
bodered: true
},
{
id: 'thinkany',
name: 'ThinkAny',
@@ -314,13 +297,6 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
padding: 5
}
},
{
id: 'hika',
name: 'Hika',
logo: HikaLogo,
url: 'https://hika.fyi/',
bodered: true
},
{
id: 'github-copilot',
name: 'GitHub Copilot',
+1 -1
View File
@@ -25,7 +25,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
// Default quick assistant model
glm45FlashModel
],
// cherryin: [],
cherryin: [],
vertexai: [],
'302ai': [
{
+2 -1
View File
@@ -96,7 +96,8 @@ export function isNotSupportTemperatureAndTopP(model: Model): boolean {
if (
(isOpenAIReasoningModel(model) && !isOpenAIOpenWeightModel(model)) ||
isOpenAIChatCompletionOnlyModel(model) ||
isQwenMTModel(model)
isQwenMTModel(model) ||
isAnthropicModel(model)
) {
return true
}
+21 -21
View File
@@ -82,16 +82,16 @@ export const CHERRYAI_PROVIDER: SystemProvider = {
}
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
// cherryin: {
// id: 'cherryin',
// name: 'CherryIN',
// type: 'openai',
// apiKey: '',
// apiHost: 'https://open.cherryin.ai',
// models: [],
// isSystem: true,
// enabled: true
// },
cherryin: {
id: 'cherryin',
name: 'CherryIN',
type: 'openai',
apiKey: '',
apiHost: 'https://open.cherryin.net',
models: [],
isSystem: true,
enabled: true
},
silicon: {
id: 'silicon',
name: 'Silicon',
@@ -742,17 +742,17 @@ type ProviderUrls = {
}
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
// cherryin: {
// api: {
// url: 'https://open.cherryin.ai'
// },
// websites: {
// official: 'https://open.cherryin.ai',
// apiKey: 'https://open.cherryin.ai/console/token',
// docs: 'https://open.cherryin.ai',
// models: 'https://open.cherryin.ai/pricing'
// }
// },
cherryin: {
api: {
url: 'https://open.cherryin.net'
},
websites: {
official: 'https://open.cherryin.ai',
apiKey: 'https://open.cherryin.ai/console/token',
docs: 'https://open.cherryin.ai',
models: 'https://open.cherryin.ai/pricing'
}
},
ph8: {
api: {
url: 'https://ph8.co'
+1 -1
View File
@@ -360,7 +360,7 @@ export const useKnowledgeBases = () => {
const deleteKnowledgeBase = (baseId: string) => {
const base = bases.find((b) => b.id === baseId)
if (!base) return
dispatch(deleteBase({ baseId, baseParams: getKnowledgeBaseParams(base) }))
dispatch(deleteBase({ baseId }))
// remove assistant knowledge_base
const _assistants = assistants.map((assistant) => {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "Nami AI Search",
"qwen": "Qwen",
"sensechat": "SenseChat",
"stepfun": "Stepfun",
"tencent-yuanbao": "Yuanbao",
"tiangong-ai": "Skywork",
"wanzhi": "Wanzhi",
"wenxin": "ERNIE",
"wps-copilot": "WPS Copilot",
"xiaoyi": "Xiaoyi",
"yuewen": "Yuewen",
"zhihu": "Zhihu"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "Later",
"message": "New version {{version}} is ready, do you want to install it now?",
"noReleaseNotes": "No release notes",
"saveDataError": "Failed to save data, please try again.",
"title": "Update"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "纳米AI搜索",
"qwen": "通义千问",
"sensechat": "商量",
"stepfun": "阶跃AI",
"tencent-yuanbao": "腾讯元宝",
"tiangong-ai": "天工AI",
"wanzhi": "万知",
"wenxin": "文心一言",
"wps-copilot": "WPS灵犀",
"xiaoyi": "小艺",
"yuewen": "跃问",
"zhihu": "知乎直答"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "稍后",
"message": "发现新版本 {{version}},是否立即安装?",
"noReleaseNotes": "暂无更新日志",
"saveDataError": "保存数据失败,请重试",
"title": "更新提示"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "納米AI搜索",
"qwen": "通義千問",
"sensechat": "商量",
"stepfun": "階躍AI",
"tencent-yuanbao": "騰訊元寶",
"tiangong-ai": "天工AI",
"wanzhi": "萬知",
"wenxin": "文心一言",
"wps-copilot": "WPS靈犀",
"xiaoyi": "小藝",
"yuewen": "躍問",
"zhihu": "知乎直答"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "稍後",
"message": "新版本 {{version}} 已準備就緒,是否立即安裝?",
"noReleaseNotes": "暫無更新日誌",
"saveDataError": "保存數據失敗,請重試",
"title": "更新提示"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "Nami AI Search",
"qwen": "Qwen",
"sensechat": "SenseChat",
"stepfun": "Stepfun",
"tencent-yuanbao": "Yuanbao",
"tiangong-ai": "Skywork",
"wanzhi": "Wanzhi",
"wenxin": "ERNIE",
"wps-copilot": "WPS Copilot",
"xiaoyi": "Xiaoyi",
"yuewen": "Yuewen",
"zhihu": "Zhihu"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "Μετά",
"message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;",
"noReleaseNotes": "Χωρίς σημειώσεις",
"saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά",
"title": "Ενημέρωση"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "Nami AI Search",
"qwen": "Qwen",
"sensechat": "SenseChat",
"stepfun": "Stepfun",
"tencent-yuanbao": "Yuanbao",
"tiangong-ai": "Skywork",
"wanzhi": "Wanzhi",
"wenxin": "ERNIE",
"wps-copilot": "WPS Copilot",
"xiaoyi": "Xiaoyi",
"yuewen": "Yuewen",
"zhihu": "Zhihu"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "Más tarde",
"message": "Nueva versión {{version}} disponible, ¿desea instalarla ahora?",
"noReleaseNotes": "Sin notas de la versión",
"saveDataError": "Error al guardar los datos, inténtalo de nuevo",
"title": "Actualización"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "Nami AI Search",
"qwen": "Qwen",
"sensechat": "SenseChat",
"stepfun": "Stepfun",
"tencent-yuanbao": "Yuanbao",
"tiangong-ai": "Skywork",
"wanzhi": "Wanzhi",
"wenxin": "ERNIE",
"wps-copilot": "WPS Copilot",
"xiaoyi": "Xiaoyi",
"yuewen": "Yuewen",
"zhihu": "Zhihu"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "Plus tard",
"message": "Nouvelle version {{version}} disponible, voulez-vous l'installer maintenant ?",
"noReleaseNotes": "Aucune note de version",
"saveDataError": "Échec de la sauvegarde des données, veuillez réessayer",
"title": "Mise à jour"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "Nami AI Search",
"qwen": "通義千問",
"sensechat": "SenseChat",
"stepfun": "Stepfun",
"tencent-yuanbao": "騰訊元宝",
"tiangong-ai": "Skywork",
"wanzhi": "万知",
"wenxin": "ERNIE",
"wps-copilot": "WPS Copilot",
"xiaoyi": "小藝",
"yuewen": "躍問",
"zhihu": "知乎直答"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "後で",
"message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?",
"noReleaseNotes": "暫無更新日誌",
"saveDataError": "データの保存に失敗しました。もう一度お試しください。",
"title": "更新"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "Nami AI Search",
"qwen": "Qwen",
"sensechat": "SenseChat",
"stepfun": "Stepfun",
"tencent-yuanbao": "Yuanbao",
"tiangong-ai": "Skywork",
"wanzhi": "Wanzhi",
"wenxin": "ERNIE",
"wps-copilot": "WPS Copilot",
"xiaoyi": "Xiaoyi",
"yuewen": "Yuewen",
"zhihu": "Zhihu"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "Mais tarde",
"message": "Nova versão {{version}} disponível, deseja instalar agora?",
"noReleaseNotes": "Sem notas de versão",
"saveDataError": "Falha ao salvar os dados, tente novamente",
"title": "Atualização"
},
"warning": {
+2 -1
View File
@@ -1806,13 +1806,13 @@
"nami-ai-search": "Nami AI Search",
"qwen": "Qwen",
"sensechat": "SenseChat",
"stepfun": "Stepfun",
"tencent-yuanbao": "Tencent Yuanbao",
"tiangong-ai": "Skywork",
"wanzhi": "Wanzhi",
"wenxin": "ERNIE",
"wps-copilot": "WPS Copilot",
"xiaoyi": "Xiaoyi",
"yuewen": "Yuewen",
"zhihu": "Zhihu"
},
"miniwindow": {
@@ -4641,6 +4641,7 @@
"later": "Позже",
"message": "Новая версия {{version}} готова, установить сейчас?",
"noReleaseNotes": "Нет заметок об обновлении",
"saveDataError": "Ошибка сохранения данных, повторите попытку",
"title": "Обновление"
},
"warning": {
@@ -359,8 +359,7 @@ const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number }
&.vertical {
grid-template-columns: repeat(1, minmax(0, 1fr));
gap: 8px;
overflow-y: auto;
overflow-x: hidden;
overflow: hidden;
}
&.grid {
grid-template-columns: repeat(
@@ -1,10 +1,26 @@
import { Alert, Spinner } from '@heroui/react'
import Scrollbar from '@renderer/components/Scrollbar'
import { Assistant } from '@renderer/types'
import { FC, useRef } from 'react'
import { useAgents } from '@renderer/hooks/agents/useAgents'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistantPresets } from '@renderer/hooks/useAssistantPresets'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAssistantsTabSortType } from '@renderer/hooks/useStore'
import { useTags } from '@renderer/hooks/useTags'
import { useAppDispatch } from '@renderer/store'
import { addIknowAction } from '@renderer/store/runtime'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { AgentSection } from './components/AgentSection'
import Assistants from './components/Assistants'
import UnifiedAddButton from './components/UnifiedAddButton'
import { UnifiedList } from './components/UnifiedList'
import { UnifiedTagGroups } from './components/UnifiedTagGroups'
import { useActiveAgent } from './hooks/useActiveAgent'
import { useUnifiedGrouping } from './hooks/useUnifiedGrouping'
import { useUnifiedItems } from './hooks/useUnifiedItems'
import { useUnifiedSorting } from './hooks/useUnifiedSorting'
interface AssistantsTabProps {
activeAssistant: Assistant
@@ -13,12 +29,143 @@ interface AssistantsTabProps {
onCreateDefaultAssistant: () => void
}
const ALERT_KEY = 'enable_api_server_to_use_agent'
const AssistantsTab: FC<AssistantsTabProps> = (props) => {
const { activeAssistant, setActiveAssistant, onCreateAssistant, onCreateDefaultAssistant } = props
const containerRef = useRef<HTMLDivElement>(null)
const { t } = useTranslation()
const { apiServer } = useSettings()
const { iknow, chat } = useRuntime()
const dispatch = useAppDispatch()
// Agent related hooks
const { agents, deleteAgent, isLoading: agentsLoading, error: agentsError } = useAgents()
const { activeAgentId } = chat
const { setActiveAgentId } = useActiveAgent()
// Assistant related hooks
const { assistants, removeAssistant, copyAssistant, updateAssistants } = useAssistants()
const { addAssistantPreset } = useAssistantPresets()
const { collapsedTags, toggleTagCollapse } = useTags()
const { assistantsTabSortType = 'list', setAssistantsTabSortType } = useAssistantsTabSortType()
const [dragging, setDragging] = useState(false)
// Unified items management
const { unifiedItems, handleUnifiedListReorder } = useUnifiedItems({
agents,
assistants,
apiServerEnabled: apiServer.enabled,
agentsLoading,
agentsError,
updateAssistants
})
// Sorting
const { sortByPinyinAsc, sortByPinyinDesc } = useUnifiedSorting({
unifiedItems,
updateAssistants
})
// Grouping
const { groupedUnifiedItems, handleUnifiedGroupReorder } = useUnifiedGrouping({
unifiedItems,
assistants,
agents,
apiServerEnabled: apiServer.enabled,
agentsLoading,
agentsError,
updateAssistants
})
useEffect(() => {
if (!agentsLoading && agents.length > 0 && !activeAgentId && apiServer.enabled) {
setActiveAgentId(agents[0].id)
}
}, [agentsLoading, agents, activeAgentId, setActiveAgentId, apiServer.enabled])
const onDeleteAssistant = useCallback(
(assistant: Assistant) => {
const remaining = assistants.filter((a) => a.id !== assistant.id)
if (assistant.id === activeAssistant?.id) {
const newActive = remaining[remaining.length - 1]
newActive ? setActiveAssistant(newActive) : onCreateDefaultAssistant()
}
removeAssistant(assistant.id)
},
[activeAssistant, assistants, removeAssistant, setActiveAssistant, onCreateDefaultAssistant]
)
const handleSortByChange = useCallback(
(sortType: AssistantsSortType) => {
setAssistantsTabSortType(sortType)
},
[setAssistantsTabSortType]
)
return (
<Container className="assistants-tab" ref={containerRef}>
<AgentSection />
<Assistants {...props} />
{!apiServer.enabled && !iknow[ALERT_KEY] && (
<Alert
color="warning"
title={t('agent.warning.enable_server')}
isClosable
onClose={() => {
dispatch(addIknowAction(ALERT_KEY))
}}
/>
)}
<UnifiedAddButton onCreateAssistant={onCreateAssistant} />
{agentsLoading && <Spinner />}
{apiServer.enabled && agentsError && <Alert color="danger" title={t('agent.list.error.failed')} />}
{assistantsTabSortType === 'tags' ? (
<UnifiedTagGroups
groupedItems={groupedUnifiedItems}
activeAssistantId={activeAssistant.id}
activeAgentId={activeAgentId}
sortBy={assistantsTabSortType}
collapsedTags={collapsedTags}
onGroupReorder={handleUnifiedGroupReorder}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}
onToggleTagCollapse={toggleTagCollapse}
onAssistantSwitch={setActiveAssistant}
onAssistantDelete={onDeleteAssistant}
onAgentDelete={deleteAgent}
onAgentPress={setActiveAgentId}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
sortByPinyinAsc={sortByPinyinAsc}
sortByPinyinDesc={sortByPinyinDesc}
/>
) : (
<UnifiedList
items={unifiedItems}
activeAssistantId={activeAssistant.id}
activeAgentId={activeAgentId}
sortBy={assistantsTabSortType}
onReorder={handleUnifiedListReorder}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}
onAssistantSwitch={setActiveAssistant}
onAssistantDelete={onDeleteAssistant}
onAgentDelete={deleteAgent}
onAgentPress={setActiveAgentId}
addPreset={addAssistantPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
sortByPinyinAsc={sortByPinyinAsc}
sortByPinyinDesc={sortByPinyinDesc}
/>
)}
{!dragging && <div style={{ minHeight: 10 }}></div>}
</Container>
)
}
@@ -27,7 +174,6 @@ const Container = styled(Scrollbar)`
display: flex;
flex-direction: column;
padding: 10px;
margin-top: 3px;
`
export default AssistantsTab
@@ -0,0 +1,24 @@
import { Button, ButtonProps, cn } from '@heroui/react'
import { PlusIcon } from 'lucide-react'
import { FC } from 'react'
interface Props extends ButtonProps {
children: React.ReactNode
}
const AddButton: FC<Props> = ({ children, className, ...props }) => {
return (
<Button
{...props}
onPress={props.onPress}
className={cn(
'h-9 w-[calc(var(--assistants-width)-20px)] justify-start rounded-full bg-transparent px-3 text-[13px] text-[var(--color-text-2)] hover:bg-[var(--color-list-item)]',
className
)}
startContent={<PlusIcon size={16} className="shrink-0" />}>
{children}
</Button>
)
}
export default AddButton
@@ -1,11 +1,14 @@
import { Button, Chip, cn } from '@heroui/react'
import { cn } from '@heroui/react'
import { DeleteIcon, EditIcon } from '@renderer/components/Icons'
import { useSessions } from '@renderer/hooks/agents/useSessions'
import { useSettings } from '@renderer/hooks/useSettings'
import AgentSettingsPopup from '@renderer/pages/settings/AgentSettings/AgentSettingsPopup'
import { AgentLabel } from '@renderer/pages/settings/AgentSettings/shared'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { AgentEntity } from '@renderer/types'
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@renderer/ui/context-menu'
import { FC, memo } from 'react'
import { Bot } from 'lucide-react'
import { FC, memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
// const logger = loggerService.withContext('AgentItem')
@@ -20,81 +23,107 @@ interface AgentItemProps {
const AgentItem: FC<AgentItemProps> = ({ agent, isActive, onDelete, onPress }) => {
const { t } = useTranslation()
const { sessions } = useSessions(agent.id)
const { clickAssistantToShowTopic, topicPosition } = useSettings()
const handlePress = useCallback(() => {
// Show session sidebar if setting is enabled (reusing the assistant setting for consistency)
if (clickAssistantToShowTopic) {
if (topicPosition === 'left') {
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
}
}
onPress()
}, [clickAssistantToShowTopic, topicPosition, onPress])
return (
<>
<ContextMenu modal={false}>
<ContextMenuTrigger>
<ButtonContainer onPress={onPress} className={isActive ? 'active' : ''}>
<AssistantNameRow className="name flex w-full justify-between" title={agent.name ?? agent.id}>
<ContextMenu modal={false}>
<ContextMenuTrigger>
<Container onClick={handlePress} isActive={isActive}>
<AssistantNameRow className="name" title={agent.name ?? agent.id}>
<AgentNameWrapper>
<AgentLabel agent={agent} />
{isActive && (
<Chip
variant="bordered"
size="sm"
radius="full"
className="aspect-square h-5 w-5 items-center justify-center border-[0.5px] bg-background text-[10px]">
{sessions.length}
</Chip>
)}
</AssistantNameRow>
</ButtonContainer>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
key="edit"
onClick={async () => {
// onOpen()
await AgentSettingsPopup.show({
agentId: agent.id
})
}}>
<EditIcon size={14} />
{t('common.edit')}
</ContextMenuItem>
<ContextMenuItem
key="delete"
className="text-danger"
onClick={() => {
window.modal.confirm({
title: t('agent.delete.title'),
content: t('agent.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => onDelete(agent)
})
}}>
<DeleteIcon size={14} className="lucide-custom text-danger" />
<span className="text-danger">{t('common.delete')}</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{/* <AgentModal isOpen={isOpen} onClose={onClose} agent={agent} /> */}
</>
</AgentNameWrapper>
</AssistantNameRow>
<MenuButton>
{isActive ? <SessionCount>{sessions.length}</SessionCount> : <Bot size={12} className="text-primary" />}
</MenuButton>
</Container>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
key="edit"
onClick={async () => {
// onOpen()
await AgentSettingsPopup.show({
agentId: agent.id
})
}}>
<EditIcon size={14} />
{t('common.edit')}
</ContextMenuItem>
<ContextMenuItem
key="delete"
className="text-danger"
onClick={() => {
window.modal.confirm({
title: t('agent.delete.title'),
content: t('agent.delete.content'),
centered: true,
okButtonProps: { danger: true },
onOk: () => onDelete(agent)
})
}}>
<DeleteIcon size={14} className="lucide-custom text-danger" />
<span className="text-danger">{t('common.delete')}</span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
const ButtonContainer: React.FC<React.ComponentProps<typeof Button>> = ({ className, children, ...props }) => (
<Button
{...props}
export const Container: React.FC<{ isActive?: boolean } & React.HTMLAttributes<HTMLDivElement>> = ({
className,
isActive,
...props
}) => (
<div
className={cn(
'relative mb-2 flex h-[37px] flex-row justify-between p-2.5',
'rounded-[var(--list-item-border-radius)]',
'border-[0.5px] border-transparent',
'w-[calc(var(--assistants-width)_-_20px)]',
'bg-transparent hover:bg-[var(--color-list-item)] hover:shadow-sm',
'cursor-pointer',
className?.includes('active') && 'bg-[var(--color-list-item)] shadow-sm',
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
className
)}>
{children}
</Button>
)}
{...props}
/>
)
const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
export const AssistantNameRow: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-[13px] text-[var(--color-text)]', className)}
{...props}
/>
)
export const AgentNameWrapper: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div className={cn('min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap', className)} {...props} />
)
export const MenuButton: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
className={cn(
'absolute top-[6px] right-[9px] flex h-[22px] min-h-[22px] w-[22px] flex-row items-center justify-center rounded-full border border-[var(--color-border)] bg-[var(--color-background)] px-[5px]',
className
)}
{...props}
/>
)
export const SessionCount: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ className, ...props }) => (
<div
className={cn(
'flex flex-row items-center justify-center rounded-full text-[10px] text-[var(--color-text)]',
className
)}
{...props}
className={cn('text-[13px] text-[var(--color-text)]', 'flex flex-row items-center gap-2', className)}
/>
)
@@ -1,39 +0,0 @@
import { Alert } from '@heroui/react'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { addIknowAction } from '@renderer/store/runtime'
import { useTranslation } from 'react-i18next'
import { Agents } from './Agents'
import { SectionName } from './SectionName'
const ALERT_KEY = 'enable_api_server_to_use_agent'
export const AgentSection = () => {
const { t } = useTranslation()
const { apiServer } = useSettings()
const { iknow } = useRuntime()
const dispatch = useAppDispatch()
if (!apiServer.enabled) {
if (iknow[ALERT_KEY]) return null
return (
<Alert
color="warning"
title={t('agent.warning.enable_server')}
isClosable
onClose={() => {
dispatch(addIknowAction(ALERT_KEY))
}}
/>
)
}
return (
<div className="agents-tab mb-2 h-full w-full">
<SectionName name={t('common.agent_other')} />
<Agents />
</div>
)
}
@@ -1,3 +1,4 @@
import { cn } from '@heroui/react'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import EmojiIcon from '@renderer/components/EmojiIcon'
import { CopyIcon, DeleteIcon, EditIcon } from '@renderer/components/Icons'
@@ -28,9 +29,8 @@ import {
Tag,
Tags
} from 'lucide-react'
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'
import { FC, memo, PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import * as tinyPinyin from 'tiny-pinyin'
import AssistantTagsPopup from './AssistantTagsPopup'
@@ -46,6 +46,8 @@ interface AssistantItemProps {
copyAssistant: (assistant: Assistant) => void
onTagClick?: (tag: string) => void
handleSortByChange?: (sortType: AssistantsSortType) => void
sortByPinyinAsc?: () => void
sortByPinyinDesc?: () => void
}
const AssistantItem: FC<AssistantItemProps> = ({
@@ -56,7 +58,9 @@ const AssistantItem: FC<AssistantItemProps> = ({
onDelete,
addPreset,
copyAssistant,
handleSortByChange
handleSortByChange,
sortByPinyinAsc: externalSortByPinyinAsc,
sortByPinyinDesc: externalSortByPinyinDesc
}) => {
const { t } = useTranslation()
const { allTags } = useTags()
@@ -78,14 +82,19 @@ const AssistantItem: FC<AssistantItemProps> = ({
setIsPending(hasPending)
}, [isActive, assistant.topics])
const sortByPinyinAsc = useCallback(() => {
// Local sort functions
const localSortByPinyinAsc = useCallback(() => {
updateAssistants(sortAssistantsByPinyin(assistants, true))
}, [assistants, updateAssistants])
const sortByPinyinDesc = useCallback(() => {
const localSortByPinyinDesc = useCallback(() => {
updateAssistants(sortAssistantsByPinyin(assistants, false))
}, [assistants, updateAssistants])
// Use external sort functions if provided, otherwise use local ones
const sortByPinyinAsc = externalSortByPinyinAsc || localSortByPinyinAsc
const sortByPinyinDesc = externalSortByPinyinDesc || localSortByPinyinDesc
const menuItems = useMemo(
() =>
getMenuItems({
@@ -145,7 +154,7 @@ const AssistantItem: FC<AssistantItemProps> = ({
menu={{ items: menuItems }}
trigger={['contextMenu']}
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
<Container onClick={handleSwitch} isActive={isActive}>
<AssistantNameRow className="name" title={fullAssistantName}>
{assistantIconType === 'model' ? (
<ModelAvatar
@@ -380,65 +389,75 @@ function getMenuItems({
]
}
const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 8px;
height: 37px;
position: relative;
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);
cursor: pointer;
const Container = ({
children,
isActive,
className,
...props
}: PropsWithChildren<{ isActive?: boolean } & React.HTMLAttributes<HTMLDivElement>>) => (
<div
{...props}
className={cn(
'relative flex h-[37px] w-[calc(var(--assistants-width)-20px)] cursor-pointer flex-row justify-between rounded-[var(--list-item-border-radius)] border-[0.5px] border-transparent px-2 hover:bg-[var(--color-list-item-hover)]',
isActive && 'bg-[var(--color-list-item)] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]',
className
)}>
{children}
</div>
)
&:hover {
background-color: var(--color-list-item-hover);
}
&.active {
background-color: var(--color-list-item);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
`
const AssistantNameRow = ({
children,
className,
...props
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
<div
{...props}
className={cn('flex min-w-0 flex-1 flex-row items-center gap-2 text-[13px] text-[var(--color-text)]', className)}>
{children}
</div>
)
const AssistantNameRow = styled.div`
color: var(--color-text);
font-size: 13px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
`
const AssistantName = ({
children,
className,
...props
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
<div
{...props}
className={cn('min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[13px]', className)}>
{children}
</div>
)
const AssistantName = styled.div`
font-size: 13px;
`
const MenuButton = ({
children,
className,
...props
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
<div
{...props}
className={cn(
'absolute top-[6px] right-[9px] flex h-[22px] min-h-[22px] min-w-[22px] flex-row items-center justify-center rounded-[11px] border-[0.5px] border-[var(--color-border)] bg-[var(--color-background)] px-[5px]',
className
)}>
{children}
</div>
)
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;
`
const TopicCount = ({
children,
className,
...props
}: PropsWithChildren<{} & React.HTMLAttributes<HTMLDivElement>>) => (
<div
{...props}
className={cn(
'flex flex-row items-center justify-center rounded-[10px] text-[10px] text-[var(--color-text)]',
className
)}>
{children}
</div>
)
export default memo(AssistantItem)
@@ -1,4 +1,4 @@
import { Alert, Button, Spinner } from '@heroui/react'
import { Alert, Spinner } from '@heroui/react'
import { DynamicVirtualList } from '@renderer/components/VirtualList'
import { useAgent } from '@renderer/hooks/agents/useAgent'
import { useSessions } from '@renderer/hooks/agents/useSessions'
@@ -13,10 +13,10 @@ import {
import { CreateSessionForm } from '@renderer/types'
import { buildAgentSessionTopicId } from '@renderer/utils/agentSession'
import { AnimatePresence, motion } from 'framer-motion'
import { Plus } from 'lucide-react'
import { memo, useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import AddButton from './AddButton'
import SessionItem from './SessionItem'
// const logger = loggerService.withContext('SessionsTab')
@@ -115,12 +115,9 @@ const Sessions: React.FC<SessionsProps> = ({ agentId }) => {
transition={{ duration: 0.3 }}
className="sessions-tab flex h-full w-full flex-col p-2">
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}>
<Button
onPress={handleCreateSession}
className="mb-2 w-full justify-start bg-transparent text-foreground-500 hover:bg-accent">
<Plus size={16} className="mr-1 shrink-0" />
<AddButton onPress={handleCreateSession} className="mb-2">
{t('agent.session.add.title')}
</Button>
</AddButton>
</motion.div>
<AnimatePresence>
{/* h-9 */}
@@ -0,0 +1,63 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons'
import { cn } from '@heroui/react'
import { Tooltip } from 'antd'
import { FC, ReactNode } from 'react'
interface TagGroupProps {
tag: string
isCollapsed: boolean
onToggle: (tag: string) => void
showTitle?: boolean
children: ReactNode
}
export const TagGroup: FC<TagGroupProps> = ({ tag, isCollapsed, onToggle, showTitle = true, children }) => {
return (
<TagsContainer>
{showTitle && (
<GroupTitle onClick={() => onToggle(tag)}>
<Tooltip title={tag}>
<GroupTitleName>
{isCollapsed ? (
<RightOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
) : (
<DownOutlined style={{ fontSize: '10px', marginRight: '5px' }} />
)}
{tag}
</GroupTitleName>
</Tooltip>
<GroupTitleDivider />
</GroupTitle>
)}
{!isCollapsed && <div>{children}</div>}
</TagsContainer>
)
}
const TagsContainer: FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, ...props }) => (
<div className={cn('flex flex-col gap-2')} {...props}>
{children}
</div>
)
const GroupTitle: FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, ...props }) => (
<div
className={cn(
'my-1 flex h-6 cursor-pointer flex-row items-center justify-between font-medium text-[var(--color-text-2)] text-xs'
)}
{...props}>
{children}
</div>
)
const GroupTitleName: FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, ...props }) => (
<div
className={cn('mr-1 box-border flex max-w-[50%] truncate px-1 text-[13px] text-[var(--color-text)] leading-6')}
{...props}>
{children}
</div>
)
const GroupTitleDivider: FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div className={cn('flex-1 border-[var(--color-border)] border-t')} {...props} />
)
@@ -41,7 +41,6 @@ import {
PackagePlus,
PinIcon,
PinOffIcon,
PlusIcon,
Save,
Sparkles,
UploadIcon,
@@ -52,6 +51,8 @@ import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
import AddButton from './AddButton'
interface Props {
assistant: Assistant
activeTopic: Topic
@@ -497,13 +498,12 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
className="topics-tab"
list={sortedTopics}
onUpdate={updateTopics}
style={{ height: '100%', padding: '13px 0 10px 10px' }}
style={{ height: '100%', padding: '11px 0 10px 10px' }}
itemContainerStyle={{ paddingBottom: '8px' }}
header={
<AddTopicButton onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
<PlusIcon size={16} />
<AddButton onPress={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)} className="mb-2">
{t('chat.add.topic.title')}
</AddTopicButton>
</AddButton>
}>
{(topic) => {
const isActive = topic.id === activeTopic?.id
@@ -740,31 +740,6 @@ const FulfilledIndicator = styled.div.attrs({
background-color: var(--color-status-success);
`
const AddTopicButton = styled.div`
display: flex;
align-items: center;
gap: 6px;
width: calc(100% - 10px);
padding: 7px 12px;
margin-bottom: 8px;
background: transparent;
color: var(--color-text-2);
font-size: 13px;
border-radius: var(--list-item-border-radius);
cursor: pointer;
transition: all 0.2s;
margin-top: -5px;
&:hover {
background-color: var(--color-list-item-hover);
color: var(--color-text-1);
}
.anticon {
font-size: 12px;
}
`
const TopicPromptText = styled.div`
color: var(--color-text-2);
font-size: 12px;
@@ -0,0 +1,61 @@
import { Button, Popover, PopoverContent, PopoverTrigger } from '@heroui/react'
import { AgentModal } from '@renderer/components/Popups/agent/AgentModal'
import { Bot, MessageSquare } from 'lucide-react'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AddButton from './AddButton'
interface UnifiedAddButtonProps {
onCreateAssistant: () => void
}
const UnifiedAddButton: FC<UnifiedAddButtonProps> = ({ onCreateAssistant }) => {
const { t } = useTranslation()
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [isAgentModalOpen, setIsAgentModalOpen] = useState(false)
const handleAddAssistant = () => {
setIsPopoverOpen(false)
onCreateAssistant()
}
const handleAddAgent = () => {
setIsPopoverOpen(false)
setIsAgentModalOpen(true)
}
return (
<div className="mb-1">
<Popover
isOpen={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
placement="bottom"
classNames={{ content: 'p-0 min-w-[200px]' }}>
<PopoverTrigger>
<AddButton>{t('chat.add.assistant.title')}</AddButton>
</PopoverTrigger>
<PopoverContent>
<div className="flex w-full flex-col gap-1 p-1">
<Button
onPress={handleAddAssistant}
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
startContent={<MessageSquare size={16} className="shrink-0" />}>
{t('chat.add.assistant.title')}
</Button>
<Button
onPress={handleAddAgent}
className="w-full justify-start bg-transparent hover:bg-[var(--color-list-item)]"
startContent={<Bot size={16} className="shrink-0" />}>
{t('agent.add.title')}
</Button>
</div>
</PopoverContent>
</Popover>
<AgentModal isOpen={isAgentModalOpen} onClose={() => setIsAgentModalOpen(false)} />
</div>
)
}
export default UnifiedAddButton
@@ -0,0 +1,108 @@
import { DraggableList } from '@renderer/components/DraggableList'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { FC, useCallback } from 'react'
import { UnifiedItem } from '../hooks/useUnifiedItems'
import AgentItem from './AgentItem'
import AssistantItem from './AssistantItem'
interface UnifiedListProps {
items: UnifiedItem[]
activeAssistantId: string
activeAgentId: string | null
sortBy: AssistantsSortType
onReorder: (newList: UnifiedItem[]) => void
onDragStart: () => void
onDragEnd: () => void
onAssistantSwitch: (assistant: Assistant) => void
onAssistantDelete: (assistant: Assistant) => void
onAgentDelete: (agentId: string) => void
onAgentPress: (agentId: string) => void
addPreset: (assistant: Assistant) => void
copyAssistant: (assistant: Assistant) => void
onCreateDefaultAssistant: () => void
handleSortByChange: (sortType: AssistantsSortType) => void
sortByPinyinAsc: () => void
sortByPinyinDesc: () => void
}
export const UnifiedList: FC<UnifiedListProps> = (props) => {
const {
items,
activeAssistantId,
activeAgentId,
sortBy,
onReorder,
onDragStart,
onDragEnd,
onAssistantSwitch,
onAssistantDelete,
onAgentDelete,
onAgentPress,
addPreset,
copyAssistant,
onCreateDefaultAssistant,
handleSortByChange,
sortByPinyinAsc,
sortByPinyinDesc
} = props
const renderUnifiedItem = useCallback(
(item: UnifiedItem) => {
if (item.type === 'agent') {
return (
<AgentItem
key={`agent-${item.data.id}`}
agent={item.data}
isActive={item.data.id === activeAgentId}
onDelete={() => onAgentDelete(item.data.id)}
onPress={() => onAgentPress(item.data.id)}
/>
)
} else {
return (
<AssistantItem
key={`assistant-${item.data.id}`}
assistant={item.data}
isActive={item.data.id === activeAssistantId}
sortBy={sortBy}
onSwitch={onAssistantSwitch}
onDelete={onAssistantDelete}
addPreset={addPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
sortByPinyinAsc={sortByPinyinAsc}
sortByPinyinDesc={sortByPinyinDesc}
/>
)
}
},
[
activeAgentId,
activeAssistantId,
sortBy,
onAssistantSwitch,
onAssistantDelete,
onAgentDelete,
onAgentPress,
addPreset,
copyAssistant,
onCreateDefaultAssistant,
handleSortByChange,
sortByPinyinAsc,
sortByPinyinDesc
]
)
return (
<DraggableList
list={items}
itemKey={(item) => `${item.type}-${item.data.id}`}
onUpdate={onReorder}
onDragStart={onDragStart}
onDragEnd={onDragEnd}>
{renderUnifiedItem}
</DraggableList>
)
}
@@ -0,0 +1,132 @@
import { DraggableList } from '@renderer/components/DraggableList'
import { Assistant, AssistantsSortType } from '@renderer/types'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { UnifiedItem } from '../hooks/useUnifiedItems'
import AgentItem from './AgentItem'
import AssistantItem from './AssistantItem'
import { TagGroup } from './TagGroup'
interface GroupedItems {
tag: string
items: UnifiedItem[]
}
interface UnifiedTagGroupsProps {
groupedItems: GroupedItems[]
activeAssistantId: string
activeAgentId: string | null
sortBy: AssistantsSortType
collapsedTags: Record<string, boolean>
onGroupReorder: (tag: string, newList: UnifiedItem[]) => void
onDragStart: () => void
onDragEnd: () => void
onToggleTagCollapse: (tag: string) => void
onAssistantSwitch: (assistant: Assistant) => void
onAssistantDelete: (assistant: Assistant) => void
onAgentDelete: (agentId: string) => void
onAgentPress: (agentId: string) => void
addPreset: (assistant: Assistant) => void
copyAssistant: (assistant: Assistant) => void
onCreateDefaultAssistant: () => void
handleSortByChange: (sortType: AssistantsSortType) => void
sortByPinyinAsc: () => void
sortByPinyinDesc: () => void
}
export const UnifiedTagGroups: FC<UnifiedTagGroupsProps> = (props) => {
const {
groupedItems,
activeAssistantId,
activeAgentId,
sortBy,
collapsedTags,
onGroupReorder,
onDragStart,
onDragEnd,
onToggleTagCollapse,
onAssistantSwitch,
onAssistantDelete,
onAgentDelete,
onAgentPress,
addPreset,
copyAssistant,
onCreateDefaultAssistant,
handleSortByChange,
sortByPinyinAsc,
sortByPinyinDesc
} = props
const { t } = useTranslation()
const renderUnifiedItem = useCallback(
(item: UnifiedItem) => {
if (item.type === 'agent') {
return (
<AgentItem
key={`agent-${item.data.id}`}
agent={item.data}
isActive={item.data.id === activeAgentId}
onDelete={() => onAgentDelete(item.data.id)}
onPress={() => onAgentPress(item.data.id)}
/>
)
} else {
return (
<AssistantItem
key={`assistant-${item.data.id}`}
assistant={item.data}
isActive={item.data.id === activeAssistantId}
sortBy={sortBy}
onSwitch={onAssistantSwitch}
onDelete={onAssistantDelete}
addPreset={addPreset}
copyAssistant={copyAssistant}
onCreateDefaultAssistant={onCreateDefaultAssistant}
handleSortByChange={handleSortByChange}
sortByPinyinAsc={sortByPinyinAsc}
sortByPinyinDesc={sortByPinyinDesc}
/>
)
}
},
[
activeAgentId,
activeAssistantId,
sortBy,
onAssistantSwitch,
onAssistantDelete,
onAgentDelete,
onAgentPress,
addPreset,
copyAssistant,
onCreateDefaultAssistant,
handleSortByChange,
sortByPinyinAsc,
sortByPinyinDesc
]
)
return (
<div>
{groupedItems.map((group) => (
<TagGroup
key={group.tag}
tag={group.tag}
isCollapsed={collapsedTags[group.tag]}
onToggle={onToggleTagCollapse}
showTitle={group.tag !== t('assistants.tags.untagged')}>
<DraggableList
list={group.items}
itemKey={(item) => `${item.type}-${item.data.id}`}
onUpdate={(newList) => onGroupReorder(group.tag, newList)}
onDragStart={onDragStart}
onDragEnd={onDragEnd}>
{renderUnifiedItem}
</DraggableList>
</TagGroup>
))}
</div>
)
}
@@ -0,0 +1,19 @@
import { useAgentSessionInitializer } from '@renderer/hooks/agents/useAgentSessionInitializer'
import { useAppDispatch } from '@renderer/store'
import { setActiveAgentId as setActiveAgentIdAction } from '@renderer/store/runtime'
import { useCallback } from 'react'
export const useActiveAgent = () => {
const dispatch = useAppDispatch()
const { initializeAgentSession } = useAgentSessionInitializer()
const setActiveAgentId = useCallback(
async (id: string) => {
dispatch(setActiveAgentIdAction(id))
await initializeAgentSession(id)
},
[dispatch, initializeAgentSession]
)
return { setActiveAgentId }
}
@@ -0,0 +1,140 @@
import { useAppDispatch } from '@renderer/store'
import { setUnifiedListOrder } from '@renderer/store/assistants'
import { AgentEntity, Assistant } from '@renderer/types'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { UnifiedItem } from './useUnifiedItems'
interface UseUnifiedGroupingOptions {
unifiedItems: UnifiedItem[]
assistants: Assistant[]
agents: AgentEntity[]
apiServerEnabled: boolean
agentsLoading: boolean
agentsError: Error | null
updateAssistants: (assistants: Assistant[]) => void
}
export const useUnifiedGrouping = (options: UseUnifiedGroupingOptions) => {
const { unifiedItems, assistants, agents, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
const { t } = useTranslation()
const dispatch = useAppDispatch()
// Group unified items by tags
const groupedUnifiedItems = useMemo(() => {
const groups = new Map<string, UnifiedItem[]>()
unifiedItems.forEach((item) => {
if (item.type === 'agent') {
// Agents go to untagged group
const groupKey = t('assistants.tags.untagged')
if (!groups.has(groupKey)) {
groups.set(groupKey, [])
}
groups.get(groupKey)!.push(item)
} else {
// Assistants use their tags
const tags = item.data.tags?.length ? item.data.tags : [t('assistants.tags.untagged')]
tags.forEach((tag) => {
if (!groups.has(tag)) {
groups.set(tag, [])
}
groups.get(tag)!.push(item)
})
}
})
// Sort groups: untagged first, then tagged groups
const untaggedKey = t('assistants.tags.untagged')
const sortedGroups = Array.from(groups.entries()).sort(([tagA], [tagB]) => {
if (tagA === untaggedKey) return -1
if (tagB === untaggedKey) return 1
return 0
})
return sortedGroups.map(([tag, items]) => ({ tag, items }))
}, [unifiedItems, t])
const handleUnifiedGroupReorder = useCallback(
(tag: string, newGroupList: UnifiedItem[]) => {
// Extract only assistants from the new list for updating
const newAssistants = newGroupList.filter((item) => item.type === 'assistant').map((item) => item.data)
// Update assistants state
let insertIndex = 0
const updatedAssistants = assistants.map((a) => {
const tags = a.tags?.length ? a.tags : [t('assistants.tags.untagged')]
if (tags.includes(tag)) {
const replaced = newAssistants[insertIndex]
insertIndex += 1
return replaced || a
}
return a
})
updateAssistants(updatedAssistants)
// Rebuild unified order and save to Redux
const newUnifiedItems: UnifiedItem[] = []
const availableAgents = new Map<string, AgentEntity>()
const availableAssistants = new Map<string, Assistant>()
if (apiServerEnabled && !agentsLoading && !agentsError) {
agents.forEach((agent) => availableAgents.set(agent.id, agent))
}
updatedAssistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
// Reconstruct order based on current groupedUnifiedItems structure
groupedUnifiedItems.forEach((group) => {
if (group.tag === tag) {
// Use the new group list for this tag
newGroupList.forEach((item) => {
newUnifiedItems.push(item)
if (item.type === 'agent') {
availableAgents.delete(item.data.id)
} else {
availableAssistants.delete(item.data.id)
}
})
} else {
// Keep existing order for other tags
group.items.forEach((item) => {
newUnifiedItems.push(item)
if (item.type === 'agent') {
availableAgents.delete(item.data.id)
} else {
availableAssistants.delete(item.data.id)
}
})
}
})
// Add any remaining items
availableAgents.forEach((agent) => newUnifiedItems.push({ type: 'agent', data: agent }))
availableAssistants.forEach((assistant) => newUnifiedItems.push({ type: 'assistant', data: assistant }))
// Save to Redux
const orderToSave = newUnifiedItems.map((item) => ({
type: item.type,
id: item.data.id
}))
dispatch(setUnifiedListOrder(orderToSave))
},
[
assistants,
t,
updateAssistants,
apiServerEnabled,
agentsLoading,
agentsError,
agents,
groupedUnifiedItems,
dispatch
]
)
return {
groupedUnifiedItems,
handleUnifiedGroupReorder
}
}
@@ -0,0 +1,73 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setUnifiedListOrder } from '@renderer/store/assistants'
import { AgentEntity, Assistant } from '@renderer/types'
import { useCallback, useMemo } from 'react'
export type UnifiedItem = { type: 'agent'; data: AgentEntity } | { type: 'assistant'; data: Assistant }
interface UseUnifiedItemsOptions {
agents: AgentEntity[]
assistants: Assistant[]
apiServerEnabled: boolean
agentsLoading: boolean
agentsError: Error | null
updateAssistants: (assistants: Assistant[]) => void
}
export const useUnifiedItems = (options: UseUnifiedItemsOptions) => {
const { agents, assistants, apiServerEnabled, agentsLoading, agentsError, updateAssistants } = options
const dispatch = useAppDispatch()
const unifiedListOrder = useAppSelector((state) => state.assistants.unifiedListOrder || [])
// Create unified items list (agents + assistants) with saved order
const unifiedItems = useMemo(() => {
const items: UnifiedItem[] = []
// Collect all available items
const availableAgents = new Map<string, AgentEntity>()
const availableAssistants = new Map<string, Assistant>()
if (apiServerEnabled && !agentsLoading && !agentsError) {
agents.forEach((agent) => availableAgents.set(agent.id, agent))
}
assistants.forEach((assistant) => availableAssistants.set(assistant.id, assistant))
// Apply saved order
unifiedListOrder.forEach((item) => {
if (item.type === 'agent' && availableAgents.has(item.id)) {
items.push({ type: 'agent', data: availableAgents.get(item.id)! })
availableAgents.delete(item.id)
} else if (item.type === 'assistant' && availableAssistants.has(item.id)) {
items.push({ type: 'assistant', data: availableAssistants.get(item.id)! })
availableAssistants.delete(item.id)
}
})
// Add new items (not in saved order) to the end
availableAgents.forEach((agent) => items.push({ type: 'agent', data: agent }))
availableAssistants.forEach((assistant) => items.push({ type: 'assistant', data: assistant }))
return items
}, [agents, assistants, apiServerEnabled, agentsLoading, agentsError, unifiedListOrder])
const handleUnifiedListReorder = useCallback(
(newList: UnifiedItem[]) => {
// Save the unified order to Redux
const orderToSave = newList.map((item) => ({
type: item.type,
id: item.data.id
}))
dispatch(setUnifiedListOrder(orderToSave))
// Extract and update assistants order
const newAssistants = newList.filter((item) => item.type === 'assistant').map((item) => item.data)
updateAssistants(newAssistants)
},
[dispatch, updateAssistants]
)
return {
unifiedItems,
handleUnifiedListReorder
}
}
@@ -0,0 +1,56 @@
import { useAppDispatch } from '@renderer/store'
import { setUnifiedListOrder } from '@renderer/store/assistants'
import { Assistant } from '@renderer/types'
import { useCallback } from 'react'
import * as tinyPinyin from 'tiny-pinyin'
import { UnifiedItem } from './useUnifiedItems'
interface UseUnifiedSortingOptions {
unifiedItems: UnifiedItem[]
updateAssistants: (assistants: Assistant[]) => void
}
export const useUnifiedSorting = (options: UseUnifiedSortingOptions) => {
const { unifiedItems, updateAssistants } = options
const dispatch = useAppDispatch()
const sortUnifiedItemsByPinyin = useCallback((items: UnifiedItem[], isAscending: boolean) => {
return [...items].sort((a, b) => {
const nameA = a.type === 'agent' ? a.data.name || a.data.id : a.data.name
const nameB = b.type === 'agent' ? b.data.name || b.data.id : b.data.name
const pinyinA = tinyPinyin.convertToPinyin(nameA, '', true)
const pinyinB = tinyPinyin.convertToPinyin(nameB, '', true)
return isAscending ? pinyinA.localeCompare(pinyinB) : pinyinB.localeCompare(pinyinA)
})
}, [])
const sortByPinyinAsc = useCallback(() => {
const sorted = sortUnifiedItemsByPinyin(unifiedItems, true)
const orderToSave = sorted.map((item) => ({
type: item.type,
id: item.data.id
}))
dispatch(setUnifiedListOrder(orderToSave))
// Also update assistants order
const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data)
updateAssistants(newAssistants)
}, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants])
const sortByPinyinDesc = useCallback(() => {
const sorted = sortUnifiedItemsByPinyin(unifiedItems, false)
const orderToSave = sorted.map((item) => ({
type: item.type,
id: item.data.id
}))
dispatch(setUnifiedListOrder(orderToSave))
// Also update assistants order
const newAssistants = sorted.filter((item) => item.type === 'assistant').map((item) => item.data)
updateAssistants(newAssistants)
}, [unifiedItems, sortUnifiedItemsByPinyin, dispatch, updateAssistants])
return {
sortByPinyinAsc,
sortByPinyinDesc
}
}
@@ -1,4 +1,6 @@
import { SyncOutlined } from '@ant-design/icons'
import { useDisclosure } from '@heroui/react'
import UpdateDialog from '@renderer/components/UpdateDialog'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { Button } from 'antd'
@@ -10,6 +12,7 @@ const UpdateAppButton: FC = () => {
const { update } = useRuntime()
const { autoCheckUpdate } = useSettings()
const { t } = useTranslation()
const { isOpen, onOpen, onClose } = useDisclosure()
if (!update) {
return null
@@ -23,13 +26,15 @@ const UpdateAppButton: FC = () => {
<Container>
<UpdateButton
className="nodrag"
onClick={() => window.api.showUpdateDialog()}
onClick={onOpen}
icon={<SyncOutlined />}
color="orange"
variant="outlined"
size="small">
{t('button.update_available')}
</UpdateButton>
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={update.info || null} />
</Container>
)
}
@@ -14,6 +14,7 @@ import styled from 'styled-components'
// Tab 模式下新的页面壳,不再直接创建 WebView,而是依赖全局 MinAppTabsPool
import MinimalToolbar from './components/MinimalToolbar'
import WebviewSearch from './components/WebviewSearch'
const logger = loggerService.withContext('MinAppPage')
@@ -184,6 +185,7 @@ const MinAppPage: FC = () => {
onOpenDevTools={handleOpenDevTools}
/>
</ToolbarWrapper>
<WebviewSearch webviewRef={webviewRef} isWebviewReady={isReady} appId={app.id} />
{!isReady && (
<LoadingMask>
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
@@ -0,0 +1,298 @@
import { Button, Input } from '@heroui/react'
import { loggerService } from '@logger'
import type { WebviewTag } from 'electron'
import { ChevronDown, ChevronUp, X } from 'lucide-react'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
type FoundInPageResult = Electron.FoundInPageResult
interface WebviewSearchProps {
webviewRef: React.RefObject<WebviewTag | null>
isWebviewReady: boolean
appId: string
}
const logger = loggerService.withContext('WebviewSearch')
const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, appId }) => {
const { t } = useTranslation()
const [isVisible, setIsVisible] = useState(false)
const [query, setQuery] = useState('')
const [matchCount, setMatchCount] = useState(0)
const [activeIndex, setActiveIndex] = useState(0)
const [currentWebview, setCurrentWebview] = useState<WebviewTag | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const focusFrameRef = useRef<number | null>(null)
const lastAppIdRef = useRef<string>(appId)
const attachedWebviewRef = useRef<WebviewTag | null>(null)
const focusInput = useCallback(() => {
if (focusFrameRef.current !== null) {
window.cancelAnimationFrame(focusFrameRef.current)
focusFrameRef.current = null
}
focusFrameRef.current = window.requestAnimationFrame(() => {
inputRef.current?.focus()
inputRef.current?.select()
})
}, [])
const resetSearchState = useCallback((options?: { keepQuery?: boolean }) => {
if (!options?.keepQuery) {
setQuery('')
}
setMatchCount(0)
setActiveIndex(0)
}, [])
const stopSearch = useCallback(() => {
const target = webviewRef.current ?? attachedWebviewRef.current
if (!target) return
try {
target.stopFindInPage('clearSelection')
} catch (error) {
logger.error('stopFindInPage failed', { error })
}
}, [webviewRef])
const closeSearch = useCallback(() => {
setIsVisible(false)
stopSearch()
resetSearchState({ keepQuery: true })
}, [resetSearchState, stopSearch])
const performSearch = useCallback(
(text: string, options?: Electron.FindInPageOptions) => {
const target = webviewRef.current ?? attachedWebviewRef.current
if (!target) {
logger.debug('Skip performSearch: webview not attached')
return
}
if (!text) {
stopSearch()
resetSearchState({ keepQuery: true })
return
}
try {
target.findInPage(text, options)
} catch (error) {
logger.error('findInPage failed', { error })
window.toast?.error(t('common.error'))
}
},
[resetSearchState, stopSearch, t, webviewRef]
)
const handleFoundInPage = useCallback((event: Event & { result?: FoundInPageResult }) => {
if (!event.result) return
const { activeMatchOrdinal, matches } = event.result
if (matches !== undefined) {
setMatchCount(matches)
}
if (activeMatchOrdinal !== undefined) {
setActiveIndex(activeMatchOrdinal)
}
}, [])
const openSearch = useCallback(() => {
if (!isWebviewReady) {
logger.debug('Skip openSearch: webview not ready')
return
}
setIsVisible(true)
focusInput()
}, [focusInput, isWebviewReady])
const goToNext = useCallback(() => {
if (!query) return
performSearch(query, { forward: true, findNext: true })
}, [performSearch, query])
const goToPrevious = useCallback(() => {
if (!query) return
performSearch(query, { forward: false, findNext: true })
}, [performSearch, query])
useEffect(() => {
const nextWebview = webviewRef.current ?? null
if (currentWebview === nextWebview) return
setCurrentWebview(nextWebview)
}, [currentWebview, webviewRef])
useEffect(() => {
const target = currentWebview
if (!target) {
attachedWebviewRef.current = null
return
}
const handle = handleFoundInPage
attachedWebviewRef.current = target
target.addEventListener('found-in-page', handle)
return () => {
target.removeEventListener('found-in-page', handle)
if (attachedWebviewRef.current === target) {
try {
target.stopFindInPage('clearSelection')
} catch (error) {
logger.error('stopFindInPage failed', { error })
}
attachedWebviewRef.current = null
}
}
}, [currentWebview, handleFoundInPage])
useEffect(() => {
if (!isVisible) return
focusInput()
}, [focusInput, isVisible])
useEffect(() => {
if (!isVisible) return
if (!query) {
performSearch('')
return
}
performSearch(query)
}, [currentWebview, isVisible, performSearch, query])
useEffect(() => {
const handleKeydown = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'f') {
event.preventDefault()
openSearch()
return
}
if (!isVisible) return
if (event.key === 'Escape') {
event.preventDefault()
closeSearch()
return
}
if (event.key === 'Enter') {
event.preventDefault()
if (event.shiftKey) {
goToPrevious()
} else {
goToNext()
}
}
}
window.addEventListener('keydown', handleKeydown, true)
return () => {
window.removeEventListener('keydown', handleKeydown, true)
}
}, [closeSearch, goToNext, goToPrevious, isVisible, openSearch])
useEffect(() => {
if (!isWebviewReady) {
setIsVisible(false)
resetSearchState()
stopSearch()
return
}
}, [isWebviewReady, resetSearchState, stopSearch])
useEffect(() => {
if (!appId) return
if (lastAppIdRef.current === appId) return
lastAppIdRef.current = appId
setIsVisible(false)
resetSearchState()
stopSearch()
}, [appId, resetSearchState, stopSearch])
useEffect(() => {
return () => {
stopSearch()
if (focusFrameRef.current !== null) {
window.cancelAnimationFrame(focusFrameRef.current)
focusFrameRef.current = null
}
}
}, [stopSearch])
if (!isVisible) {
return null
}
const matchLabel = `${matchCount > 0 ? Math.max(activeIndex, 1) : 0}/${matchCount}`
const noResultTitle = matchCount === 0 && query ? t('common.no_results') : undefined
const disableNavigation = !query || matchCount === 0
return (
<div className="pointer-events-auto absolute top-3 right-3 z-50 flex items-center gap-2 rounded-xl border border-default-200 bg-background px-2 py-1 shadow-lg">
<Input
ref={inputRef}
autoFocus
value={query}
onValueChange={setQuery}
spellCheck={'false'}
placeholder={t('common.search')}
size="sm"
radius="sm"
variant="flat"
classNames={{
base: 'w-[240px]',
inputWrapper:
'h-8 bg-transparent border border-transparent shadow-none hover:border-transparent hover:bg-transparent focus:border-transparent data-[hover=true]:border-transparent data-[focus=true]:border-transparent data-[focus-visible=true]:outline-none data-[focus-visible=true]:ring-0',
input: 'text-small focus:outline-none focus-visible:outline-none',
innerWrapper: 'gap-0'
}}
/>
<span
className="min-w-[44px] text-center text-default-500 text-small tabular-nums"
title={noResultTitle}
role="status"
aria-live="polite"
aria-atomic="true">
{matchLabel}
</span>
<div className="h-4 w-px bg-default-200" />
<Button
size="sm"
variant="light"
radius="full"
isIconOnly
onPress={goToPrevious}
isDisabled={disableNavigation}
aria-label="Previous match"
className="text-default-500 hover:text-default-900">
<ChevronUp size={16} />
</Button>
<Button
size="sm"
variant="light"
radius="full"
isIconOnly
onPress={goToNext}
isDisabled={disableNavigation}
aria-label="Next match"
className="text-default-500 hover:text-default-900">
<ChevronDown size={16} />
</Button>
<div className="h-4 w-px bg-default-200" />
<Button
size="sm"
variant="light"
radius="full"
isIconOnly
onPress={closeSearch}
aria-label={t('common.close')}
className="text-default-500 hover:text-default-900">
<X size={16} />
</Button>
</div>
)
}
export default WebviewSearch
@@ -0,0 +1,237 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import type { WebviewTag } from 'electron'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import WebviewSearch from '../WebviewSearch'
const translations: Record<string, string> = {
'common.close': 'Close',
'common.error': 'Error',
'common.no_results': 'No results',
'common.search': 'Search'
}
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => translations[key] ?? key
})
}))
const createWebviewMock = () => {
const listeners = new Map<string, Set<(event: Event & { result?: Electron.FoundInPageResult }) => void>>()
const findInPageMock = vi.fn()
const stopFindInPageMock = vi.fn()
const webview = {
addEventListener: vi.fn(
(type: string, listener: (event: Event & { result?: Electron.FoundInPageResult }) => void) => {
if (!listeners.has(type)) {
listeners.set(type, new Set())
}
listeners.get(type)!.add(listener)
}
),
removeEventListener: vi.fn(
(type: string, listener: (event: Event & { result?: Electron.FoundInPageResult }) => void) => {
listeners.get(type)?.delete(listener)
}
),
findInPage: findInPageMock as unknown as WebviewTag['findInPage'],
stopFindInPage: stopFindInPageMock as unknown as WebviewTag['stopFindInPage']
} as unknown as WebviewTag
const emit = (type: string, result?: Electron.FoundInPageResult) => {
listeners.get(type)?.forEach((listener) => {
const event = new CustomEvent(type) as Event & { result?: Electron.FoundInPageResult }
event.result = result
listener(event)
})
}
return {
emit,
findInPageMock,
stopFindInPageMock,
webview
}
}
const openSearchOverlay = async () => {
await act(async () => {
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', ctrlKey: true }))
})
await waitFor(() => {
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument()
})
}
const originalRAF = window.requestAnimationFrame
const originalCAF = window.cancelAnimationFrame
const requestAnimationFrameMock = vi.fn((callback: FrameRequestCallback) => {
callback(0)
return 1
})
const cancelAnimationFrameMock = vi.fn()
beforeAll(() => {
Object.defineProperty(window, 'requestAnimationFrame', {
value: requestAnimationFrameMock,
writable: true
})
Object.defineProperty(window, 'cancelAnimationFrame', {
value: cancelAnimationFrameMock,
writable: true
})
})
afterAll(() => {
Object.defineProperty(window, 'requestAnimationFrame', {
value: originalRAF
})
Object.defineProperty(window, 'cancelAnimationFrame', {
value: originalCAF
})
})
describe('WebviewSearch', () => {
const toastMock = {
error: vi.fn(),
success: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
addToast: vi.fn()
}
beforeEach(() => {
Object.assign(window, { toast: toastMock })
})
afterEach(() => {
vi.clearAllMocks()
})
it('opens the search overlay with keyboard shortcut', async () => {
const { webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
await openSearchOverlay()
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument()
})
it('performs searches and navigates between results', async () => {
const { emit, findInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const user = userEvent.setup()
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
await openSearchOverlay()
const input = screen.getByRole('textbox')
await user.type(input, 'Cherry')
await waitFor(() => {
expect(findInPageMock).toHaveBeenCalledWith('Cherry', undefined)
})
await act(async () => {
emit('found-in-page', {
requestId: 1,
matches: 3,
activeMatchOrdinal: 1,
selectionArea: undefined as unknown as Electron.Rectangle,
finalUpdate: false
} as Electron.FoundInPageResult)
})
const nextButton = screen.getByRole('button', { name: 'Next match' })
await waitFor(() => {
expect(nextButton).not.toBeDisabled()
})
await user.click(nextButton)
await waitFor(() => {
expect(findInPageMock).toHaveBeenLastCalledWith('Cherry', { forward: true, findNext: true })
})
const previousButton = screen.getByRole('button', { name: 'Previous match' })
await user.click(previousButton)
await waitFor(() => {
expect(findInPageMock).toHaveBeenLastCalledWith('Cherry', { forward: false, findNext: true })
})
})
it('clears search state when appId changes', async () => {
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const user = userEvent.setup()
const { rerender } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
await openSearchOverlay()
const input = screen.getByRole('textbox')
await user.type(input, 'Cherry')
await waitFor(() => {
expect(findInPageMock).toHaveBeenCalled()
})
await act(async () => {
rerender(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-2" />)
})
await waitFor(() => {
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
})
expect(stopFindInPageMock).toHaveBeenCalledWith('clearSelection')
})
it('shows toast error when search fails', async () => {
const { findInPageMock, webview } = createWebviewMock()
findInPageMock.mockImplementation(() => {
throw new Error('findInPage failed')
})
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const user = userEvent.setup()
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
await openSearchOverlay()
const input = screen.getByRole('textbox')
await user.type(input, 'Cherry')
await waitFor(() => {
expect(toastMock.error).toHaveBeenCalledWith('Error')
})
})
it('stops search when component unmounts', async () => {
const { stopFindInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
const { unmount } = render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
await openSearchOverlay()
stopFindInPageMock.mockClear()
unmount()
expect(stopFindInPageMock).toHaveBeenCalledWith('clearSelection')
})
it('ignores keyboard shortcut when webview is not ready', async () => {
const { findInPageMock, webview } = createWebviewMock()
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady={false} appId="app-1" />)
await act(async () => {
fireEvent.keyDown(window, { key: 'f', ctrlKey: true })
})
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
expect(findInPageMock).not.toHaveBeenCalled()
})
})
+16 -12
View File
@@ -385,21 +385,25 @@ const NotesPage: FC = () => {
}, [activeFilePath])
// 获取目标文件夹路径(选中文件夹或根目录)
const getTargetFolderPath = useCallback(() => {
if (selectedFolderId) {
const selectedNode = findNode(notesTree, selectedFolderId)
if (selectedNode && selectedNode.type === 'folder') {
return selectedNode.externalPath
const getTargetFolderPath = useCallback(
(targetFolderId?: string) => {
const folderId = targetFolderId || selectedFolderId
if (folderId) {
const selectedNode = findNode(notesTree, folderId)
if (selectedNode && selectedNode.type === 'folder') {
return selectedNode.externalPath
}
}
}
return notesPath // 默认返回根目录
}, [selectedFolderId, notesTree, notesPath])
return notesPath // 默认返回根目录
},
[selectedFolderId, notesTree, notesPath]
)
// 创建文件夹
const handleCreateFolder = useCallback(
async (name: string) => {
async (name: string, targetFolderId?: string) => {
try {
const targetPath = getTargetFolderPath()
const targetPath = getTargetFolderPath(targetFolderId)
if (!targetPath) {
throw new Error('No folder path selected')
}
@@ -415,11 +419,11 @@ const NotesPage: FC = () => {
// 创建笔记
const handleCreateNote = useCallback(
async (name: string) => {
async (name: string, targetFolderId?: string) => {
try {
isCreatingNoteRef.current = true
const targetPath = getTargetFolderPath()
const targetPath = getTargetFolderPath(targetFolderId)
if (!targetPath) {
throw new Error('No folder path selected')
}
+184 -114
View File
@@ -34,8 +34,8 @@ import { useSelector } from 'react-redux'
import styled from 'styled-components'
interface NotesSidebarProps {
onCreateFolder: (name: string, parentId?: string) => void
onCreateNote: (name: string, parentId?: string) => void
onCreateFolder: (name: string, targetFolderId?: string) => void
onCreateNote: (name: string, targetFolderId?: string) => void
onSelectNode: (node: NotesTreeNode) => void
onDeleteNode: (nodeId: string) => void
onRenameNode: (nodeId: string, newName: string) => void
@@ -71,6 +71,8 @@ interface TreeNodeProps {
onDrop: (e: React.DragEvent, node: NotesTreeNode) => void
onDragEnd: () => void
renderChildren?: boolean // 控制是否渲染子节点
openDropdownKey: string | null
onDropdownOpenChange: (key: string | null) => void
}
const TreeNode = memo<TreeNodeProps>(
@@ -94,7 +96,9 @@ const TreeNode = memo<TreeNodeProps>(
onDragLeave,
onDrop,
onDragEnd,
renderChildren = true
renderChildren = true,
openDropdownKey,
onDropdownOpenChange
}) => {
const { t } = useTranslation()
@@ -119,8 +123,12 @@ const TreeNode = memo<TreeNodeProps>(
return (
<div key={node.id}>
<Dropdown menu={{ items: getMenuItems(node) }} trigger={['contextMenu']}>
<div>
<Dropdown
menu={{ items: getMenuItems(node) }}
trigger={['contextMenu']}
open={openDropdownKey === node.id}
onOpenChange={(open) => onDropdownOpenChange(open ? node.id : null)}>
<div onContextMenu={(e) => e.stopPropagation()}>
<TreeNodeContainer
active={isActive}
depth={depth}
@@ -206,6 +214,8 @@ const TreeNode = memo<TreeNodeProps>(
onDrop={onDrop}
onDragEnd={onDragEnd}
renderChildren={renderChildren}
openDropdownKey={openDropdownKey}
onDropdownOpenChange={onDropdownOpenChange}
/>
))}
</div>
@@ -244,6 +254,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
const [isShowSearch, setIsShowSearch] = useState(false)
const [searchKeyword, setSearchKeyword] = useState('')
const [isDragOverSidebar, setIsDragOverSidebar] = useState(false)
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null)
const dragNodeRef = useRef<HTMLDivElement | null>(null)
const scrollbarRef = useRef<any>(null)
@@ -588,6 +599,28 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
})
}
if (node.type === 'folder') {
baseMenuItems.push(
{
label: t('notes.new_note'),
key: 'new_note',
icon: <FilePlus size={14} />,
onClick: () => {
onCreateNote(t('notes.untitled_note'), node.id)
}
},
{
label: t('notes.new_folder'),
key: 'new_folder',
icon: <Folder size={14} />,
onClick: () => {
onCreateFolder(t('notes.untitled_folder'), node.id)
}
},
{ type: 'divider' }
)
}
baseMenuItems.push(
{
label: t('notes.rename'),
@@ -702,7 +735,9 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
handleDeleteNode,
renamingNodeIds,
handleAutoRename,
exportMenuOptions
exportMenuOptions,
onCreateNote,
onCreateFolder
]
)
@@ -783,6 +818,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
fileInput.click()
}, [onUploadFiles])
const getEmptyAreaMenuItems = useCallback((): MenuProps['items'] => {
return [
{
label: t('notes.new_note'),
key: 'new_note',
icon: <FilePlus size={14} />,
onClick: handleCreateNote
},
{
label: t('notes.new_folder'),
key: 'new_folder',
icon: <Folder size={14} />,
onClick: handleCreateFolder
}
]
}, [t, handleCreateNote, handleCreateFolder])
return (
<SidebarContainer
onDragOver={(e) => {
@@ -812,31 +864,90 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
<NotesTreeContainer>
{shouldUseVirtualization ? (
<VirtualizedTreeContainer ref={parentRef}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative'
}}>
{virtualizer.getVirtualItems().map((virtualItem) => {
const { node, depth } = flattenedNodes[virtualItem.index]
return (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`
}}>
<div style={{ padding: '0 8px' }}>
<Dropdown
menu={{ items: getEmptyAreaMenuItems() }}
trigger={['contextMenu']}
open={openDropdownKey === 'empty-area'}
onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}>
<VirtualizedTreeContainer ref={parentRef}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
width: '100%',
position: 'relative'
}}>
{virtualizer.getVirtualItems().map((virtualItem) => {
const { node, depth } = flattenedNodes[virtualItem.index]
return (
<div
key={virtualItem.key}
data-index={virtualItem.index}
ref={virtualizer.measureElement}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`
}}>
<div style={{ padding: '0 8px' }}>
<TreeNode
node={node}
depth={depth}
selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id}
editingNodeId={editingNodeId}
renamingNodeIds={renamingNodeIds}
newlyRenamedNodeIds={newlyRenamedNodeIds}
draggedNodeId={draggedNodeId}
dragOverNodeId={dragOverNodeId}
dragPosition={dragPosition}
inPlaceEdit={inPlaceEdit}
getMenuItems={getMenuItems}
onSelectNode={onSelectNode}
onToggleExpanded={onToggleExpanded}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
renderChildren={false}
openDropdownKey={openDropdownKey}
onDropdownOpenChange={setOpenDropdownKey}
/>
</div>
</div>
)
})}
</div>
{!isShowStarred && !isShowSearch && (
<DropHintNode>
<TreeNodeContainer active={false} depth={0}>
<TreeNodeContent>
<NodeIcon>
<FilePlus size={16} />
</NodeIcon>
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
</TreeNodeContent>
</TreeNodeContainer>
</DropHintNode>
)}
</VirtualizedTreeContainer>
</Dropdown>
) : (
<Dropdown
menu={{ items: getEmptyAreaMenuItems() }}
trigger={['contextMenu']}
open={openDropdownKey === 'empty-area'}
onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}>
<StyledScrollbar ref={scrollbarRef}>
<TreeContent>
{isShowStarred || isShowSearch
? filteredTree.map((node) => (
<TreeNode
key={node.id}
node={node}
depth={depth}
depth={0}
selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id}
editingNodeId={editingNodeId}
@@ -854,92 +965,51 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
renderChildren={false}
openDropdownKey={openDropdownKey}
onDropdownOpenChange={setOpenDropdownKey}
/>
</div>
</div>
)
})}
</div>
{!isShowStarred && !isShowSearch && (
<DropHintNode>
<TreeNodeContainer active={false} depth={0}>
<TreeNodeContent>
<NodeIcon>
<FilePlus size={16} />
</NodeIcon>
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
</TreeNodeContent>
</TreeNodeContainer>
</DropHintNode>
)}
</VirtualizedTreeContainer>
) : (
<StyledScrollbar ref={scrollbarRef}>
<TreeContent>
{isShowStarred || isShowSearch
? filteredTree.map((node) => (
<TreeNode
key={node.id}
node={node}
depth={0}
selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id}
editingNodeId={editingNodeId}
renamingNodeIds={renamingNodeIds}
newlyRenamedNodeIds={newlyRenamedNodeIds}
draggedNodeId={draggedNodeId}
dragOverNodeId={dragOverNodeId}
dragPosition={dragPosition}
inPlaceEdit={inPlaceEdit}
getMenuItems={getMenuItems}
onSelectNode={onSelectNode}
onToggleExpanded={onToggleExpanded}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
/>
))
: notesTree.map((node) => (
<TreeNode
key={node.id}
node={node}
depth={0}
selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id}
editingNodeId={editingNodeId}
renamingNodeIds={renamingNodeIds}
newlyRenamedNodeIds={newlyRenamedNodeIds}
draggedNodeId={draggedNodeId}
dragOverNodeId={dragOverNodeId}
dragPosition={dragPosition}
inPlaceEdit={inPlaceEdit}
getMenuItems={getMenuItems}
onSelectNode={onSelectNode}
onToggleExpanded={onToggleExpanded}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
/>
))}
{!isShowStarred && !isShowSearch && (
<DropHintNode>
<TreeNodeContainer active={false} depth={0}>
<TreeNodeContent>
<NodeIcon>
<FilePlus size={16} />
</NodeIcon>
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
</TreeNodeContent>
</TreeNodeContainer>
</DropHintNode>
)}
</TreeContent>
</StyledScrollbar>
))
: notesTree.map((node) => (
<TreeNode
key={node.id}
node={node}
depth={0}
selectedFolderId={selectedFolderId}
activeNodeId={activeNode?.id}
editingNodeId={editingNodeId}
renamingNodeIds={renamingNodeIds}
newlyRenamedNodeIds={newlyRenamedNodeIds}
draggedNodeId={draggedNodeId}
dragOverNodeId={dragOverNodeId}
dragPosition={dragPosition}
inPlaceEdit={inPlaceEdit}
getMenuItems={getMenuItems}
onSelectNode={onSelectNode}
onToggleExpanded={onToggleExpanded}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onDragEnd={handleDragEnd}
openDropdownKey={openDropdownKey}
onDropdownOpenChange={setOpenDropdownKey}
/>
))}
{!isShowStarred && !isShowSearch && (
<DropHintNode>
<TreeNodeContainer active={false} depth={0}>
<TreeNodeContent>
<NodeIcon>
<FilePlus size={16} />
</NodeIcon>
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
</TreeNodeContent>
</TreeNodeContainer>
</DropHintNode>
)}
</TreeContent>
</StyledScrollbar>
</Dropdown>
)}
</NotesTreeContainer>
@@ -12,6 +12,7 @@ import { HStack, VStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant'
import { getProviderLogo } from '@renderer/config/providers'
import { LanguagesEnum } from '@renderer/config/translate'
import { useTheme } from '@renderer/context/ThemeProvider'
import { usePaintings } from '@renderer/hooks/usePaintings'
@@ -26,7 +27,7 @@ import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import type { FileMetadata, Painting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
import { Avatar, Button, Input, InputNumber, Radio, Select, Slider, Switch, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } from 'react'
@@ -388,7 +389,16 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
<ContentContainer id="content-container">
<LeftContainer>
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
<Select value={providerOptions[2].value} onChange={handleProviderChange} options={providerOptions} />
<Select value={providerOptions[2].value} onChange={handleProviderChange}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
))}
</Select>
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
@@ -662,4 +672,14 @@ const StyledInputNumber = styled(InputNumber)`
width: 70px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
const ProviderLogo = styled(Avatar)`
flex-shrink: 0;
`
export default SiliconPage
@@ -1,18 +1,21 @@
import { GithubOutlined } from '@ant-design/icons'
import { useDisclosure } from '@heroui/react'
import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack } from '@renderer/components/Layout'
import UpdateDialog from '@renderer/components/UpdateDialog'
import { APP_NAME, AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { handleSaveData, useAppDispatch } from '@renderer/store'
import { useAppDispatch } from '@renderer/store'
import { setUpdateState } from '@renderer/store/runtime'
import { ThemeMode } from '@renderer/types'
import { runAsyncFunction } from '@renderer/utils'
import { UpgradeChannel } from '@shared/config/constant'
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
import { UpdateInfo } from 'builder-util-runtime'
import { debounce } from 'lodash'
import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react'
import { BadgeQuestionMark } from 'lucide-react'
@@ -27,6 +30,8 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitl
const AboutSettings: FC = () => {
const [version, setVersion] = useState('')
const [isPortable, setIsPortable] = useState(false)
const [updateDialogInfo, setUpdateDialogInfo] = useState<UpdateInfo | null>(null)
const { isOpen, onOpen, onClose } = useDisclosure()
const { t } = useTranslation()
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
const { theme } = useTheme()
@@ -41,8 +46,9 @@ const AboutSettings: FC = () => {
}
if (update.downloaded) {
await handleSaveData()
window.api.showUpdateDialog()
// Open update dialog directly in renderer
setUpdateDialogInfo(update.info || null)
onOpen()
return
}
@@ -341,6 +347,9 @@ const AboutSettings: FC = () => {
<Button onClick={debug}>{t('settings.about.debug.open')}</Button>
</SettingRow>
</SettingGroup>
{/* Update Dialog */}
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={updateDialogInfo} />
</SettingContainer>
)
}
@@ -76,7 +76,7 @@ export const AccessibleDirsSetting: React.FC<AccessibleDirsSettingProps> = ({ ba
<li
key={path}
className="flex items-center justify-between gap-2 rounded-medium border border-default-200 px-3 py-2">
<span className="truncate text-sm" title={path}>
<span className="w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-sm" title={path}>
{path}
</span>
<Button size="sm" variant="light" color="danger" onPress={() => removeAccessiblePath(path)}>
@@ -172,15 +172,22 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
const onUpdateApiVersion = () => updateProvider({ apiVersion })
const openApiKeyList = async () => {
if (localApiKey !== provider.apiKey) {
updateProvider({ apiKey: formatApiKeys(localApiKey) })
await new Promise((resolve) => setTimeout(resolve, 0))
}
await ApiKeyListPopup.show({
providerId: provider.id,
title: `${fancyProviderName} ${t('settings.provider.api.key.list.title')}`
title: `${fancyProviderName} ${t('settings.provider.api.key.list.title')}`,
providerType: 'llm'
})
}
const onCheckApi = async () => {
const formattedLocalKey = formatApiKeys(localApiKey)
// 如果存在多个密钥,直接打开管理窗口
if (provider.apiKey.includes(',')) {
if (formattedLocalKey.includes(',')) {
await openApiKeyList()
return
}
@@ -204,7 +211,7 @@ const ProviderSetting: FC<Props> = ({ providerId }) => {
try {
setApiKeyConnectivity((prev) => ({ ...prev, checking: true, status: HealthStatus.NOT_CHECKED }))
await checkApi({ ...provider, apiHost }, model)
await checkApi({ ...provider, apiHost, apiKey: formattedLocalKey }, model)
window.toast.success({
timeout: 2000,
@@ -15,7 +15,7 @@ import {
WebSearchProviderResult,
WebSearchStatus
} from '@renderer/types'
import { hasObjectKey, uuid } from '@renderer/utils'
import { hasObjectKey, removeSpecialCharactersForFileName, uuid } from '@renderer/utils'
import { addAbortController } from '@renderer/utils/abortController'
import { formatErrorMessage } from '@renderer/utils/error'
import { ExtractResults } from '@renderer/utils/extract'
@@ -55,7 +55,7 @@ class WebSearchService {
dispose: (requestState: RequestState, requestId: string) => {
if (!requestState.searchBase) return
window.api.knowledgeBase
.delete(getKnowledgeBaseParams(requestState.searchBase), requestState.searchBase.id)
.delete(removeSpecialCharactersForFileName(requestState.searchBase.id))
.catch((error) => logger.warn(`Failed to cleanup search base for ${requestId}:`, error))
}
})
@@ -216,6 +216,7 @@ class WebSearchService {
documentCount: number,
requestId: string
): Promise<KnowledgeBase> {
// requestId: eg: openai-responses-openai/gpt-5-timestamp-uuid
const baseId = `websearch-compression-${requestId}`
const state = this.getRequestState(requestId)
@@ -226,7 +227,8 @@ class WebSearchService {
// 清理旧的知识库
if (state.searchBase) {
await window.api.knowledgeBase.delete(getKnowledgeBaseParams(state.searchBase), state.searchBase.id)
// 将requestId中的 '/' 映射为 '_'
await window.api.knowledgeBase.delete(removeSpecialCharactersForFileName(state.searchBase.id))
}
if (!config.embeddingModel) {
@@ -462,7 +464,9 @@ class WebSearchService {
// 处理 summarize
if (questions[0] === 'summarize' && links && links.length > 0) {
const contents = await fetchWebContents(links, undefined, undefined, { signal })
const contents = await fetchWebContents(links, undefined, undefined, {
signal
})
webSearchProvider.topicId &&
endSpan({
topicId: webSearchProvider.topicId,
@@ -121,7 +121,8 @@ export class BlockManager {
newMessagesActions.upsertBlockReference({
messageId: this.deps.assistantMsgId,
blockId: newBlock.id,
status: newBlock.status
status: newBlock.status,
blockType: newBlock.type
})
)
+7 -1
View File
@@ -13,6 +13,7 @@ export interface AssistantsState {
tagsOrder: string[]
collapsedTags: Record<string, boolean>
presets: AssistantPreset[]
unifiedListOrder: Array<{ type: 'agent' | 'assistant'; id: string }>
}
const initialState: AssistantsState = {
@@ -20,7 +21,8 @@ const initialState: AssistantsState = {
assistants: [getDefaultAssistant()],
tagsOrder: [],
collapsedTags: {},
presets: []
presets: [],
unifiedListOrder: []
}
const assistantsSlice = createSlice({
@@ -96,6 +98,9 @@ const assistantsSlice = createSlice({
[tag]: !prev[tag]
}
},
setUnifiedListOrder: (state, action: PayloadAction<Array<{ type: 'agent' | 'assistant'; id: string }>>) => {
state.unifiedListOrder = action.payload
},
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
const topic = action.payload.topic
topic.createdAt = topic.createdAt || new Date().toISOString()
@@ -244,6 +249,7 @@ export const {
setTagsOrder,
updateAssistantSettings,
updateTagCollapse,
setUnifiedListOrder,
setAssistantPresets,
addAssistantPreset,
removeAssistantPreset,
+1 -1
View File
@@ -65,7 +65,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 160,
version: 162,
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
migrate
},
+3 -10
View File
@@ -1,14 +1,7 @@
import { loggerService } from '@logger'
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import FileManager from '@renderer/services/FileManager'
import {
FileMetadata,
KnowledgeBase,
KnowledgeBaseParams,
KnowledgeItem,
PreprocessProvider,
ProcessingStatus
} from '@renderer/types'
import { FileMetadata, KnowledgeBase, KnowledgeItem, PreprocessProvider, ProcessingStatus } from '@renderer/types'
const logger = loggerService.withContext('Store:Knowledge')
@@ -28,13 +21,13 @@ const knowledgeSlice = createSlice({
state.bases.push(action.payload)
},
deleteBase(state, action: PayloadAction<{ baseId: string; baseParams: KnowledgeBaseParams }>) {
deleteBase(state, action: PayloadAction<{ baseId: string }>) {
const base = state.bases.find((b) => b.id === action.payload.baseId)
if (base) {
state.bases = state.bases.filter((b) => b.id !== action.payload.baseId)
const files = base.items.filter((item) => item.type === 'file')
FileManager.deleteFiles(files.map((item) => item.content) as FileMetadata[])
window.api.knowledgeBase.delete(action.payload.baseParams, action.payload.baseId)
window.api.knowledgeBase.delete(action.payload.baseId)
}
},
+19 -6
View File
@@ -68,6 +68,7 @@ function removeMiniAppIconsFromState(state: RootState) {
function removeMiniAppFromState(state: RootState, id: string) {
if (state.minapps) {
state.minapps.pinned = state.minapps.pinned.filter((app) => app.id !== id)
state.minapps.enabled = state.minapps.enabled.filter((app) => app.id !== id)
state.minapps.disabled = state.minapps.disabled.filter((app) => app.id !== id)
}
@@ -2596,16 +2597,30 @@ const migrateConfig = {
return state
}
},
'160': (state: RootState) => {
'161': (state: RootState) => {
try {
removeMiniAppFromState(state, 'nm-search')
removeMiniAppFromState(state, 'hika')
removeMiniAppFromState(state, 'hugging-chat')
addProvider(state, 'cherryin')
state.llm.providers = moveProvider(state.llm.providers, 'cherryin', 1)
return state
} catch (error) {
logger.error('migrate 161 error', error as Error)
return state
}
},
'162': (state: RootState) => {
try {
// @ts-ignore
if (state?.agents?.agents) {
// @ts-ignore
state.assistants.presets = [...state.agents.agents]
// @ts-ignore
delete state.agents.agents
}
if (state.settings.sidebarIcons) {
state.settings.sidebarIcons.visible = state.settings.sidebarIcons.visible.map((icon) => {
// @ts-ignore
return icon === 'agents' ? 'store' : icon
@@ -2615,6 +2630,7 @@ const migrateConfig = {
return icon === 'agents' ? 'store' : icon
})
}
state.llm.providers.forEach((provider) => {
if (provider.anthropicApiHost) {
return
@@ -2642,16 +2658,13 @@ const migrateConfig = {
case 'new-api':
provider.anthropicApiHost = 'http://localhost:3000'
break
case 'cherryai':
provider.anthropicApiHost = 'https://api.cherry-ai.com'
break
case 'grok':
provider.anthropicApiHost = 'https://api.x.ai'
}
})
return state
} catch (error) {
logger.error('migrate 160 error', error as Error)
logger.error('migrate 162 error', error as Error)
return state
}
}
+8 -3
View File
@@ -2,7 +2,7 @@ import { loggerService } from '@logger'
import { createEntityAdapter, createSlice, EntityState, PayloadAction } from '@reduxjs/toolkit'
// Separate type-only imports from value imports
import type { Message } from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
const logger = loggerService.withContext('newMessage')
@@ -50,6 +50,7 @@ interface UpsertBlockReferencePayload {
messageId: string
blockId: string
status?: MessageBlockStatus
blockType?: MessageBlockType
}
// Payload for removing a single message
@@ -218,7 +219,7 @@ export const messagesSlice = createSlice({
messagesAdapter.removeMany(state, messageIds)
},
upsertBlockReference(state, action: PayloadAction<UpsertBlockReferencePayload>) {
const { messageId, blockId, status } = action.payload
const { messageId, blockId, status, blockType } = action.payload
const messageToUpdate = state.entities[messageId]
if (!messageToUpdate) {
@@ -231,7 +232,11 @@ export const messagesSlice = createSlice({
// Update Block ID
const currentBlocks = messageToUpdate.blocks || []
if (!currentBlocks.includes(blockId)) {
changes.blocks = [...currentBlocks, blockId]
if (blockType === MessageBlockType.THINKING) {
changes.blocks = [blockId, ...currentBlocks]
} else {
changes.blocks = [...currentBlocks, blockId]
}
}
// Update Message Status based on Block Status
@@ -41,7 +41,7 @@ export class StreamHandler {
this.usage.total_tokens += completionChunk.usage.total_tokens || 0
}
context = chunk.choices
.map((choice) => {
?.map((choice) => {
if (!choice.delta) {
return ''
} else if ('reasoning_content' in choice.delta) {
+1 -1
View File
@@ -106,7 +106,7 @@ export type Provider = {
}
export const SystemProviderIds = {
// cherryin: 'cherryin',
cherryin: 'cherryin',
silicon: 'silicon',
aihubmix: 'aihubmix',
ocoolai: 'ocoolai',
+160 -80
View File
@@ -74,112 +74,125 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/amazon-bedrock@npm:^3.0.29":
version: 3.0.29
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.29"
"@ai-sdk/amazon-bedrock@npm:^3.0.35":
version: 3.0.35
resolution: "@ai-sdk/amazon-bedrock@npm:3.0.35"
dependencies:
"@ai-sdk/anthropic": "npm:2.0.22"
"@ai-sdk/anthropic": "npm:2.0.27"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
"@smithy/eventstream-codec": "npm:^4.0.1"
"@smithy/util-utf8": "npm:^4.0.0"
aws4fetch: "npm:^1.0.20"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/7add02e6c13774943929bb5d568b3110f6badc6d95cb56c6d3011cafc45778e27c0133417dd7fe835e7f0b1ae7767c22a7d5e3d39f725e2aa44e2b6e47d95fb7
checksum: 10c0/0e3e0ed1730fa6a14d8d7ca14b7823ec0b80c9d666435d97a505e7fb0c1818378343cdb647e3cc08d7f15d201cbeb04272c5128065f6cc6858b4404961eca761
languageName: node
linkType: hard
"@ai-sdk/anthropic@npm:2.0.22, @ai-sdk/anthropic@npm:^2.0.22":
version: 2.0.22
resolution: "@ai-sdk/anthropic@npm:2.0.22"
"@ai-sdk/anthropic@npm:2.0.27, @ai-sdk/anthropic@npm:^2.0.27":
version: 2.0.27
resolution: "@ai-sdk/anthropic@npm:2.0.27"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/d922d2ff606b2429fb14c099628ba6734ef7c9b0e9225635f3faaf2d067362dea6ae0e920a35c05ccf15a01c59fef93ead5f147a9609dd3dd8c3ac18a3123b85
checksum: 10c0/b568b3b8639af8ec7ea9b766061a4f18bcdef16f2bb12da3a4c4124c751bd6aab1b96dbe1a0eb8e38831d305871ce0787a6536d1a4d8a8ab8aaf03aca3e48e3f
languageName: node
linkType: hard
"@ai-sdk/azure@npm:^2.0.42":
version: 2.0.42
resolution: "@ai-sdk/azure@npm:2.0.42"
"@ai-sdk/azure@npm:^2.0.49":
version: 2.0.49
resolution: "@ai-sdk/azure@npm:2.0.49"
dependencies:
"@ai-sdk/openai": "npm:2.0.42"
"@ai-sdk/openai": "npm:2.0.48"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/14d3d6edac691df57879a9a7efc46d5d00b6bde5b64cd62a67a7668455c341171119ae90a431e57ac37009bced19add50b3da26998376b7e56e080bc2c997c00
checksum: 10c0/d4dc5a8e0cbe0cefc8db987c4a7b784a9898d40cc55ef38618c71eba7f40dbef77b754aec1d507559f643fed49e538ffe2b677b327f001a2efc0474f6b544ba9
languageName: node
linkType: hard
"@ai-sdk/deepseek@npm:^1.0.20":
version: 1.0.20
resolution: "@ai-sdk/deepseek@npm:1.0.20"
"@ai-sdk/deepseek@npm:^1.0.23":
version: 1.0.23
resolution: "@ai-sdk/deepseek@npm:1.0.23"
dependencies:
"@ai-sdk/openai-compatible": "npm:1.0.19"
"@ai-sdk/openai-compatible": "npm:1.0.22"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/e66ece8cf6371c2bac5436ed82cd1e2bb5c367fae6df60090f91cff62bf241f4df0abded99c33558013f8dc0bcc7d962f2126086eba8587ba929da50afd3d806
checksum: 10c0/39736e9787420ce86d0f2ce6935ba51f2b721acfb4c0d2b77064a8b939cf22b0767a83b82a5c99efff1311080532a3aaa2f34804d7981133f671a050521ed197
languageName: node
linkType: hard
"@ai-sdk/gateway@npm:1.0.32":
version: 1.0.32
resolution: "@ai-sdk/gateway@npm:1.0.32"
"@ai-sdk/gateway@npm:1.0.39":
version: 1.0.39
resolution: "@ai-sdk/gateway@npm:1.0.39"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
"@vercel/oidc": "npm:3.0.2"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/82c98db6e4e8e235e1ff66410318ebe77cc1518ebf06d8d4757b4f30aaa3bf7075d3028816438551fef2f89e2d4c8c26e4efcd9913a06717aee1308dad3ddc30
checksum: 10c0/1b6eedf12ac641c96a1eb75e48e43474694b60eb7dca273f76a636a4e2bfc89efda1d9855d5abf9cc464e23cdbf5a3119fed65c3d22cec726e29a2bad3c3318b
languageName: node
linkType: hard
"@ai-sdk/google-vertex@npm:^3.0.33":
version: 3.0.33
resolution: "@ai-sdk/google-vertex@npm:3.0.33"
"@ai-sdk/google-vertex@npm:^3.0.40":
version: 3.0.40
resolution: "@ai-sdk/google-vertex@npm:3.0.40"
dependencies:
"@ai-sdk/anthropic": "npm:2.0.22"
"@ai-sdk/google": "npm:2.0.17"
"@ai-sdk/anthropic": "npm:2.0.27"
"@ai-sdk/google": "npm:2.0.20"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
google-auth-library: "npm:^9.15.0"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/d440e46f702385985a34f2260074eb41cf2516036598039c8c72d6155825114452942c3c012a181da7661341bee9a38958e5f9a53bba145b9c5dc4446411a651
checksum: 10c0/680a06e1b80bc036744e2f13e1a55b57661c3674000ab82b863d6536730edfc3696b1b0b2235f6354de11fa323c4ef817d8edbd2dbf94dc4037ea882e560c9ea
languageName: node
linkType: hard
"@ai-sdk/google@npm:2.0.17":
version: 2.0.17
resolution: "@ai-sdk/google@npm:2.0.17"
"@ai-sdk/google@npm:2.0.20":
version: 2.0.20
resolution: "@ai-sdk/google@npm:2.0.20"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/174bcde507e5bf4bf95f20dbe4eaba73870715b13779e320f3df44995606e4d7ccd1e1f4b759d224deaf58bdfc6aa2e43a24dcbe5fa335ddfe91df1b06114218
checksum: 10c0/9c73bb67061673b16f0996c85bf4e79ab9968c8a203c4f9731bf569e45960db88950dfc227aca69661ea805d381b285697ba1763faa03a38c01b86e6d2e90629
languageName: node
linkType: hard
"@ai-sdk/mistral@npm:^2.0.17":
version: 2.0.17
resolution: "@ai-sdk/mistral@npm:2.0.17"
"@ai-sdk/mistral@npm:^2.0.19":
version: 2.0.19
resolution: "@ai-sdk/mistral@npm:2.0.19"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/58a129357c93cc7f2b15b2ba6ccfb9df3fb72e06163641602ea41c858f835cd76985d66665a56e4ed3fa1eb19ca75a83ae12986d466ec41942e9bf13d558c441
checksum: 10c0/522d1e4631e9318f82f5993030c8fa2a28341e749bc920d32966d91d5cd5a4d1638980b7e0a62601aaaaf7a25e04fefed18b07ce50034c5c5d903ac5bebb65ec
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@npm:1.0.19, @ai-sdk/openai-compatible@npm:^1.0.19":
"@ai-sdk/openai-compatible@npm:1.0.22, @ai-sdk/openai-compatible@npm:^1.0.22":
version: 1.0.22
resolution: "@ai-sdk/openai-compatible@npm:1.0.22"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/31eb07b63eaf07384391e81d824e16589af540f3af2fde1cb24f2a6d04dd07ddb843c9301dbceca78fa5ae5002cb235fc376c41532ab167d1564491526e6011b
languageName: node
linkType: hard
"@ai-sdk/openai-compatible@npm:^1.0.19":
version: 1.0.19
resolution: "@ai-sdk/openai-compatible@npm:1.0.19"
dependencies:
@@ -191,27 +204,39 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/openai@npm:2.0.42, @ai-sdk/openai@npm:^2.0.42":
version: 2.0.42
resolution: "@ai-sdk/openai@npm:2.0.42"
"@ai-sdk/openai@npm:2.0.48, @ai-sdk/openai@npm:^2.0.48":
version: 2.0.48
resolution: "@ai-sdk/openai@npm:2.0.48"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/b1ab158aafc86735e53c4621ffe125d469bc1732c533193652768a9f66ecd4d169303ce7ca59069b7baf725da49e55bcf81210848f09f66deaf2a8335399e6d7
checksum: 10c0/6c584d7ffb80025da6b7253106a83f8c7a023e8ca322fd32e6858453782d6a0a6d268d7afa7145e3ea743a9c6cbc882932bb59eb1a659750f5205639c414fb49
languageName: node
linkType: hard
"@ai-sdk/perplexity@npm:^2.0.11":
version: 2.0.11
resolution: "@ai-sdk/perplexity@npm:2.0.11"
"@ai-sdk/openai@npm:^2.0.42":
version: 2.0.47
resolution: "@ai-sdk/openai@npm:2.0.47"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.11"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/a8722b68f529b3d1baaa1ba4624c61efe732f22b24dfc20e27afae07bb25d72532bcb62d022191ab5e49df24496af619eabc092a4e6ad293b3fe231ef61b6467
checksum: 10c0/7fabcdda707134971bcc2b285705d4595f8bf419285dbdd9266b3b0858ea11b6ac200e63dd2eeb1822f99571910093d64d4a76154a365331cf184f56452933c6
languageName: node
linkType: hard
"@ai-sdk/perplexity@npm:^2.0.13":
version: 2.0.13
resolution: "@ai-sdk/perplexity@npm:2.0.13"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/80434eebec088d5f373901f1beb77ca4ba564df5f04dec43c69a7996ea0d88344a3d86ca5e5ef2dc4f5c45f45fc478dabf3a0e44a3faea86a0190c087491a661
languageName: node
linkType: hard
@@ -228,6 +253,32 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.11":
version: 3.0.11
resolution: "@ai-sdk/provider-utils@npm:3.0.11"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/31081b127b48f3eefb448eaca59574b4631da9577aa0778622d28669c71bbde0361c9b37962c5edbb1d0c163ed1479755fc889da9251a03e906b1e27d0d2eb24
languageName: node
linkType: hard
"@ai-sdk/provider-utils@npm:3.0.12, @ai-sdk/provider-utils@npm:^3.0.12":
version: 3.0.12
resolution: "@ai-sdk/provider-utils@npm:3.0.12"
dependencies:
"@ai-sdk/provider": "npm:2.0.0"
"@standard-schema/spec": "npm:^1.0.0"
eventsource-parser: "npm:^3.0.5"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/83886bf188cad0cc655b680b710a10413989eaba9ec59dd24a58b985c02a8a1d50ad0f96dd5259385c07592ec3c37a7769fdf4a1ef569a73c9edbdb2cd585915
languageName: node
linkType: hard
"@ai-sdk/provider@npm:2.0.0, @ai-sdk/provider@npm:^2.0.0":
version: 2.0.0
resolution: "@ai-sdk/provider@npm:2.0.0"
@@ -237,16 +288,25 @@ __metadata:
languageName: node
linkType: hard
"@ai-sdk/xai@npm:^2.0.23":
version: 2.0.23
resolution: "@ai-sdk/xai@npm:2.0.23"
"@ai-sdk/provider@npm:^2.1.0-beta.4":
version: 2.1.0-beta.5
resolution: "@ai-sdk/provider@npm:2.1.0-beta.5"
dependencies:
"@ai-sdk/openai-compatible": "npm:1.0.19"
json-schema: "npm:^0.4.0"
checksum: 10c0/4f51813285a8e92be18ef14645b0505bb0c9d2daa0d9290cac8a3c1f87d8e2e8507b1edf1818ae2305a90723e8cd44477f55f0631407dee35912ab8fdded52ba
languageName: node
linkType: hard
"@ai-sdk/xai@npm:^2.0.26":
version: 2.0.26
resolution: "@ai-sdk/xai@npm:2.0.26"
dependencies:
"@ai-sdk/openai-compatible": "npm:1.0.22"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/4cf6b3bc71024797d1b2e37b57fb746f7387f9a7c1da530fd040aad1a840603a1a86fb7df7e428c723eba9b1547f89063d68f84e6e08444d2d4f152dee321dc3
checksum: 10c0/72fef55a96d9c3820de02beb9b63e53902649c5db906a892b7818a984b6e8afe161daa225b8d527b74f783e2c4eecd474af6e96efbb95761aca2c508e0c7c2d9
languageName: node
linkType: hard
@@ -2367,14 +2427,14 @@ __metadata:
version: 0.0.0-use.local
resolution: "@cherrystudio/ai-core@workspace:packages/aiCore"
dependencies:
"@ai-sdk/anthropic": "npm:^2.0.22"
"@ai-sdk/azure": "npm:^2.0.42"
"@ai-sdk/deepseek": "npm:^1.0.20"
"@ai-sdk/openai": "npm:^2.0.42"
"@ai-sdk/openai-compatible": "npm:^1.0.19"
"@ai-sdk/anthropic": "npm:^2.0.27"
"@ai-sdk/azure": "npm:^2.0.49"
"@ai-sdk/deepseek": "npm:^1.0.23"
"@ai-sdk/openai": "npm:^2.0.48"
"@ai-sdk/openai-compatible": "npm:^1.0.22"
"@ai-sdk/provider": "npm:^2.0.0"
"@ai-sdk/provider-utils": "npm:^3.0.10"
"@ai-sdk/xai": "npm:^2.0.23"
"@ai-sdk/provider-utils": "npm:^3.0.12"
"@ai-sdk/xai": "npm:^2.0.26"
tsdown: "npm:^0.12.9"
typescript: "npm:^5.0.0"
vitest: "npm:^3.2.4"
@@ -7673,6 +7733,18 @@ __metadata:
languageName: node
linkType: hard
"@opeoginni/github-copilot-openai-compatible@npm:0.1.18":
version: 0.1.18
resolution: "@opeoginni/github-copilot-openai-compatible@npm:0.1.18"
dependencies:
"@ai-sdk/openai": "npm:^2.0.42"
"@ai-sdk/openai-compatible": "npm:^1.0.19"
"@ai-sdk/provider": "npm:^2.1.0-beta.4"
"@ai-sdk/provider-utils": "npm:^3.0.10"
checksum: 10c0/31b87ed150883bbdd33a0203e45831859560fdf174f0285384fdcb1d01fc4a56ca15f31d648e8d6d3a2d4d5c6e327ddecbf422543eeefaa7e8fdd7dc2f2a3b08
languageName: node
linkType: hard
"@oxc-project/runtime@npm:0.71.0":
version: 0.71.0
resolution: "@oxc-project/runtime@npm:0.71.0"
@@ -13878,6 +13950,13 @@ __metadata:
languageName: node
linkType: hard
"@vercel/oidc@npm:3.0.2":
version: 3.0.2
resolution: "@vercel/oidc@npm:3.0.2"
checksum: 10c0/8d4c8553baa5aed339ab7614d775139bc124a6d443b76877ab17e98c156daa4dbeb3cf2f3bf21fabfae2ac0dd3ff462ab43b9398708e02483e5923d302a1c4c8
languageName: node
linkType: hard
"@vimeo/player@npm:2.29.0":
version: 2.29.0
resolution: "@vimeo/player@npm:2.29.0"
@@ -14181,10 +14260,10 @@ __metadata:
"@agentic/exa": "npm:^7.3.3"
"@agentic/searxng": "npm:^7.3.3"
"@agentic/tavily": "npm:^7.3.3"
"@ai-sdk/amazon-bedrock": "npm:^3.0.29"
"@ai-sdk/google-vertex": "npm:^3.0.33"
"@ai-sdk/mistral": "npm:^2.0.17"
"@ai-sdk/perplexity": "npm:^2.0.11"
"@ai-sdk/amazon-bedrock": "npm:^3.0.35"
"@ai-sdk/google-vertex": "npm:^3.0.40"
"@ai-sdk/mistral": "npm:^2.0.19"
"@ai-sdk/perplexity": "npm:^2.0.13"
"@ant-design/v5-patch-for-react-19": "npm:^1.0.3"
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.1#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.1-d937b73fed.patch"
"@anthropic-ai/sdk": "npm:^0.41.0"
@@ -14238,6 +14317,7 @@ __metadata:
"@opentelemetry/sdk-trace-base": "npm:^2.0.0"
"@opentelemetry/sdk-trace-node": "npm:^2.0.0"
"@opentelemetry/sdk-trace-web": "npm:^2.0.0"
"@opeoginni/github-copilot-openai-compatible": "npm:0.1.18"
"@playwright/test": "npm:^1.52.0"
"@radix-ui/react-context-menu": "npm:^2.2.16"
"@reduxjs/toolkit": "npm:^2.2.5"
@@ -14306,7 +14386,7 @@ __metadata:
"@viz-js/lang-dot": "npm:^1.0.5"
"@viz-js/viz": "npm:^3.14.0"
"@xyflow/react": "npm:^12.4.4"
ai: "npm:^5.0.59"
ai: "npm:^5.0.68"
antd: "patch:antd@npm%3A5.27.0#~/.yarn/patches/antd-npm-5.27.0-aa91c36546.patch"
archiver: "npm:^7.0.1"
async-mutex: "npm:^0.5.0"
@@ -14569,17 +14649,17 @@ __metadata:
languageName: node
linkType: hard
"ai@npm:^5.0.59":
version: 5.0.59
resolution: "ai@npm:5.0.59"
"ai@npm:^5.0.68":
version: 5.0.68
resolution: "ai@npm:5.0.68"
dependencies:
"@ai-sdk/gateway": "npm:1.0.32"
"@ai-sdk/gateway": "npm:1.0.39"
"@ai-sdk/provider": "npm:2.0.0"
"@ai-sdk/provider-utils": "npm:3.0.10"
"@ai-sdk/provider-utils": "npm:3.0.12"
"@opentelemetry/api": "npm:1.9.0"
peerDependencies:
zod: ^3.25.76 || ^4.1.8
checksum: 10c0/daa956e753b93fbc30afbfba5be2ebb73e3c280dae3064e13949f04d5a22c0f4ea5698cc87e24a23ed6585d9cf7febee61b915292dbbd4286dc40c449cf2b845
checksum: 10c0/0c042cd58c7193a47b06b3074a9e62790c4d5a8134e8e12bbb750714151e9aa217c641ee60c8cbe59d9869bade52ccbb283f9fcbf6d79711ebf1f774fa3feee3
languageName: node
linkType: hard