Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50d1342680 | |||
| ee4c310725 | |||
| a000ff2a1a | |||
| 2f9576b2ae | |||
| 92554dd398 | |||
| 9473ddc762 | |||
| 5f469a71f3 | |||
| 87bac60afc | |||
| 704339e835 | |||
| c8ab7180ba | |||
| 11757546c3 | |||
| 420b9ec2f2 | |||
| 1c73271e33 | |||
| acdbe6b9ed | |||
| 6c201228d9 | |||
| 73b2a375ad | |||
| 89bb830b60 | |||
| 2399db4944 | |||
| 62774b34d3 |
+2
-2
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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',
|
||||
|
||||
@@ -25,7 +25,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
// Default quick assistant model
|
||||
glm45FlashModel
|
||||
],
|
||||
// cherryin: [],
|
||||
cherryin: [],
|
||||
vertexai: [],
|
||||
'302ai': [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -65,7 +65,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 160,
|
||||
version: 162,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -106,7 +106,7 @@ export type Provider = {
|
||||
}
|
||||
|
||||
export const SystemProviderIds = {
|
||||
// cherryin: 'cherryin',
|
||||
cherryin: 'cherryin',
|
||||
silicon: 'silicon',
|
||||
aihubmix: 'aihubmix',
|
||||
ocoolai: 'ocoolai',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user