Compare commits
13 Commits
copilot/ad
...
v1.6.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
170632a199 | ||
|
|
cd5841cdd4 | ||
|
|
763afc5ca2 | ||
|
|
45f033ff4e | ||
|
|
f8fadcc73f | ||
|
|
a94e5dad5f | ||
|
|
632fd4c567 | ||
|
|
401e17eb0e | ||
|
|
80fc118465 | ||
|
|
9a8d7640f5 | ||
|
|
2b3f6d5640 | ||
|
|
a2d81e6204 | ||
|
|
b6107c5fb1 |
@@ -1,8 +1,8 @@
|
|||||||
diff --git a/dist/index.mjs b/dist/index.mjs
|
diff --git a/dist/index.mjs b/dist/index.mjs
|
||||||
index 110f37ec18c98b1d55ae2b73cc716194e6f9094d..17e109b7778cbebb904f1919e768d21a2833d965 100644
|
index b957cb824faa79cf01ba3a504f221870bd8e306a..4d71d30f655775d61537d9d8b73f6e17d41fa67e 100644
|
||||||
--- a/dist/index.mjs
|
--- a/dist/index.mjs
|
||||||
+++ b/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
|
// src/get-model-path.ts
|
||||||
function getModelPath(modelId) {
|
function getModelPath(modelId) {
|
||||||
@@ -125,21 +125,17 @@ afterSign: scripts/notarize.js
|
|||||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
What's New in v1.6.3
|
What's New in v1.6.4
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Notes: Add spell-check control, automatic table line wrapping, export functionality, and LLM-based renaming
|
- Providers: add CherryIN provider
|
||||||
- UI: Expand topic rename clickable area, add middle-click tab closing, remove redundant scrollbars, fix message menubar overflow
|
- Notes: Add right-click context menu to create notes and folders
|
||||||
- Editor: Add read-only extension support, make TextFilePreview read-only but copyable
|
- Mini App: Add search functionality in mini app page
|
||||||
- Models: Update support for DeepSeek v3.2, Claude 4.5, GLM 4.6, Gemini regex, and vision models
|
- Update Dialog: Add updating dialog in renderer process
|
||||||
- Code Tools: Add GitHub Copilot CLI integration
|
- Mini App: Remove some mini apps
|
||||||
|
|
||||||
Bug Fixes:
|
Bug Fixes:
|
||||||
- Fix migration for missing providers
|
- Fix reasoning block insertion order - now inserts before content block
|
||||||
- Fix forked topic retaining old name after rename
|
- Fix knowledge base deletion and web search RAG errors
|
||||||
- Restore first token latency reporting in metrics
|
- Fix Qwen model URL configuration
|
||||||
- Fix UI scrollbar and overflow issues
|
|
||||||
|
|
||||||
Technical Updates:
|
|
||||||
- Upgrade to Electron 37.6.0
|
|
||||||
- Update dependencies across packages
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "1.6.3",
|
"version": "1.6.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -369,7 +369,7 @@
|
|||||||
"undici": "6.21.2",
|
"undici": "6.21.2",
|
||||||
"vite": "npm:rolldown-vite@latest",
|
"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",
|
"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",
|
"packageManager": "yarn@4.9.1",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export enum IpcChannel {
|
|||||||
App_SetLanguage = 'app:set-language',
|
App_SetLanguage = 'app:set-language',
|
||||||
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
|
App_SetEnableSpellCheck = 'app:set-enable-spell-check',
|
||||||
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
|
App_SetSpellCheckLanguages = 'app:set-spell-check-languages',
|
||||||
App_ShowUpdateDialog = 'app:show-update-dialog',
|
|
||||||
App_CheckForUpdate = 'app:check-for-update',
|
App_CheckForUpdate = 'app:check-for-update',
|
||||||
|
App_QuitAndInstall = 'app:quit-and-install',
|
||||||
App_Reload = 'app:reload',
|
App_Reload = 'app:reload',
|
||||||
App_Quit = 'app:quit',
|
App_Quit = 'app:quit',
|
||||||
App_Info = 'app:info',
|
App_Info = 'app:info',
|
||||||
@@ -229,7 +229,6 @@ export enum IpcChannel {
|
|||||||
// events
|
// events
|
||||||
BackupProgress = 'backup-progress',
|
BackupProgress = 'backup-progress',
|
||||||
ThemeUpdated = 'theme:updated',
|
ThemeUpdated = 'theme:updated',
|
||||||
UpdateDownloadedCancelled = 'update-downloaded-cancelled',
|
|
||||||
RestoreProgress = 'restore-progress',
|
RestoreProgress = 'restore-progress',
|
||||||
UpdateError = 'update-error',
|
UpdateError = 'update-error',
|
||||||
UpdateAvailable = 'update-available',
|
UpdateAvailable = 'update-available',
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
|
ipcMain.handle(IpcChannel.Open_Website, (_, url: string) => shell.openExternal(url))
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
ipcMain.handle(IpcChannel.App_ShowUpdateDialog, () => appUpdater.showUpdateDialog(mainWindow))
|
ipcMain.handle(IpcChannel.App_QuitAndInstall, () => appUpdater.quitAndInstall())
|
||||||
|
|
||||||
// language
|
// language
|
||||||
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
|
ipcMain.handle(IpcChannel.App_SetLanguage, (_, language) => {
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { isWin } from '@main/constant'
|
import { isWin } from '@main/constant'
|
||||||
import { getIpCountry } from '@main/utils/ipService'
|
import { getIpCountry } from '@main/utils/ipService'
|
||||||
import { locales } from '@main/utils/locales'
|
|
||||||
import { generateUserAgent } from '@main/utils/systemInfo'
|
import { generateUserAgent } from '@main/utils/systemInfo'
|
||||||
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
|
import { FeedUrl, UpgradeChannel } from '@shared/config/constant'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import { CancellationToken, UpdateInfo } from 'builder-util-runtime'
|
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 { AppUpdater as _AppUpdater, autoUpdater, Logger, NsisUpdater, UpdateCheckResult } from 'electron-updater'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import semver from 'semver'
|
import semver from 'semver'
|
||||||
|
|
||||||
import icon from '../../../build/icon.png?asset'
|
|
||||||
import { configManager } from './ConfigManager'
|
import { configManager } from './ConfigManager'
|
||||||
import { windowService } from './WindowService'
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
@@ -26,7 +24,6 @@ const LANG_MARKERS = {
|
|||||||
|
|
||||||
export default class AppUpdater {
|
export default class AppUpdater {
|
||||||
autoUpdater: _AppUpdater = autoUpdater
|
autoUpdater: _AppUpdater = autoUpdater
|
||||||
private releaseInfo: UpdateInfo | undefined
|
|
||||||
private cancellationToken: CancellationToken = new CancellationToken()
|
private cancellationToken: CancellationToken = new CancellationToken()
|
||||||
private updateCheckResult: UpdateCheckResult | null = null
|
private updateCheckResult: UpdateCheckResult | null = null
|
||||||
|
|
||||||
@@ -66,7 +63,6 @@ export default class AppUpdater {
|
|||||||
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => {
|
||||||
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
const processedReleaseInfo = this.processReleaseInfo(releaseInfo)
|
||||||
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo)
|
windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo)
|
||||||
this.releaseInfo = processedReleaseInfo
|
|
||||||
logger.info('update downloaded', processedReleaseInfo)
|
logger.info('update downloaded', processedReleaseInfo)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,37 +243,9 @@ export default class AppUpdater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async showUpdateDialog(mainWindow: BrowserWindow) {
|
public quitAndInstall() {
|
||||||
if (!this.releaseInfo) {
|
app.isQuitting = true
|
||||||
return
|
setImmediate(() => autoUpdater.quitAndInstall())
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -349,38 +317,9 @@ export default class AppUpdater {
|
|||||||
|
|
||||||
return processedInfo
|
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 {
|
interface GithubReleaseInfo {
|
||||||
draft: boolean
|
draft: boolean
|
||||||
prerelease: boolean
|
prerelease: boolean
|
||||||
tag_name: string
|
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 { fileStorage } from '@main/services/FileStorage'
|
||||||
import { windowService } from '@main/services/WindowService'
|
import { windowService } from '@main/services/WindowService'
|
||||||
import { getDataPath } from '@main/utils'
|
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 { TraceMethod } from '@mcp-trace/trace-core'
|
||||||
import { MB } from '@shared/config/constant'
|
import { MB } from '@shared/config/constant'
|
||||||
import type { LoaderReturn } from '@shared/config/types'
|
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
|
* Delete knowledge base file
|
||||||
*/
|
*/
|
||||||
private deleteKnowledgeFile = (id: string): boolean => {
|
private deleteKnowledgeFile = (id: string): boolean => {
|
||||||
const dbPath = path.join(this.storageDir, id)
|
const dbPath = this.getDbPath(id)
|
||||||
if (fs.existsSync(dbPath)) {
|
if (fs.existsSync(dbPath)) {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(dbPath, { recursive: true })
|
fs.rmSync(dbPath, { recursive: true })
|
||||||
@@ -244,7 +249,8 @@ class KnowledgeService {
|
|||||||
dimensions
|
dimensions
|
||||||
})
|
})
|
||||||
try {
|
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
|
// Save database instance for later closing
|
||||||
this.dbInstances.set(id, libSqlDb)
|
this.dbInstances.set(id, libSqlDb)
|
||||||
|
|
||||||
|
|||||||
@@ -274,46 +274,4 @@ describe('AppUpdater', () => {
|
|||||||
expect(result.releaseNotes).toBeNull()
|
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) =>
|
setProxy: (proxy: string | undefined, bypassRules?: string) =>
|
||||||
ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules),
|
ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules),
|
||||||
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
|
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),
|
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
|
||||||
setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable),
|
setEnableSpellCheck: (isEnable: boolean) => ipcRenderer.invoke(IpcChannel.App_SetEnableSpellCheck, isEnable),
|
||||||
setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages),
|
setSpellCheckLanguages: (languages: string[]) => ipcRenderer.invoke(IpcChannel.App_SetSpellCheckLanguages, languages),
|
||||||
@@ -221,7 +221,7 @@ const api = {
|
|||||||
create: (base: KnowledgeBaseParams, context?: SpanContext) =>
|
create: (base: KnowledgeBaseParams, context?: SpanContext) =>
|
||||||
tracedInvoke(IpcChannel.KnowledgeBase_Create, context, base),
|
tracedInvoke(IpcChannel.KnowledgeBase_Create, context, base),
|
||||||
reset: (base: KnowledgeBaseParams) => ipcRenderer.invoke(IpcChannel.KnowledgeBase_Reset, 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: ({
|
add: ({
|
||||||
base,
|
base,
|
||||||
item,
|
item,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { CherryWebSearchConfig } from '@renderer/store/websearch'
|
|||||||
import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
|
import { type Assistant, type MCPTool, type Provider } from '@renderer/types'
|
||||||
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
import type { StreamTextParams } from '@renderer/types/aiCoreTypes'
|
||||||
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
|
import { mapRegexToPatterns } from '@renderer/utils/blacklistMatchPattern'
|
||||||
|
import { replacePromptVariables } from '@renderer/utils/prompt'
|
||||||
import type { ModelMessage, Tool } from 'ai'
|
import type { ModelMessage, Tool } from 'ai'
|
||||||
import { stepCountIs } from 'ai'
|
import { stepCountIs } from 'ai'
|
||||||
|
|
||||||
@@ -166,7 +167,7 @@ export async function buildStreamTextParams(
|
|||||||
params.tools = tools
|
params.tools = tools
|
||||||
}
|
}
|
||||||
if (assistant.prompt) {
|
if (assistant.prompt) {
|
||||||
params.system = assistant.prompt
|
params.system = await replacePromptVariables(assistant.prompt, model.name)
|
||||||
}
|
}
|
||||||
logger.debug('params', params)
|
logger.debug('params', params)
|
||||||
return {
|
return {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 9.3 KiB |
BIN
src/renderer/src/assets/images/apps/stepfun.png
Normal file
BIN
src/renderer/src/assets/images/apps/stepfun.png
Normal file
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 */}
|
||||||
<GoogleLoginTip isReady={isReady} currentUrl={currentUrl} currentAppId={currentMinappId} />
|
<GoogleLoginTip isReady={isReady} currentUrl={currentUrl} currentAppId={currentMinappId} />
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<EmptyView>
|
<EmptyView style={{ backgroundColor: 'var(--color-background-soft)' }}>
|
||||||
<Avatar
|
<Avatar
|
||||||
src={currentAppInfo?.logo}
|
src={currentAppInfo?.logo}
|
||||||
size={80}
|
size={80}
|
||||||
|
|||||||
101
src/renderer/src/components/UpdateDialog.tsx
Normal file
101
src/renderer/src/components/UpdateDialog.tsx
Normal file
@@ -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
|
||||||
@@ -39,6 +39,7 @@ import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
|||||||
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
||||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
||||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.webp?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 ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
||||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
||||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
||||||
@@ -46,7 +47,6 @@ import WPSLingXiLogo from '@renderer/assets/images/apps/wpslingxi.webp?url'
|
|||||||
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
import XiaoYiAppLogo from '@renderer/assets/images/apps/xiaoyi.webp?url'
|
||||||
import YouLogo from '@renderer/assets/images/apps/you.jpg?url'
|
import YouLogo from '@renderer/assets/images/apps/you.jpg?url'
|
||||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.webp?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 ZaiAppLogo from '@renderer/assets/images/apps/zai.png?url'
|
||||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
||||||
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
||||||
@@ -145,14 +145,14 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
{
|
{
|
||||||
id: 'dashscope',
|
id: 'dashscope',
|
||||||
name: i18n.t('minapps.qwen'),
|
name: i18n.t('minapps.qwen'),
|
||||||
url: 'https://tongyi.aliyun.com/qianwen/',
|
url: 'https://www.tongyi.com/',
|
||||||
logo: QwenModelLogo
|
logo: QwenModelLogo
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stepfun',
|
id: 'stepfun',
|
||||||
name: i18n.t('minapps.yuewen'),
|
name: i18n.t('minapps.stepfun'),
|
||||||
url: 'https://yuewen.cn/chats/new',
|
url: 'https://stepfun.com',
|
||||||
logo: YuewenAppLogo,
|
logo: StepfunAppLogo,
|
||||||
bodered: true
|
bodered: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
|||||||
// Default quick assistant model
|
// Default quick assistant model
|
||||||
glm45FlashModel
|
glm45FlashModel
|
||||||
],
|
],
|
||||||
// cherryin: [],
|
cherryin: [],
|
||||||
vertexai: [],
|
vertexai: [],
|
||||||
'302ai': [
|
'302ai': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,16 +80,16 @@ export const CHERRYAI_PROVIDER: SystemProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
|
export const SYSTEM_PROVIDERS_CONFIG: Record<SystemProviderId, SystemProvider> = {
|
||||||
// cherryin: {
|
cherryin: {
|
||||||
// id: 'cherryin',
|
id: 'cherryin',
|
||||||
// name: 'CherryIN',
|
name: 'CherryIN',
|
||||||
// type: 'openai',
|
type: 'openai',
|
||||||
// apiKey: '',
|
apiKey: '',
|
||||||
// apiHost: 'https://open.cherryin.ai',
|
apiHost: 'https://open.cherryin.net',
|
||||||
// models: [],
|
models: [],
|
||||||
// isSystem: true,
|
isSystem: true,
|
||||||
// enabled: true
|
enabled: true
|
||||||
// },
|
},
|
||||||
silicon: {
|
silicon: {
|
||||||
id: 'silicon',
|
id: 'silicon',
|
||||||
name: 'Silicon',
|
name: 'Silicon',
|
||||||
@@ -732,17 +732,17 @@ type ProviderUrls = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
export const PROVIDER_URLS: Record<SystemProviderId, ProviderUrls> = {
|
||||||
// cherryin: {
|
cherryin: {
|
||||||
// api: {
|
api: {
|
||||||
// url: 'https://open.cherryin.ai'
|
url: 'https://open.cherryin.net'
|
||||||
// },
|
},
|
||||||
// websites: {
|
websites: {
|
||||||
// official: 'https://open.cherryin.ai',
|
official: 'https://open.cherryin.ai',
|
||||||
// apiKey: 'https://open.cherryin.ai/console/token',
|
apiKey: 'https://open.cherryin.ai/console/token',
|
||||||
// docs: 'https://open.cherryin.ai',
|
docs: 'https://open.cherryin.ai',
|
||||||
// models: 'https://open.cherryin.ai/pricing'
|
models: 'https://open.cherryin.ai/pricing'
|
||||||
// }
|
}
|
||||||
// },
|
},
|
||||||
ph8: {
|
ph8: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://ph8.co'
|
url: 'https://ph8.co'
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export const useKnowledgeBases = () => {
|
|||||||
const deleteKnowledgeBase = (baseId: string) => {
|
const deleteKnowledgeBase = (baseId: string) => {
|
||||||
const base = bases.find((b) => b.id === baseId)
|
const base = bases.find((b) => b.id === baseId)
|
||||||
if (!base) return
|
if (!base) return
|
||||||
dispatch(deleteBase({ baseId, baseParams: getKnowledgeBaseParams(base) }))
|
dispatch(deleteBase({ baseId }))
|
||||||
|
|
||||||
// remove assistant knowledge_base
|
// remove assistant knowledge_base
|
||||||
const _assistants = assistants.map((assistant) => {
|
const _assistants = assistants.map((assistant) => {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "Nami AI Search",
|
"nami-ai-search": "Nami AI Search",
|
||||||
"qwen": "Qwen",
|
"qwen": "Qwen",
|
||||||
"sensechat": "SenseChat",
|
"sensechat": "SenseChat",
|
||||||
|
"stepfun": "Stepfun",
|
||||||
"tencent-yuanbao": "Yuanbao",
|
"tencent-yuanbao": "Yuanbao",
|
||||||
"tiangong-ai": "Skywork",
|
"tiangong-ai": "Skywork",
|
||||||
"wanzhi": "Wanzhi",
|
"wanzhi": "Wanzhi",
|
||||||
"wenxin": "ERNIE",
|
"wenxin": "ERNIE",
|
||||||
"wps-copilot": "WPS Copilot",
|
"wps-copilot": "WPS Copilot",
|
||||||
"xiaoyi": "Xiaoyi",
|
"xiaoyi": "Xiaoyi",
|
||||||
"yuewen": "Yuewen",
|
|
||||||
"zhihu": "Zhihu"
|
"zhihu": "Zhihu"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "Later",
|
"later": "Later",
|
||||||
"message": "New version {{version}} is ready, do you want to install it now?",
|
"message": "New version {{version}} is ready, do you want to install it now?",
|
||||||
"noReleaseNotes": "No release notes",
|
"noReleaseNotes": "No release notes",
|
||||||
|
"saveDataError": "Failed to save data, please try again.",
|
||||||
"title": "Update"
|
"title": "Update"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "纳米AI搜索",
|
"nami-ai-search": "纳米AI搜索",
|
||||||
"qwen": "通义千问",
|
"qwen": "通义千问",
|
||||||
"sensechat": "商量",
|
"sensechat": "商量",
|
||||||
|
"stepfun": "阶跃AI",
|
||||||
"tencent-yuanbao": "腾讯元宝",
|
"tencent-yuanbao": "腾讯元宝",
|
||||||
"tiangong-ai": "天工AI",
|
"tiangong-ai": "天工AI",
|
||||||
"wanzhi": "万知",
|
"wanzhi": "万知",
|
||||||
"wenxin": "文心一言",
|
"wenxin": "文心一言",
|
||||||
"wps-copilot": "WPS灵犀",
|
"wps-copilot": "WPS灵犀",
|
||||||
"xiaoyi": "小艺",
|
"xiaoyi": "小艺",
|
||||||
"yuewen": "跃问",
|
|
||||||
"zhihu": "知乎直答"
|
"zhihu": "知乎直答"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "稍后",
|
"later": "稍后",
|
||||||
"message": "发现新版本 {{version}},是否立即安装?",
|
"message": "发现新版本 {{version}},是否立即安装?",
|
||||||
"noReleaseNotes": "暂无更新日志",
|
"noReleaseNotes": "暂无更新日志",
|
||||||
|
"saveDataError": "保存数据失败,请重试",
|
||||||
"title": "更新提示"
|
"title": "更新提示"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "納米AI搜索",
|
"nami-ai-search": "納米AI搜索",
|
||||||
"qwen": "通義千問",
|
"qwen": "通義千問",
|
||||||
"sensechat": "商量",
|
"sensechat": "商量",
|
||||||
|
"stepfun": "階躍AI",
|
||||||
"tencent-yuanbao": "騰訊元寶",
|
"tencent-yuanbao": "騰訊元寶",
|
||||||
"tiangong-ai": "天工AI",
|
"tiangong-ai": "天工AI",
|
||||||
"wanzhi": "萬知",
|
"wanzhi": "萬知",
|
||||||
"wenxin": "文心一言",
|
"wenxin": "文心一言",
|
||||||
"wps-copilot": "WPS靈犀",
|
"wps-copilot": "WPS靈犀",
|
||||||
"xiaoyi": "小藝",
|
"xiaoyi": "小藝",
|
||||||
"yuewen": "躍問",
|
|
||||||
"zhihu": "知乎直答"
|
"zhihu": "知乎直答"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "稍後",
|
"later": "稍後",
|
||||||
"message": "新版本 {{version}} 已準備就緒,是否立即安裝?",
|
"message": "新版本 {{version}} 已準備就緒,是否立即安裝?",
|
||||||
"noReleaseNotes": "暫無更新日誌",
|
"noReleaseNotes": "暫無更新日誌",
|
||||||
|
"saveDataError": "保存數據失敗,請重試",
|
||||||
"title": "更新提示"
|
"title": "更新提示"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "Nami AI Search",
|
"nami-ai-search": "Nami AI Search",
|
||||||
"qwen": "Qwen",
|
"qwen": "Qwen",
|
||||||
"sensechat": "SenseChat",
|
"sensechat": "SenseChat",
|
||||||
|
"stepfun": "Stepfun",
|
||||||
"tencent-yuanbao": "Yuanbao",
|
"tencent-yuanbao": "Yuanbao",
|
||||||
"tiangong-ai": "Skywork",
|
"tiangong-ai": "Skywork",
|
||||||
"wanzhi": "Wanzhi",
|
"wanzhi": "Wanzhi",
|
||||||
"wenxin": "ERNIE",
|
"wenxin": "ERNIE",
|
||||||
"wps-copilot": "WPS Copilot",
|
"wps-copilot": "WPS Copilot",
|
||||||
"xiaoyi": "Xiaoyi",
|
"xiaoyi": "Xiaoyi",
|
||||||
"yuewen": "Yuewen",
|
|
||||||
"zhihu": "Zhihu"
|
"zhihu": "Zhihu"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "Μετά",
|
"later": "Μετά",
|
||||||
"message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;",
|
"message": "Νέα έκδοση {{version}} είναι έτοιμη, θέλετε να την εγκαταστήσετε τώρα;",
|
||||||
"noReleaseNotes": "Χωρίς σημειώσεις",
|
"noReleaseNotes": "Χωρίς σημειώσεις",
|
||||||
|
"saveDataError": "Η αποθήκευση των δεδομένων απέτυχε, δοκιμάστε ξανά",
|
||||||
"title": "Ενημέρωση"
|
"title": "Ενημέρωση"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "Nami AI Search",
|
"nami-ai-search": "Nami AI Search",
|
||||||
"qwen": "Qwen",
|
"qwen": "Qwen",
|
||||||
"sensechat": "SenseChat",
|
"sensechat": "SenseChat",
|
||||||
|
"stepfun": "Stepfun",
|
||||||
"tencent-yuanbao": "Yuanbao",
|
"tencent-yuanbao": "Yuanbao",
|
||||||
"tiangong-ai": "Skywork",
|
"tiangong-ai": "Skywork",
|
||||||
"wanzhi": "Wanzhi",
|
"wanzhi": "Wanzhi",
|
||||||
"wenxin": "ERNIE",
|
"wenxin": "ERNIE",
|
||||||
"wps-copilot": "WPS Copilot",
|
"wps-copilot": "WPS Copilot",
|
||||||
"xiaoyi": "Xiaoyi",
|
"xiaoyi": "Xiaoyi",
|
||||||
"yuewen": "Yuewen",
|
|
||||||
"zhihu": "Zhihu"
|
"zhihu": "Zhihu"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "Más tarde",
|
"later": "Más tarde",
|
||||||
"message": "Nueva versión {{version}} disponible, ¿desea instalarla ahora?",
|
"message": "Nueva versión {{version}} disponible, ¿desea instalarla ahora?",
|
||||||
"noReleaseNotes": "Sin notas de la versión",
|
"noReleaseNotes": "Sin notas de la versión",
|
||||||
|
"saveDataError": "Error al guardar los datos, inténtalo de nuevo",
|
||||||
"title": "Actualización"
|
"title": "Actualización"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "Nami AI Search",
|
"nami-ai-search": "Nami AI Search",
|
||||||
"qwen": "Qwen",
|
"qwen": "Qwen",
|
||||||
"sensechat": "SenseChat",
|
"sensechat": "SenseChat",
|
||||||
|
"stepfun": "Stepfun",
|
||||||
"tencent-yuanbao": "Yuanbao",
|
"tencent-yuanbao": "Yuanbao",
|
||||||
"tiangong-ai": "Skywork",
|
"tiangong-ai": "Skywork",
|
||||||
"wanzhi": "Wanzhi",
|
"wanzhi": "Wanzhi",
|
||||||
"wenxin": "ERNIE",
|
"wenxin": "ERNIE",
|
||||||
"wps-copilot": "WPS Copilot",
|
"wps-copilot": "WPS Copilot",
|
||||||
"xiaoyi": "Xiaoyi",
|
"xiaoyi": "Xiaoyi",
|
||||||
"yuewen": "Yuewen",
|
|
||||||
"zhihu": "Zhihu"
|
"zhihu": "Zhihu"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "Plus tard",
|
"later": "Plus tard",
|
||||||
"message": "Nouvelle version {{version}} disponible, voulez-vous l'installer maintenant ?",
|
"message": "Nouvelle version {{version}} disponible, voulez-vous l'installer maintenant ?",
|
||||||
"noReleaseNotes": "Aucune note de version",
|
"noReleaseNotes": "Aucune note de version",
|
||||||
|
"saveDataError": "Échec de la sauvegarde des données, veuillez réessayer",
|
||||||
"title": "Mise à jour"
|
"title": "Mise à jour"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "Nami AI Search",
|
"nami-ai-search": "Nami AI Search",
|
||||||
"qwen": "通義千問",
|
"qwen": "通義千問",
|
||||||
"sensechat": "SenseChat",
|
"sensechat": "SenseChat",
|
||||||
|
"stepfun": "Stepfun",
|
||||||
"tencent-yuanbao": "騰訊元宝",
|
"tencent-yuanbao": "騰訊元宝",
|
||||||
"tiangong-ai": "Skywork",
|
"tiangong-ai": "Skywork",
|
||||||
"wanzhi": "万知",
|
"wanzhi": "万知",
|
||||||
"wenxin": "ERNIE",
|
"wenxin": "ERNIE",
|
||||||
"wps-copilot": "WPS Copilot",
|
"wps-copilot": "WPS Copilot",
|
||||||
"xiaoyi": "小藝",
|
"xiaoyi": "小藝",
|
||||||
"yuewen": "躍問",
|
|
||||||
"zhihu": "知乎直答"
|
"zhihu": "知乎直答"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "後で",
|
"later": "後で",
|
||||||
"message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?",
|
"message": "新バージョン {{version}} が利用可能です。今すぐインストールしますか?",
|
||||||
"noReleaseNotes": "暫無更新日誌",
|
"noReleaseNotes": "暫無更新日誌",
|
||||||
|
"saveDataError": "データの保存に失敗しました。もう一度お試しください。",
|
||||||
"title": "更新"
|
"title": "更新"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "Nami AI Search",
|
"nami-ai-search": "Nami AI Search",
|
||||||
"qwen": "Qwen",
|
"qwen": "Qwen",
|
||||||
"sensechat": "SenseChat",
|
"sensechat": "SenseChat",
|
||||||
|
"stepfun": "Stepfun",
|
||||||
"tencent-yuanbao": "Yuanbao",
|
"tencent-yuanbao": "Yuanbao",
|
||||||
"tiangong-ai": "Skywork",
|
"tiangong-ai": "Skywork",
|
||||||
"wanzhi": "Wanzhi",
|
"wanzhi": "Wanzhi",
|
||||||
"wenxin": "ERNIE",
|
"wenxin": "ERNIE",
|
||||||
"wps-copilot": "WPS Copilot",
|
"wps-copilot": "WPS Copilot",
|
||||||
"xiaoyi": "Xiaoyi",
|
"xiaoyi": "Xiaoyi",
|
||||||
"yuewen": "Yuewen",
|
|
||||||
"zhihu": "Zhihu"
|
"zhihu": "Zhihu"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "Mais tarde",
|
"later": "Mais tarde",
|
||||||
"message": "Nova versão {{version}} disponível, deseja instalar agora?",
|
"message": "Nova versão {{version}} disponível, deseja instalar agora?",
|
||||||
"noReleaseNotes": "Sem notas de versão",
|
"noReleaseNotes": "Sem notas de versão",
|
||||||
|
"saveDataError": "Falha ao salvar os dados, tente novamente",
|
||||||
"title": "Atualização"
|
"title": "Atualização"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -1583,13 +1583,13 @@
|
|||||||
"nami-ai-search": "Nami AI Search",
|
"nami-ai-search": "Nami AI Search",
|
||||||
"qwen": "Qwen",
|
"qwen": "Qwen",
|
||||||
"sensechat": "SenseChat",
|
"sensechat": "SenseChat",
|
||||||
|
"stepfun": "Stepfun",
|
||||||
"tencent-yuanbao": "Tencent Yuanbao",
|
"tencent-yuanbao": "Tencent Yuanbao",
|
||||||
"tiangong-ai": "Skywork",
|
"tiangong-ai": "Skywork",
|
||||||
"wanzhi": "Wanzhi",
|
"wanzhi": "Wanzhi",
|
||||||
"wenxin": "ERNIE",
|
"wenxin": "ERNIE",
|
||||||
"wps-copilot": "WPS Copilot",
|
"wps-copilot": "WPS Copilot",
|
||||||
"xiaoyi": "Xiaoyi",
|
"xiaoyi": "Xiaoyi",
|
||||||
"yuewen": "Yuewen",
|
|
||||||
"zhihu": "Zhihu"
|
"zhihu": "Zhihu"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
@@ -4412,6 +4412,7 @@
|
|||||||
"later": "Позже",
|
"later": "Позже",
|
||||||
"message": "Новая версия {{version}} готова, установить сейчас?",
|
"message": "Новая версия {{version}} готова, установить сейчас?",
|
||||||
"noReleaseNotes": "Нет заметок об обновлении",
|
"noReleaseNotes": "Нет заметок об обновлении",
|
||||||
|
"saveDataError": "Ошибка сохранения данных, повторите попытку",
|
||||||
"title": "Обновление"
|
"title": "Обновление"
|
||||||
},
|
},
|
||||||
"warning": {
|
"warning": {
|
||||||
|
|||||||
@@ -359,8 +359,7 @@ const GridContainer = styled(Scrollbar)<{ $count: number; $gridColumns: number }
|
|||||||
&.vertical {
|
&.vertical {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
&.grid {
|
&.grid {
|
||||||
grid-template-columns: repeat(
|
grid-template-columns: repeat(
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { SyncOutlined } from '@ant-design/icons'
|
import { SyncOutlined } from '@ant-design/icons'
|
||||||
|
import { useDisclosure } from '@heroui/react'
|
||||||
|
import UpdateDialog from '@renderer/components/UpdateDialog'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
@@ -10,6 +12,7 @@ const UpdateAppButton: FC = () => {
|
|||||||
const { update } = useRuntime()
|
const { update } = useRuntime()
|
||||||
const { autoCheckUpdate } = useSettings()
|
const { autoCheckUpdate } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
|
||||||
if (!update) {
|
if (!update) {
|
||||||
return null
|
return null
|
||||||
@@ -23,13 +26,15 @@ const UpdateAppButton: FC = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<UpdateButton
|
<UpdateButton
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
onClick={() => window.api.showUpdateDialog()}
|
onClick={onOpen}
|
||||||
icon={<SyncOutlined />}
|
icon={<SyncOutlined />}
|
||||||
color="orange"
|
color="orange"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
size="small">
|
size="small">
|
||||||
{t('button.update_available')}
|
{t('button.update_available')}
|
||||||
</UpdateButton>
|
</UpdateButton>
|
||||||
|
|
||||||
|
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={update.info || null} />
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
// Tab 模式下新的页面壳,不再直接创建 WebView,而是依赖全局 MinAppTabsPool
|
// Tab 模式下新的页面壳,不再直接创建 WebView,而是依赖全局 MinAppTabsPool
|
||||||
import MinimalToolbar from './components/MinimalToolbar'
|
import MinimalToolbar from './components/MinimalToolbar'
|
||||||
|
import WebviewSearch from './components/WebviewSearch'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MinAppPage')
|
const logger = loggerService.withContext('MinAppPage')
|
||||||
|
|
||||||
@@ -184,6 +185,7 @@ const MinAppPage: FC = () => {
|
|||||||
onOpenDevTools={handleOpenDevTools}
|
onOpenDevTools={handleOpenDevTools}
|
||||||
/>
|
/>
|
||||||
</ToolbarWrapper>
|
</ToolbarWrapper>
|
||||||
|
<WebviewSearch webviewRef={webviewRef} isWebviewReady={isReady} appId={app.id} />
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<LoadingMask>
|
<LoadingMask>
|
||||||
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
||||||
|
|||||||
298
src/renderer/src/pages/minapps/components/WebviewSearch.tsx
Normal file
298
src/renderer/src/pages/minapps/components/WebviewSearch.tsx
Normal file
@@ -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])
|
}, [activeFilePath])
|
||||||
|
|
||||||
// 获取目标文件夹路径(选中文件夹或根目录)
|
// 获取目标文件夹路径(选中文件夹或根目录)
|
||||||
const getTargetFolderPath = useCallback(() => {
|
const getTargetFolderPath = useCallback(
|
||||||
if (selectedFolderId) {
|
(targetFolderId?: string) => {
|
||||||
const selectedNode = findNode(notesTree, selectedFolderId)
|
const folderId = targetFolderId || selectedFolderId
|
||||||
if (selectedNode && selectedNode.type === 'folder') {
|
if (folderId) {
|
||||||
return selectedNode.externalPath
|
const selectedNode = findNode(notesTree, folderId)
|
||||||
|
if (selectedNode && selectedNode.type === 'folder') {
|
||||||
|
return selectedNode.externalPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return notesPath // 默认返回根目录
|
||||||
return notesPath // 默认返回根目录
|
},
|
||||||
}, [selectedFolderId, notesTree, notesPath])
|
[selectedFolderId, notesTree, notesPath]
|
||||||
|
)
|
||||||
|
|
||||||
// 创建文件夹
|
// 创建文件夹
|
||||||
const handleCreateFolder = useCallback(
|
const handleCreateFolder = useCallback(
|
||||||
async (name: string) => {
|
async (name: string, targetFolderId?: string) => {
|
||||||
try {
|
try {
|
||||||
const targetPath = getTargetFolderPath()
|
const targetPath = getTargetFolderPath(targetFolderId)
|
||||||
if (!targetPath) {
|
if (!targetPath) {
|
||||||
throw new Error('No folder path selected')
|
throw new Error('No folder path selected')
|
||||||
}
|
}
|
||||||
@@ -415,11 +419,11 @@ const NotesPage: FC = () => {
|
|||||||
|
|
||||||
// 创建笔记
|
// 创建笔记
|
||||||
const handleCreateNote = useCallback(
|
const handleCreateNote = useCallback(
|
||||||
async (name: string) => {
|
async (name: string, targetFolderId?: string) => {
|
||||||
try {
|
try {
|
||||||
isCreatingNoteRef.current = true
|
isCreatingNoteRef.current = true
|
||||||
|
|
||||||
const targetPath = getTargetFolderPath()
|
const targetPath = getTargetFolderPath(targetFolderId)
|
||||||
if (!targetPath) {
|
if (!targetPath) {
|
||||||
throw new Error('No folder path selected')
|
throw new Error('No folder path selected')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import { useSelector } from 'react-redux'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface NotesSidebarProps {
|
interface NotesSidebarProps {
|
||||||
onCreateFolder: (name: string, parentId?: string) => void
|
onCreateFolder: (name: string, targetFolderId?: string) => void
|
||||||
onCreateNote: (name: string, parentId?: string) => void
|
onCreateNote: (name: string, targetFolderId?: string) => void
|
||||||
onSelectNode: (node: NotesTreeNode) => void
|
onSelectNode: (node: NotesTreeNode) => void
|
||||||
onDeleteNode: (nodeId: string) => void
|
onDeleteNode: (nodeId: string) => void
|
||||||
onRenameNode: (nodeId: string, newName: string) => void
|
onRenameNode: (nodeId: string, newName: string) => void
|
||||||
@@ -71,6 +71,8 @@ interface TreeNodeProps {
|
|||||||
onDrop: (e: React.DragEvent, node: NotesTreeNode) => void
|
onDrop: (e: React.DragEvent, node: NotesTreeNode) => void
|
||||||
onDragEnd: () => void
|
onDragEnd: () => void
|
||||||
renderChildren?: boolean // 控制是否渲染子节点
|
renderChildren?: boolean // 控制是否渲染子节点
|
||||||
|
openDropdownKey: string | null
|
||||||
|
onDropdownOpenChange: (key: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const TreeNode = memo<TreeNodeProps>(
|
const TreeNode = memo<TreeNodeProps>(
|
||||||
@@ -94,7 +96,9 @@ const TreeNode = memo<TreeNodeProps>(
|
|||||||
onDragLeave,
|
onDragLeave,
|
||||||
onDrop,
|
onDrop,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
renderChildren = true
|
renderChildren = true,
|
||||||
|
openDropdownKey,
|
||||||
|
onDropdownOpenChange
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -119,8 +123,12 @@ const TreeNode = memo<TreeNodeProps>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={node.id}>
|
<div key={node.id}>
|
||||||
<Dropdown menu={{ items: getMenuItems(node) }} trigger={['contextMenu']}>
|
<Dropdown
|
||||||
<div>
|
menu={{ items: getMenuItems(node) }}
|
||||||
|
trigger={['contextMenu']}
|
||||||
|
open={openDropdownKey === node.id}
|
||||||
|
onOpenChange={(open) => onDropdownOpenChange(open ? node.id : null)}>
|
||||||
|
<div onContextMenu={(e) => e.stopPropagation()}>
|
||||||
<TreeNodeContainer
|
<TreeNodeContainer
|
||||||
active={isActive}
|
active={isActive}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
@@ -206,6 +214,8 @@ const TreeNode = memo<TreeNodeProps>(
|
|||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
renderChildren={renderChildren}
|
renderChildren={renderChildren}
|
||||||
|
openDropdownKey={openDropdownKey}
|
||||||
|
onDropdownOpenChange={onDropdownOpenChange}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -244,6 +254,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
const [isShowSearch, setIsShowSearch] = useState(false)
|
const [isShowSearch, setIsShowSearch] = useState(false)
|
||||||
const [searchKeyword, setSearchKeyword] = useState('')
|
const [searchKeyword, setSearchKeyword] = useState('')
|
||||||
const [isDragOverSidebar, setIsDragOverSidebar] = useState(false)
|
const [isDragOverSidebar, setIsDragOverSidebar] = useState(false)
|
||||||
|
const [openDropdownKey, setOpenDropdownKey] = useState<string | null>(null)
|
||||||
const dragNodeRef = useRef<HTMLDivElement | null>(null)
|
const dragNodeRef = useRef<HTMLDivElement | null>(null)
|
||||||
const scrollbarRef = useRef<any>(null)
|
const scrollbarRef = useRef<any>(null)
|
||||||
|
|
||||||
@@ -571,6 +582,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(
|
baseMenuItems.push(
|
||||||
{
|
{
|
||||||
label: t('notes.rename'),
|
label: t('notes.rename'),
|
||||||
@@ -674,7 +707,9 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
handleDeleteNode,
|
handleDeleteNode,
|
||||||
renamingNodeIds,
|
renamingNodeIds,
|
||||||
handleAutoRename,
|
handleAutoRename,
|
||||||
exportMenuOptions
|
exportMenuOptions,
|
||||||
|
onCreateNote,
|
||||||
|
onCreateFolder
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -755,6 +790,23 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
fileInput.click()
|
fileInput.click()
|
||||||
}, [onUploadFiles])
|
}, [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 (
|
return (
|
||||||
<SidebarContainer
|
<SidebarContainer
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
@@ -784,31 +836,90 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
|
|
||||||
<NotesTreeContainer>
|
<NotesTreeContainer>
|
||||||
{shouldUseVirtualization ? (
|
{shouldUseVirtualization ? (
|
||||||
<VirtualizedTreeContainer ref={parentRef}>
|
<Dropdown
|
||||||
<div
|
menu={{ items: getEmptyAreaMenuItems() }}
|
||||||
style={{
|
trigger={['contextMenu']}
|
||||||
height: `${virtualizer.getTotalSize()}px`,
|
open={openDropdownKey === 'empty-area'}
|
||||||
width: '100%',
|
onOpenChange={(open) => setOpenDropdownKey(open ? 'empty-area' : null)}>
|
||||||
position: 'relative'
|
<VirtualizedTreeContainer ref={parentRef}>
|
||||||
}}>
|
<div
|
||||||
{virtualizer.getVirtualItems().map((virtualItem) => {
|
style={{
|
||||||
const { node, depth } = flattenedNodes[virtualItem.index]
|
height: `${virtualizer.getTotalSize()}px`,
|
||||||
return (
|
width: '100%',
|
||||||
<div
|
position: 'relative'
|
||||||
key={virtualItem.key}
|
}}>
|
||||||
data-index={virtualItem.index}
|
{virtualizer.getVirtualItems().map((virtualItem) => {
|
||||||
ref={virtualizer.measureElement}
|
const { node, depth } = flattenedNodes[virtualItem.index]
|
||||||
style={{
|
return (
|
||||||
position: 'absolute',
|
<div
|
||||||
top: 0,
|
key={virtualItem.key}
|
||||||
left: 0,
|
data-index={virtualItem.index}
|
||||||
width: '100%',
|
ref={virtualizer.measureElement}
|
||||||
transform: `translateY(${virtualItem.start}px)`
|
style={{
|
||||||
}}>
|
position: 'absolute',
|
||||||
<div style={{ padding: '0 8px' }}>
|
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
|
<TreeNode
|
||||||
|
key={node.id}
|
||||||
node={node}
|
node={node}
|
||||||
depth={depth}
|
depth={0}
|
||||||
selectedFolderId={selectedFolderId}
|
selectedFolderId={selectedFolderId}
|
||||||
activeNodeId={activeNode?.id}
|
activeNodeId={activeNode?.id}
|
||||||
editingNodeId={editingNodeId}
|
editingNodeId={editingNodeId}
|
||||||
@@ -826,92 +937,51 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
renderChildren={false}
|
openDropdownKey={openDropdownKey}
|
||||||
|
onDropdownOpenChange={setOpenDropdownKey}
|
||||||
/>
|
/>
|
||||||
</div>
|
))
|
||||||
</div>
|
: notesTree.map((node) => (
|
||||||
)
|
<TreeNode
|
||||||
})}
|
key={node.id}
|
||||||
</div>
|
node={node}
|
||||||
{!isShowStarred && !isShowSearch && (
|
depth={0}
|
||||||
<DropHintNode>
|
selectedFolderId={selectedFolderId}
|
||||||
<TreeNodeContainer active={false} depth={0}>
|
activeNodeId={activeNode?.id}
|
||||||
<TreeNodeContent>
|
editingNodeId={editingNodeId}
|
||||||
<NodeIcon>
|
renamingNodeIds={renamingNodeIds}
|
||||||
<FilePlus size={16} />
|
newlyRenamedNodeIds={newlyRenamedNodeIds}
|
||||||
</NodeIcon>
|
draggedNodeId={draggedNodeId}
|
||||||
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
|
dragOverNodeId={dragOverNodeId}
|
||||||
</TreeNodeContent>
|
dragPosition={dragPosition}
|
||||||
</TreeNodeContainer>
|
inPlaceEdit={inPlaceEdit}
|
||||||
</DropHintNode>
|
getMenuItems={getMenuItems}
|
||||||
)}
|
onSelectNode={onSelectNode}
|
||||||
</VirtualizedTreeContainer>
|
onToggleExpanded={onToggleExpanded}
|
||||||
) : (
|
onDragStart={handleDragStart}
|
||||||
<StyledScrollbar ref={scrollbarRef}>
|
onDragOver={handleDragOver}
|
||||||
<TreeContent>
|
onDragLeave={handleDragLeave}
|
||||||
{isShowStarred || isShowSearch
|
onDrop={handleDrop}
|
||||||
? filteredTree.map((node) => (
|
onDragEnd={handleDragEnd}
|
||||||
<TreeNode
|
openDropdownKey={openDropdownKey}
|
||||||
key={node.id}
|
onDropdownOpenChange={setOpenDropdownKey}
|
||||||
node={node}
|
/>
|
||||||
depth={0}
|
))}
|
||||||
selectedFolderId={selectedFolderId}
|
{!isShowStarred && !isShowSearch && (
|
||||||
activeNodeId={activeNode?.id}
|
<DropHintNode>
|
||||||
editingNodeId={editingNodeId}
|
<TreeNodeContainer active={false} depth={0}>
|
||||||
renamingNodeIds={renamingNodeIds}
|
<TreeNodeContent>
|
||||||
newlyRenamedNodeIds={newlyRenamedNodeIds}
|
<NodeIcon>
|
||||||
draggedNodeId={draggedNodeId}
|
<FilePlus size={16} />
|
||||||
dragOverNodeId={dragOverNodeId}
|
</NodeIcon>
|
||||||
dragPosition={dragPosition}
|
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
|
||||||
inPlaceEdit={inPlaceEdit}
|
</TreeNodeContent>
|
||||||
getMenuItems={getMenuItems}
|
</TreeNodeContainer>
|
||||||
onSelectNode={onSelectNode}
|
</DropHintNode>
|
||||||
onToggleExpanded={onToggleExpanded}
|
)}
|
||||||
onDragStart={handleDragStart}
|
</TreeContent>
|
||||||
onDragOver={handleDragOver}
|
</StyledScrollbar>
|
||||||
onDragLeave={handleDragLeave}
|
</Dropdown>
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
</NotesTreeContainer>
|
</NotesTreeContainer>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { HStack, VStack } from '@renderer/components/Layout'
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import TranslateButton from '@renderer/components/TranslateButton'
|
import TranslateButton from '@renderer/components/TranslateButton'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { getProviderLogo } from '@renderer/config/providers'
|
||||||
import { LanguagesEnum } from '@renderer/config/translate'
|
import { LanguagesEnum } from '@renderer/config/translate'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||||
@@ -26,7 +27,7 @@ import { useAppDispatch } from '@renderer/store'
|
|||||||
import { setGenerating } from '@renderer/store/runtime'
|
import { setGenerating } from '@renderer/store/runtime'
|
||||||
import type { FileMetadata, Painting } from '@renderer/types'
|
import type { FileMetadata, Painting } from '@renderer/types'
|
||||||
import { getErrorMessage, uuid } from '@renderer/utils'
|
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 TextArea from 'antd/es/input/TextArea'
|
||||||
import { Info } from 'lucide-react'
|
import { Info } from 'lucide-react'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
@@ -388,7 +389,16 @@ const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
|||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
<LeftContainer>
|
<LeftContainer>
|
||||||
<SettingTitle style={{ marginBottom: 5 }}>{t('common.provider')}</SettingTitle>
|
<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>
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('common.model')}</SettingTitle>
|
||||||
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
<Select value={painting.model} options={modelOptions} onChange={onSelectModel} />
|
||||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>{t('paintings.image.size')}</SettingTitle>
|
||||||
@@ -662,4 +672,14 @@ const StyledInputNumber = styled(InputNumber)`
|
|||||||
width: 70px;
|
width: 70px;
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const SelectOptionContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProviderLogo = styled(Avatar)`
|
||||||
|
flex-shrink: 0;
|
||||||
|
`
|
||||||
|
|
||||||
export default SiliconPage
|
export default SiliconPage
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
import { GithubOutlined } from '@ant-design/icons'
|
import { GithubOutlined } from '@ant-design/icons'
|
||||||
|
import { useDisclosure } from '@heroui/react'
|
||||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import UpdateDialog from '@renderer/components/UpdateDialog'
|
||||||
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
import { APP_NAME, AppLogo } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import { handleSaveData, useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setUpdateState } from '@renderer/store/runtime'
|
import { setUpdateState } from '@renderer/store/runtime'
|
||||||
import { ThemeMode } from '@renderer/types'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
import { UpgradeChannel } from '@shared/config/constant'
|
import { UpgradeChannel } from '@shared/config/constant'
|
||||||
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
|
import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd'
|
||||||
|
import { UpdateInfo } from 'builder-util-runtime'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react'
|
import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react'
|
||||||
import { BadgeQuestionMark } from 'lucide-react'
|
import { BadgeQuestionMark } from 'lucide-react'
|
||||||
@@ -27,6 +30,8 @@ import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitl
|
|||||||
const AboutSettings: FC = () => {
|
const AboutSettings: FC = () => {
|
||||||
const [version, setVersion] = useState('')
|
const [version, setVersion] = useState('')
|
||||||
const [isPortable, setIsPortable] = useState(false)
|
const [isPortable, setIsPortable] = useState(false)
|
||||||
|
const [updateDialogInfo, setUpdateDialogInfo] = useState<UpdateInfo | null>(null)
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
|
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
@@ -41,8 +46,9 @@ const AboutSettings: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (update.downloaded) {
|
if (update.downloaded) {
|
||||||
await handleSaveData()
|
// Open update dialog directly in renderer
|
||||||
window.api.showUpdateDialog()
|
setUpdateDialogInfo(update.info || null)
|
||||||
|
onOpen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,6 +347,9 @@ const AboutSettings: FC = () => {
|
|||||||
<Button onClick={debug}>{t('settings.about.debug.open')}</Button>
|
<Button onClick={debug}>{t('settings.about.debug.open')}</Button>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
|
|
||||||
|
{/* Update Dialog */}
|
||||||
|
<UpdateDialog isOpen={isOpen} onClose={onClose} releaseInfo={updateDialogInfo} />
|
||||||
</SettingContainer>
|
</SettingContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
WebSearchProviderResult,
|
WebSearchProviderResult,
|
||||||
WebSearchStatus
|
WebSearchStatus
|
||||||
} from '@renderer/types'
|
} from '@renderer/types'
|
||||||
import { hasObjectKey, uuid } from '@renderer/utils'
|
import { hasObjectKey, removeSpecialCharactersForFileName, uuid } from '@renderer/utils'
|
||||||
import { addAbortController } from '@renderer/utils/abortController'
|
import { addAbortController } from '@renderer/utils/abortController'
|
||||||
import { formatErrorMessage } from '@renderer/utils/error'
|
import { formatErrorMessage } from '@renderer/utils/error'
|
||||||
import { ExtractResults } from '@renderer/utils/extract'
|
import { ExtractResults } from '@renderer/utils/extract'
|
||||||
@@ -55,7 +55,7 @@ class WebSearchService {
|
|||||||
dispose: (requestState: RequestState, requestId: string) => {
|
dispose: (requestState: RequestState, requestId: string) => {
|
||||||
if (!requestState.searchBase) return
|
if (!requestState.searchBase) return
|
||||||
window.api.knowledgeBase
|
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))
|
.catch((error) => logger.warn(`Failed to cleanup search base for ${requestId}:`, error))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -216,6 +216,7 @@ class WebSearchService {
|
|||||||
documentCount: number,
|
documentCount: number,
|
||||||
requestId: string
|
requestId: string
|
||||||
): Promise<KnowledgeBase> {
|
): Promise<KnowledgeBase> {
|
||||||
|
// requestId: eg: openai-responses-openai/gpt-5-timestamp-uuid
|
||||||
const baseId = `websearch-compression-${requestId}`
|
const baseId = `websearch-compression-${requestId}`
|
||||||
const state = this.getRequestState(requestId)
|
const state = this.getRequestState(requestId)
|
||||||
|
|
||||||
@@ -226,7 +227,8 @@ class WebSearchService {
|
|||||||
|
|
||||||
// 清理旧的知识库
|
// 清理旧的知识库
|
||||||
if (state.searchBase) {
|
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) {
|
if (!config.embeddingModel) {
|
||||||
@@ -462,7 +464,9 @@ class WebSearchService {
|
|||||||
|
|
||||||
// 处理 summarize
|
// 处理 summarize
|
||||||
if (questions[0] === 'summarize' && links && links.length > 0) {
|
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 &&
|
webSearchProvider.topicId &&
|
||||||
endSpan({
|
endSpan({
|
||||||
topicId: webSearchProvider.topicId,
|
topicId: webSearchProvider.topicId,
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ export class BlockManager {
|
|||||||
newMessagesActions.upsertBlockReference({
|
newMessagesActions.upsertBlockReference({
|
||||||
messageId: this.deps.assistantMsgId,
|
messageId: this.deps.assistantMsgId,
|
||||||
blockId: newBlock.id,
|
blockId: newBlock.id,
|
||||||
status: newBlock.status
|
status: newBlock.status,
|
||||||
|
blockType: newBlock.type
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 159,
|
version: 160,
|
||||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
import {
|
import { FileMetadata, KnowledgeBase, KnowledgeItem, PreprocessProvider, ProcessingStatus } from '@renderer/types'
|
||||||
FileMetadata,
|
|
||||||
KnowledgeBase,
|
|
||||||
KnowledgeBaseParams,
|
|
||||||
KnowledgeItem,
|
|
||||||
PreprocessProvider,
|
|
||||||
ProcessingStatus
|
|
||||||
} from '@renderer/types'
|
|
||||||
|
|
||||||
const logger = loggerService.withContext('Store:Knowledge')
|
const logger = loggerService.withContext('Store:Knowledge')
|
||||||
|
|
||||||
@@ -28,13 +21,13 @@ const knowledgeSlice = createSlice({
|
|||||||
state.bases.push(action.payload)
|
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)
|
const base = state.bases.find((b) => b.id === action.payload.baseId)
|
||||||
if (base) {
|
if (base) {
|
||||||
state.bases = state.bases.filter((b) => b.id !== action.payload.baseId)
|
state.bases = state.bases.filter((b) => b.id !== action.payload.baseId)
|
||||||
const files = base.items.filter((item) => item.type === 'file')
|
const files = base.items.filter((item) => item.type === 'file')
|
||||||
FileManager.deleteFiles(files.map((item) => item.content) as FileMetadata[])
|
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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2568,6 +2568,19 @@ const migrateConfig = {
|
|||||||
logger.error('migrate 159 error', error as Error)
|
logger.error('migrate 159 error', error as Error)
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'160': (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 160 error', error as Error)
|
||||||
|
return state
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { loggerService } from '@logger'
|
|||||||
import { createEntityAdapter, createSlice, EntityState, PayloadAction } from '@reduxjs/toolkit'
|
import { createEntityAdapter, createSlice, EntityState, PayloadAction } from '@reduxjs/toolkit'
|
||||||
// Separate type-only imports from value imports
|
// Separate type-only imports from value imports
|
||||||
import type { Message } from '@renderer/types/newMessage'
|
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')
|
const logger = loggerService.withContext('newMessage')
|
||||||
|
|
||||||
@@ -50,6 +50,7 @@ interface UpsertBlockReferencePayload {
|
|||||||
messageId: string
|
messageId: string
|
||||||
blockId: string
|
blockId: string
|
||||||
status?: MessageBlockStatus
|
status?: MessageBlockStatus
|
||||||
|
blockType?: MessageBlockType
|
||||||
}
|
}
|
||||||
|
|
||||||
// Payload for removing a single message
|
// Payload for removing a single message
|
||||||
@@ -217,7 +218,7 @@ export const messagesSlice = createSlice({
|
|||||||
messagesAdapter.removeMany(state, messageIds)
|
messagesAdapter.removeMany(state, messageIds)
|
||||||
},
|
},
|
||||||
upsertBlockReference(state, action: PayloadAction<UpsertBlockReferencePayload>) {
|
upsertBlockReference(state, action: PayloadAction<UpsertBlockReferencePayload>) {
|
||||||
const { messageId, blockId, status } = action.payload
|
const { messageId, blockId, status, blockType } = action.payload
|
||||||
|
|
||||||
const messageToUpdate = state.entities[messageId]
|
const messageToUpdate = state.entities[messageId]
|
||||||
if (!messageToUpdate) {
|
if (!messageToUpdate) {
|
||||||
@@ -230,7 +231,11 @@ export const messagesSlice = createSlice({
|
|||||||
// Update Block ID
|
// Update Block ID
|
||||||
const currentBlocks = messageToUpdate.blocks || []
|
const currentBlocks = messageToUpdate.blocks || []
|
||||||
if (!currentBlocks.includes(blockId)) {
|
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
|
// Update Message Status based on Block Status
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class StreamHandler {
|
|||||||
this.usage.total_tokens += completionChunk.usage.total_tokens || 0
|
this.usage.total_tokens += completionChunk.usage.total_tokens || 0
|
||||||
}
|
}
|
||||||
context = chunk.choices
|
context = chunk.choices
|
||||||
.map((choice) => {
|
?.map((choice) => {
|
||||||
if (!choice.delta) {
|
if (!choice.delta) {
|
||||||
return ''
|
return ''
|
||||||
} else if ('reasoning_content' in choice.delta) {
|
} else if ('reasoning_content' in choice.delta) {
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ export type Provider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SystemProviderIds = {
|
export const SystemProviderIds = {
|
||||||
// cherryin: 'cherryin',
|
cherryin: 'cherryin',
|
||||||
silicon: 'silicon',
|
silicon: 'silicon',
|
||||||
aihubmix: 'aihubmix',
|
aihubmix: 'aihubmix',
|
||||||
ovms: 'ovms',
|
ovms: 'ovms',
|
||||||
|
|||||||
12
yarn.lock
12
yarn.lock
@@ -167,6 +167,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.17#~/.yarn/patches/@ai-sdk-google-npm-2.0.17-fd88491de4.patch":
|
||||||
|
version: 2.0.17
|
||||||
|
resolution: "@ai-sdk/google@patch:@ai-sdk/google@npm%3A2.0.17#~/.yarn/patches/@ai-sdk-google-npm-2.0.17-fd88491de4.patch::version=2.0.17&hash=620163"
|
||||||
|
dependencies:
|
||||||
|
"@ai-sdk/provider": "npm:2.0.0"
|
||||||
|
"@ai-sdk/provider-utils": "npm:3.0.10"
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.25.76 || ^4.1.8
|
||||||
|
checksum: 10c0/5a02f9becfc956607b54f269d5bfc71c76fee1aa201632cbd8266be53bb837454e4096f9b217e631957f5e1c85d7949029d25b8abfccdd2fe93fe205042b76a5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@ai-sdk/mistral@npm:^2.0.17":
|
"@ai-sdk/mistral@npm:^2.0.17":
|
||||||
version: 2.0.17
|
version: 2.0.17
|
||||||
resolution: "@ai-sdk/mistral@npm:2.0.17"
|
resolution: "@ai-sdk/mistral@npm:2.0.17"
|
||||||
|
|||||||
Reference in New Issue
Block a user