Merge branch 'develop' into mutiple-select

This commit is contained in:
自由的世界人 2025-05-17 22:41:59 +08:00 committed by GitHub
commit 6292ced495
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 1613 additions and 751 deletions

View File

@ -91,6 +91,7 @@ afterSign: scripts/notarize.js
artifactBuildCompleted: scripts/artifact-build-completed.js
releaseInfo:
releaseNotes: |
⚠️ 注意:升级前请备份数据,否则将无法降级
重构消息结构,支持不同类型消息按时间顺序显示
智能体支持导入和导出
快捷面板增加网络搜索引擎选择

View File

@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.3.4",
"version": "1.3.5",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@ -74,9 +74,6 @@
"@strongtz/win32-arm64-msvc": "^0.4.7",
"@tanstack/react-query": "^5.27.0",
"@types/react-infinite-scroll-component": "^5.0.0",
"@uiw/codemirror-extensions-langs": "^4.23.12",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"archiver": "^7.0.1",
"async-mutex": "^0.5.0",
"color": "^5.0.0",
@ -94,13 +91,10 @@
"got-scraping": "^4.1.1",
"jsdom": "^26.0.0",
"markdown-it": "^14.1.0",
"mermaid": "^11.6.0",
"node-stream-zip": "^1.15.0",
"officeparser": "^4.1.1",
"os-proxy-config": "^1.1.2",
"proxy-agent": "^6.5.0",
"rc-virtual-list": "^3.18.6",
"react-window": "^1.8.11",
"tar": "^7.4.3",
"turndown": "^7.2.0",
"turndown-plugin-gfm": "^1.0.2",
@ -123,17 +117,14 @@
"@eslint/js": "^9.22.0",
"@google/genai": "^0.13.0",
"@hello-pangea/dnd": "^16.6.0",
"@iconify-json/svg-spinners": "^1.2.2",
"@kangfenmao/keyv-storage": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.11.3",
"@mozilla/readability": "^0.6.0",
"@notionhq/client": "^2.2.15",
"@reduxjs/toolkit": "^2.2.5",
"@shikijs/markdown-it": "^3.2.2",
"@swc/plugin-styled-components": "^7.1.3",
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
"@swc/plugin-styled-components": "^7.1.5",
"@tryfabric/martian": "^1.2.4",
"@types/adm-zip": "^0",
"@types/diff": "^7",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
@ -147,15 +138,15 @@
"@types/react-window": "^1",
"@types/tinycolor2": "^1",
"@types/ws": "^8",
"@uiw/codemirror-extensions-langs": "^4.23.12",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
"@vitejs/plugin-react-swc": "^3.9.0",
"@vitest/coverage-v8": "^3.1.1",
"@vitest/ui": "^3.1.1",
"@vitest/web-worker": "^3.1.3",
"@xyflow/react": "^12.4.4",
"antd": "^5.22.5",
"applescript": "^1.0.0",
"axios": "^1.7.3",
"babel-plugin-styled-components": "^2.1.4",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11",
"dexie": "^4.0.8",
@ -179,12 +170,14 @@
"lodash": "^4.17.21",
"lru-cache": "^11.1.0",
"lucide-react": "^0.487.0",
"mermaid": "^11.6.0",
"mime": "^4.0.4",
"motion": "^12.10.5",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"p-queue": "^8.1.0",
"prettier": "^3.5.3",
"rc-virtual-list": "^3.18.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hotkeys-hook": "^4.6.1",
@ -195,6 +188,7 @@
"react-router": "6",
"react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-window": "^1.8.11",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"rehype-katex": "^7.0.1",
@ -204,12 +198,11 @@
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "^1.77.2",
"sass": "^1.88.0",
"shiki": "^3.2.2",
"string-width": "^7.2.0",
"styled-components": "^6.1.11",
"tiny-pinyin": "^1.3.2",
"tinycolor2": "^1.6.0",
"tokenx": "^0.4.1",
"typescript": "^5.6.2",
"uuid": "^10.0.0",

View File

@ -1,3 +1,5 @@
import '@main/config'
import { electronApp, optimizer } from '@electron-toolkit/utils'
import { replaceDevtoolsFont } from '@main/utils/windowUtil'
import { IpcChannel } from '@shared/IpcChannel'
@ -42,7 +44,7 @@ if (!app.requestSingleInstanceLock()) {
} else {
// Portable dir must be setup before app ready
setUserDataDir()
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.

View File

@ -77,7 +77,8 @@ class BackupManager {
_: Electron.IpcMainInvokeEvent,
fileName: string,
data: string,
destinationPath: string = this.backupDir
destinationPath: string = this.backupDir,
skipBackupFile: boolean = false
): Promise<string> {
const mainWindow = windowService.getMainWindow()
@ -104,23 +105,30 @@ class BackupManager {
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
// 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
Logger.log('[BackupManager IPC] ', skipBackupFile)
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
if (!skipBackupFile) {
// 复制 Data 目录到临时目录
const sourcePath = path.join(app.getPath('userData'), 'Data')
const tempDataDir = path.join(this.tempDir, 'Data')
// 使用流式复制
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
copiedSize += size
const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
// 获取源目录总大小
const totalSize = await this.getDirSize(sourcePath)
let copiedSize = 0
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
// 使用流式复制
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
copiedSize += size
const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50))
onProgress({ stage: 'copying_files', progress, total: 100 })
})
await this.setWritableRecursive(tempDataDir)
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
} else {
Logger.log('[BackupManager] Skip the backup of the file')
await fs.promises.mkdir(path.join(this.tempDir, 'Data')) // 不创建空 Data 目录会导致 restore 失败
}
// 创建输出文件流
const backupedFilePath = path.join(destinationPath, fileName)
@ -279,7 +287,7 @@ class BackupManager {
async backupToWebdav(_: Electron.IpcMainInvokeEvent, data: string, webdavConfig: WebDavConfig) {
const filename = webdavConfig.fileName || 'cherry-studio.backup.zip'
const backupedFilePath = await this.backup(_, filename, data)
const backupedFilePath = await this.backup(_, filename, data, undefined, webdavConfig.skipBackupFile)
const webdavClient = new WebDav(webdavConfig)
try {
const result = await webdavClient.putFileContents(filename, fs.createReadStream(backupedFilePath), {

View File

@ -62,7 +62,7 @@ export class ConfigManager {
}
getTrayOnClose(): boolean {
return !!this.get(ConfigKeys.TrayOnClose, true)
return !!this.get(ConfigKeys.TrayOnClose, false)
}
setTrayOnClose(value: boolean) {

View File

@ -1,5 +1,7 @@
import { BrowserWindow } from 'electron'
import { isDev, isWin } from '../constant'
function isTilingWindowManager() {
if (process.platform === 'darwin') {
return false
@ -15,31 +17,59 @@ function isTilingWindowManager() {
return tilingSystems.some((system) => desktopEnv?.includes(system))
}
//see: https://github.com/electron/electron/issues/42055#issuecomment-2449365647
export const replaceDevtoolsFont = (browserWindow: BrowserWindow) => {
if (process.platform === 'win32') {
//only for windows and dev, don't do this in production to avoid performance issues
if (isWin && isDev) {
browserWindow.webContents.on('devtools-opened', () => {
const css = `
:root {
--sys-color-base: var(--ref-palette-neutral100);
--source-code-font-family: consolas;
--source-code-font-family: consolas !important;
--source-code-font-size: 12px;
--monospace-font-family: consolas;
--monospace-font-family: consolas !important;
--monospace-font-size: 12px;
--default-font-family: system-ui, sans-serif;
--default-font-size: 12px;
--ref-palette-neutral99: #ffffffff;
}
.-theme-with-dark-background {
.theme-with-dark-background {
--sys-color-base: var(--ref-palette-secondary25);
}
body {
--default-font-family: system-ui,sans-serif;
}`
--default-font-family: system-ui, sans-serif;
}
`
browserWindow.webContents.devToolsWebContents?.executeJavaScript(`
const overriddenStyle = document.createElement('style');
overriddenStyle.innerHTML = '${css.replaceAll('\n', ' ')}';
document.body.append(overriddenStyle);
document.body.classList.remove('platform-windows');`)
document.querySelectorAll('.platform-windows').forEach(el => el.classList.remove('platform-windows'));
addStyleToAutoComplete();
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
for (let i = 0; i < mutation.addedNodes.length; i++) {
const item = mutation.addedNodes[i];
if (item.classList.contains('editor-tooltip-host')) {
addStyleToAutoComplete();
}
}
}
}
});
observer.observe(document.body, {childList: true});
function addStyleToAutoComplete() {
document.querySelectorAll('.editor-tooltip-host').forEach(element => {
if (element.shadowRoot.querySelectorAll('[data-key="overridden-dev-tools-font"]').length === 0) {
const overriddenStyle = document.createElement('style');
overriddenStyle.setAttribute('data-key', 'overridden-dev-tools-font');
overriddenStyle.innerHTML = '.cm-tooltip-autocomplete ul[role=listbox] {font-family: consolas !important;}';
element.shadowRoot.append(overriddenStyle);
}
});
}
`)
})
}
}

View File

@ -2,7 +2,7 @@ import type { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
import { electronAPI } from '@electron-toolkit/preload'
import { IpcChannel } from '@shared/IpcChannel'
import { FileType, KnowledgeBaseParams, KnowledgeItem, MCPServer, Shortcut, WebDavConfig } from '@types'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell } from 'electron'
import { contextBridge, ipcRenderer, OpenDialogOptions, shell, webUtils } from 'electron'
import { CreateDirectoryOptions } from 'webdav'
// Custom APIs for renderer
@ -37,8 +37,8 @@ const api = {
decompress: (text: Buffer) => ipcRenderer.invoke(IpcChannel.Zip_Decompress, text)
},
backup: {
backup: (fileName: string, data: string, destinationPath?: string) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath),
backup: (fileName: string, data: string, destinationPath?: string, skipBackupFile?: boolean) =>
ipcRenderer.invoke(IpcChannel.Backup_Backup, fileName, data, destinationPath, skipBackupFile),
restore: (backupPath: string) => ipcRenderer.invoke(IpcChannel.Backup_Restore, backupPath),
backupToWebdav: (data: string, webdavConfig: WebDavConfig) =>
ipcRenderer.invoke(IpcChannel.Backup_BackupToWebdav, data, webdavConfig),
@ -73,7 +73,8 @@ const api = {
download: (url: string) => ipcRenderer.invoke(IpcChannel.File_Download, url),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
binaryImage: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryImage, fileId),
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId)
base64File: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64File, fileId),
getPathForFile: (file: File) => webUtils.getPathForFile(file)
},
fs: {
read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)

View File

@ -0,0 +1,130 @@
:root {
--color-white: #ffffff;
--color-white-soft: rgba(255, 255, 255, 0.8);
--color-white-mute: rgba(255, 255, 255, 0.94);
--color-black: #181818;
--color-black-soft: #222222;
--color-black-mute: #333333;
--color-gray-1: #515c67;
--color-gray-2: #414853;
--color-gray-3: #32363f;
--color-text-1: rgba(255, 255, 245, 0.9);
--color-text-2: rgba(235, 235, 245, 0.6);
--color-text-3: rgba(235, 235, 245, 0.38);
--color-background: var(--color-black);
--color-background-soft: var(--color-black-soft);
--color-background-mute: var(--color-black-mute);
--color-background-opacity: rgba(34, 34, 34, 0.7);
--inner-glow-opacity: 0.3; // For the glassmorphism effect in the dropdown menu
--color-primary: #00b96b;
--color-primary-soft: #00b96b99;
--color-primary-mute: #00b96b33;
--color-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff19;
--color-border-soft: #ffffff10;
--color-border-mute: #ffffff05;
--color-error: #f44336;
--color-link: #338cff;
--color-code-background: #323232;
--color-hover: rgba(40, 40, 40, 1);
--color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333;
--color-group-background: var(--color-background-soft);
--color-reference: #404040;
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
--modal-background: #1f1f1f;
--color-highlight: rgba(0, 0, 0, 1);
--color-background-highlight: rgba(255, 255, 0, 0.9);
--color-background-highlight-accent: rgba(255, 150, 50, 0.9);
--navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background: #1f1f1f;
--navbar-height: 40px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
--assistants-width: 275px;
--topic-list-width: 275px;
--settings-width: 250px;
--chat-background: #111111;
--chat-background-user: #28b561;
--chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black);
--list-item-border-radius: 16px;
}
[theme-mode='light'] {
--color-white: #ffffff;
--color-white-soft: rgba(0, 0, 0, 0.04);
--color-white-mute: #eee;
--color-black: #1b1b1f;
--color-black-soft: #262626;
--color-black-mute: #363636;
--color-gray-1: #8e8e93;
--color-gray-2: #aeaeb2;
--color-gray-3: #c7c7cc;
--color-text-1: rgba(0, 0, 0, 1);
--color-text-2: rgba(0, 0, 0, 0.6);
--color-text-3: rgba(0, 0, 0, 0.38);
--color-background: var(--color-white);
--color-background-soft: var(--color-white-soft);
--color-background-mute: var(--color-white-mute);
--color-background-opacity: rgba(235, 235, 235, 0.7);
--inner-glow-opacity: 0.1;
--color-primary: #00b96b;
--color-primary-soft: #00b96b99;
--color-primary-mute: #00b96b33;
--color-text: var(--color-text-1);
--color-icon: #00000099;
--color-icon-white: #000000;
--color-border: #00000019;
--color-border-soft: #00000010;
--color-border-mute: #00000005;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #e3e3e3;
--color-hover: var(--color-white-mute);
--color-active: var(--color-white-soft);
--color-frame-border: #ddd;
--color-group-background: var(--color-white);
--color-reference: #cfe1ff;
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
--modal-background: var(--color-white);
--color-highlight: initial;
--color-background-highlight: rgba(255, 255, 0, 0.5);
--color-background-highlight-accent: rgba(255, 150, 50, 0.5);
--navbar-background-mac: rgba(255, 255, 255, 0.55);
--navbar-background: rgba(244, 244, 244);
--chat-background: #f3f3f3;
--chat-background-user: #95ec69;
--chat-background-assistant: #ffffff;
--chat-text-user: var(--color-text);
}

View File

@ -0,0 +1,12 @@
:root {
--font-family:
Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-family-serif:
serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, Ubuntu, Roboto, Oxygen, Cantarell, 'Open Sans',
'Helvetica Neue', Arial, 'Noto Sans', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--code-font-family: 'Cascadia Code', 'Fira Code', 'Consolas', Menlo, Courier, monospace;
}

View File

@ -1,3 +1,5 @@
@use './color.scss';
@use './font.scss';
@use './markdown.scss';
@use './ant.scss';
@use './scrollbar.scss';
@ -6,73 +8,6 @@
@import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/ubuntu/ubuntu.css';
:root {
--color-white: #ffffff;
--color-white-soft: rgba(255, 255, 255, 0.8);
--color-white-mute: rgba(255, 255, 255, 0.94);
--color-black: #181818;
--color-black-soft: #222222;
--color-black-mute: #333333;
--color-gray-1: #515c67;
--color-gray-2: #414853;
--color-gray-3: #32363f;
--color-text-1: rgba(255, 255, 245, 0.9);
--color-text-2: rgba(235, 235, 245, 0.6);
--color-text-3: rgba(235, 235, 245, 0.38);
--color-background: var(--color-black);
--color-background-soft: var(--color-black-soft);
--color-background-mute: var(--color-black-mute);
--color-background-opacity: rgba(34, 34, 34, 0.7);
--inner-glow-opacity: 0.3; // For the glassmorphism effect in the dropdown menu
--color-primary: #00b96b;
--color-primary-soft: #00b96b99;
--color-primary-mute: #00b96b33;
--color-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff19;
--color-border-soft: #ffffff10;
--color-border-mute: #ffffff05;
--color-error: #f44336;
--color-link: #338cff;
--color-code-background: #323232;
--color-hover: rgba(40, 40, 40, 1);
--color-active: rgba(55, 55, 55, 1);
--color-frame-border: #333;
--color-group-background: var(--color-background-soft);
--color-reference: #404040;
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
--modal-background: #1f1f1f;
--navbar-background-mac: rgba(20, 20, 20, 0.55);
--navbar-background: #1f1f1f;
--navbar-height: 40px;
--sidebar-width: 50px;
--status-bar-height: 40px;
--input-bar-height: 100px;
--assistants-width: 275px;
--topic-list-width: 275px;
--settings-width: 250px;
--chat-background: #111111;
--chat-background-user: #28b561;
--chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black);
--list-item-border-radius: 16px;
}
body {
-webkit-user-select: none;
-moz-user-select: none;
@ -80,62 +15,6 @@ body {
user-select: none;
}
body[theme-mode='light'] {
--color-white: #ffffff;
--color-white-soft: rgba(0, 0, 0, 0.04);
--color-white-mute: #eee;
--color-black: #1b1b1f;
--color-black-soft: #262626;
--color-black-mute: #363636;
--color-gray-1: #8e8e93;
--color-gray-2: #aeaeb2;
--color-gray-3: #c7c7cc;
--color-text-1: rgba(0, 0, 0, 1);
--color-text-2: rgba(0, 0, 0, 0.6);
--color-text-3: rgba(0, 0, 0, 0.38);
--color-background: var(--color-white);
--color-background-soft: var(--color-white-soft);
--color-background-mute: var(--color-white-mute);
--color-background-opacity: rgba(235, 235, 235, 0.7);
--inner-glow-opacity: 0.1;
--color-primary: #00b96b;
--color-primary-soft: #00b96b99;
--color-primary-mute: #00b96b33;
--color-text: var(--color-text-1);
--color-icon: #00000099;
--color-icon-white: #000000;
--color-border: #00000019;
--color-border-soft: #00000010;
--color-border-mute: #00000005;
--color-error: #f44336;
--color-link: #1677ff;
--color-code-background: #e3e3e3;
--color-hover: var(--color-white-mute);
--color-active: var(--color-white-soft);
--color-frame-border: #ddd;
--color-group-background: var(--color-white);
--color-reference: #cfe1ff;
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
--modal-background: var(--color-white);
--navbar-background-mac: rgba(255, 255, 255, 0.55);
--navbar-background: rgba(244, 244, 244);
--chat-background: #f3f3f3;
--chat-background-user: #95ec69;
--chat-background-assistant: #ffffff;
--chat-text-user: var(--color-text);
}
*,
*::before,
*::after {
@ -163,9 +42,7 @@ body {
font-size: 14px;
line-height: 1.6;
overflow: hidden;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
font-family: var(--font-family);
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@ -291,3 +168,11 @@ body,
.lucide {
color: var(--color-icon);
}
span.highlight {
background-color: var(--color-background-highlight);
color: var(--color-highlight);
}
span.highlight.selected {
background-color: var(--color-background-highlight-accent);
}

View File

@ -20,10 +20,8 @@
h5,
h6 {
margin: 1em 0 1em 0;
font-weight: 800;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
font-weight: bold;
font-family: var(--font-family);
}
h1 {
@ -117,7 +115,7 @@
}
code {
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
font-family: var(--code-font-family);
}
pre {
@ -155,7 +153,7 @@
padding-left: 1em;
color: var(--color-text-light);
border-left: 4px solid var(--color-border);
font-family: Georgia, 'Times New Roman', Times, serif;
font-family: var(--font-family);
}
table {
@ -173,9 +171,7 @@
th {
background-color: var(--color-background-mute);
font-weight: bold;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
font-family: var(--font-family);
}
img {
@ -310,7 +306,7 @@ mjx-container {
/* CodeMirror 相关样式 */
.cm-editor {
.cm-scroller {
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
font-family: var(--code-font-family);
padding: 1px;
border-radius: 5px;

View File

@ -0,0 +1,710 @@
import { ToolbarButton } from '@renderer/pages/home/Inputbar/Inputbar'
import NarrowLayout from '@renderer/pages/home/Messages/NarrowLayout'
import { Tooltip } from 'antd'
import { debounce } from 'lodash'
import { CaseSensitive, ChevronDown, ChevronUp, User, WholeWord, X } from 'lucide-react'
import React, { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
const HIGHLIGHT_CLASS = 'highlight'
const HIGHLIGHT_SELECT_CLASS = 'selected'
interface Props {
children?: React.ReactNode
searchTarget: React.RefObject<React.ReactNode> | React.RefObject<HTMLElement> | HTMLElement
/**
* `node``node``Node.TEXT_NODE`
*
* `true``node`
*/
filter: (node: Node) => boolean
includeUser?: boolean
onIncludeUserChange?: (value: boolean) => void
}
enum SearchCompletedState {
NotSearched,
FirstSearched
}
enum SearchTargetIndex {
Next,
Prev
}
export interface ContentSearchRef {
disable(): void
enable(initialText?: string): void
// 搜索下一个并定位
searchNext(): void
// 搜索上一个并定位
searchPrev(): void
// 搜索并定位
search(): void
// 搜索但不定位,或者说是更新
silentSearch(): void
focus(): void
}
interface MatchInfo {
index: number
length: number
text: string
}
const escapeRegExp = (string: string): string => {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
}
const findWindowVerticalCenterElementIndex = (elementList: HTMLElement[]): number | null => {
if (!elementList || elementList.length === 0) {
return null
}
let closestElementIndex: number | null = null
let minVerticalDistance = Infinity
const windowCenterY = window.innerHeight / 2
for (let i = 0; i < elementList.length; i++) {
const element = elementList[i]
if (!(element instanceof HTMLElement)) {
continue
}
const rect = element.getBoundingClientRect()
if (rect.bottom < 0 || rect.top > window.innerHeight) {
continue
}
const elementCenterY = rect.top + rect.height / 2
const verticalDistance = Math.abs(elementCenterY - windowCenterY)
if (verticalDistance < minVerticalDistance) {
minVerticalDistance = verticalDistance
closestElementIndex = i
}
}
return closestElementIndex
}
const highlightText = (
textNode: Node,
searchText: string,
highlightClass: string,
isCaseSensitive: boolean,
isWholeWord: boolean
): HTMLSpanElement[] | null => {
const textNodeParentNode: HTMLElement | null = textNode.parentNode as HTMLElement
if (textNodeParentNode) {
if (textNodeParentNode.classList.contains(highlightClass)) {
return null
}
}
if (textNode.nodeType !== Node.TEXT_NODE || !textNode.textContent) {
return null
}
const textContent = textNode.textContent
const escapedSearchText = escapeRegExp(searchText)
// 检查搜索文本是否仅包含拉丁字母
const hasOnlyLatinLetters = /^[a-zA-Z\s]+$/.test(searchText)
// 只有当搜索文本仅包含拉丁字母时才应用大小写敏感
const regexFlags = hasOnlyLatinLetters && isCaseSensitive ? 'g' : 'gi'
const regexPattern = isWholeWord ? `\\b${escapedSearchText}\\b` : escapedSearchText
const regex = new RegExp(regexPattern, regexFlags)
let match
const matches: MatchInfo[] = []
while ((match = regex.exec(textContent)) !== null) {
if (typeof match.index === 'number' && typeof match[0] === 'string') {
matches.push({ index: match.index, length: match[0].length, text: match[0] })
} else {
console.error('Unexpected match format:', match)
}
}
if (matches.length === 0) {
return null
}
const parentNode = textNode.parentNode
if (!parentNode) {
return null
}
const fragment = document.createDocumentFragment()
let currentIndex = 0
const highlightTextSet = new Set<HTMLSpanElement>()
matches.forEach(({ index, length, text }) => {
if (index > currentIndex) {
fragment.appendChild(document.createTextNode(textContent.substring(currentIndex, index)))
}
const highlightSpan = document.createElement('span')
highlightSpan.className = highlightClass
highlightSpan.textContent = text // Use the matched text to preserve case if not case-sensitive
fragment.appendChild(highlightSpan)
highlightTextSet.add(highlightSpan)
currentIndex = index + length
})
if (currentIndex < textContent.length) {
fragment.appendChild(document.createTextNode(textContent.substring(currentIndex)))
}
parentNode.replaceChild(fragment, textNode)
return [...highlightTextSet]
}
const mergeAdjacentTextNodes = (node: HTMLElement) => {
const children = Array.from(node.childNodes)
const groups: Array<Node | { text: string; nodes: Node[] }> = []
let currentTextGroup: { text: string; nodes: Node[] } | null = null
for (const child of children) {
if (child.nodeType === Node.TEXT_NODE) {
if (currentTextGroup === null) {
currentTextGroup = {
text: child.textContent ?? '',
nodes: [child]
}
} else {
currentTextGroup.text += child.textContent
currentTextGroup.nodes.push(child)
}
} else {
if (currentTextGroup !== null) {
groups.push(currentTextGroup!)
currentTextGroup = null
}
groups.push(child)
}
}
if (currentTextGroup !== null) {
groups.push(currentTextGroup)
}
const newChildren = groups.map((group) => {
if (group instanceof Node) {
return group
} else {
return document.createTextNode(group.text)
}
})
node.replaceChildren(...newChildren)
}
// eslint-disable-next-line @eslint-react/no-forward-ref
export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
({ searchTarget, filter, includeUser = false, onIncludeUserChange }, ref) => {
const target: HTMLElement | null = (() => {
if (searchTarget instanceof HTMLElement) {
return searchTarget
} else {
return (searchTarget.current as HTMLElement) ?? null
}
})()
const containerRef = React.useRef<HTMLDivElement>(null)
const searchInputRef = React.useRef<HTMLInputElement>(null)
const [searchResultIndex, setSearchResultIndex] = useState(0)
const [totalCount, setTotalCount] = useState(0)
const [enableContentSearch, setEnableContentSearch] = useState(false)
const [searchCompleted, setSearchCompleted] = useState(SearchCompletedState.NotSearched)
const [isCaseSensitive, setIsCaseSensitive] = useState(false)
const [isWholeWord, setIsWholeWord] = useState(false)
const [shouldScroll, setShouldScroll] = useState(false)
const highlightTextSet = useState(new Set<Node>())[0]
const prevSearchText = useRef('')
const { t } = useTranslation()
const locateByIndex = (index: number, shouldScroll = true) => {
if (target) {
const highlightTextNodes = [...highlightTextSet] as HTMLElement[]
highlightTextNodes.sort((a, b) => {
const { top: aTop } = a.getBoundingClientRect()
const { top: bTop } = b.getBoundingClientRect()
return aTop - bTop
})
for (const node of highlightTextNodes) {
node.classList.remove(HIGHLIGHT_SELECT_CLASS)
}
setSearchResultIndex(index)
if (highlightTextNodes.length > 0) {
const highlightTextNode = highlightTextNodes[index] ?? null
if (highlightTextNode) {
highlightTextNode.classList.add(HIGHLIGHT_SELECT_CLASS)
if (shouldScroll) {
highlightTextNode.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
}
}
}
}
}
const restoreHighlight = () => {
const highlightTextParentNodeSet = new Set<HTMLElement>()
// Make a copy because the set might be modified during iteration indirectly
const nodesToRestore = [...highlightTextSet]
for (const highlightTextNode of nodesToRestore) {
if (highlightTextNode.textContent) {
const textNode = document.createTextNode(highlightTextNode.textContent)
const node = highlightTextNode as HTMLElement
if (node.parentNode) {
highlightTextParentNodeSet.add(node.parentNode as HTMLElement)
node.replaceWith(textNode) // This removes the node from the DOM
}
}
}
highlightTextSet.clear() // Clear the original set after processing
for (const parentNode of highlightTextParentNodeSet) {
mergeAdjacentTextNodes(parentNode)
}
// highlightTextSet.clear() // Already cleared
}
const search = (searchTargetIndex?: SearchTargetIndex): number | null => {
const searchText = searchInputRef.current?.value.trim() ?? null
if (target && searchText !== null && searchText !== '') {
restoreHighlight()
const iter = document.createNodeIterator(target, NodeFilter.SHOW_TEXT)
let textNode: Node | null
const textNodeSet: Set<Node> = new Set()
while ((textNode = iter.nextNode())) {
if (filter(textNode)) {
textNodeSet.add(textNode)
}
}
const highlightTextSetTemp = new Set<HTMLSpanElement>()
for (const node of textNodeSet) {
const list = highlightText(node, searchText, HIGHLIGHT_CLASS, isCaseSensitive, isWholeWord)
if (list) {
list.forEach((node) => highlightTextSetTemp.add(node))
}
}
const highlightTextList = [...highlightTextSetTemp]
setTotalCount(highlightTextList.length)
highlightTextSetTemp.forEach((node) => highlightTextSet.add(node))
const changeIndex = () => {
let index: number
switch (searchTargetIndex) {
case SearchTargetIndex.Next:
{
index = (searchResultIndex + 1) % highlightTextList.length
}
break
case SearchTargetIndex.Prev:
{
index = (searchResultIndex - 1 + highlightTextList.length) % highlightTextList.length
}
break
default: {
index = searchResultIndex
}
}
return Math.max(index, 0)
}
const targetIndex = (() => {
switch (searchCompleted) {
case SearchCompletedState.NotSearched: {
setSearchCompleted(SearchCompletedState.FirstSearched)
const index = findWindowVerticalCenterElementIndex(highlightTextList)
if (index !== null) {
setSearchResultIndex(index)
return index
} else {
setSearchResultIndex(0)
return 0
}
}
case SearchCompletedState.FirstSearched: {
return changeIndex()
}
default: {
return null
}
}
})()
if (targetIndex === null) {
return null
} else {
const totalCount = highlightTextSet.size
if (targetIndex >= totalCount) {
return totalCount - 1
} else {
return targetIndex
}
}
} else {
return null
}
}
const _searchHandlerDebounce = debounce(() => {
implementation.search()
}, 300)
const searchHandler = useCallback(_searchHandlerDebounce, [_searchHandlerDebounce])
const userInputHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value.trim()
if (value.length === 0) {
restoreHighlight()
setTotalCount(0)
setSearchResultIndex(0)
setSearchCompleted(SearchCompletedState.NotSearched)
} else {
// 用户输入时允许滚动
setShouldScroll(true)
searchHandler()
}
prevSearchText.current = value
}
const keyDownHandler = (event: React.KeyboardEvent<HTMLInputElement>) => {
const { code, key, shiftKey } = event
if (key === 'Process') {
return
}
switch (code) {
case 'Enter':
{
if (shiftKey) {
implementation.searchPrev()
} else {
implementation.searchNext()
}
event.preventDefault()
}
break
case 'Escape':
{
implementation.disable()
}
break
}
}
const searchInputFocus = () => requestAnimationFrame(() => searchInputRef.current?.focus())
const userOutlinedButtonOnClick = () => {
if (onIncludeUserChange) {
onIncludeUserChange(!includeUser)
}
searchInputFocus()
}
const implementation = {
disable() {
setEnableContentSearch(false)
restoreHighlight()
setShouldScroll(false)
},
enable(initialText?: string) {
setEnableContentSearch(true)
setShouldScroll(false) // Default to false, search itself might set it to true
if (searchInputRef.current) {
const inputEl = searchInputRef.current
if (initialText && initialText.trim().length > 0) {
inputEl.value = initialText
// Trigger search after setting initial text
// Need to make sure search() uses the new value
// and also to focus and select
requestAnimationFrame(() => {
inputEl.focus()
inputEl.select()
setShouldScroll(true)
const targetIndex = search()
if (targetIndex !== null) {
locateByIndex(targetIndex, true) // Ensure scrolling
} else {
// If search returns null (e.g., empty input or no matches with initial text), clear state
restoreHighlight()
setTotalCount(0)
setSearchResultIndex(0)
setSearchCompleted(SearchCompletedState.NotSearched)
}
})
} else {
requestAnimationFrame(() => {
inputEl.focus()
inputEl.select()
})
// Only search if there's existing text and no new initialText
if (inputEl.value.trim()) {
const targetIndex = search()
if (targetIndex !== null) {
setSearchResultIndex(targetIndex)
// locateByIndex(targetIndex, false); // Don't scroll if just enabling with existing text
}
}
}
}
},
searchNext() {
if (enableContentSearch) {
const targetIndex = search(SearchTargetIndex.Next)
if (targetIndex !== null) {
locateByIndex(targetIndex)
}
}
},
searchPrev() {
if (enableContentSearch) {
const targetIndex = search(SearchTargetIndex.Prev)
if (targetIndex !== null) {
locateByIndex(targetIndex)
}
}
},
resetSearchState() {
if (enableContentSearch) {
setSearchCompleted(SearchCompletedState.NotSearched)
// Maybe also reset index? Depends on desired behavior
// setSearchResultIndex(0);
}
},
search() {
if (enableContentSearch) {
const targetIndex = search()
if (targetIndex !== null) {
locateByIndex(targetIndex, shouldScroll)
} else {
// If search returns null (e.g., empty input), clear state
restoreHighlight()
setTotalCount(0)
setSearchResultIndex(0)
setSearchCompleted(SearchCompletedState.NotSearched)
}
}
},
silentSearch() {
if (enableContentSearch) {
const targetIndex = search()
if (targetIndex !== null) {
// 只更新索引,不触发滚动
locateByIndex(targetIndex, false)
}
}
},
focus() {
searchInputFocus()
}
}
useImperativeHandle(ref, () => ({
disable() {
implementation.disable()
},
enable(initialText?: string) {
implementation.enable(initialText)
},
searchNext() {
implementation.searchNext()
},
searchPrev() {
implementation.searchPrev()
},
search() {
implementation.search()
},
silentSearch() {
implementation.silentSearch()
},
focus() {
implementation.focus()
}
}))
// Re-run search when options change and search is active
useEffect(() => {
if (enableContentSearch && searchInputRef.current?.value.trim()) {
implementation.search()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCaseSensitive, isWholeWord, enableContentSearch]) // Add enableContentSearch dependency
const prevButtonOnClick = () => {
implementation.searchPrev()
searchInputFocus()
}
const nextButtonOnClick = () => {
implementation.searchNext()
searchInputFocus()
}
const closeButtonOnClick = () => {
implementation.disable()
}
const caseSensitiveButtonOnClick = () => {
setIsCaseSensitive(!isCaseSensitive)
searchInputFocus()
}
const wholeWordButtonOnClick = () => {
setIsWholeWord(!isWholeWord)
searchInputFocus()
}
return (
<Container ref={containerRef} style={enableContentSearch ? {} : { display: 'none' }}>
<NarrowLayout style={{ width: '100%' }}>
<SearchBarContainer>
<InputWrapper>
<Input ref={searchInputRef} onInput={userInputHandler} onKeyDown={keyDownHandler} />
<ToolBar>
<Tooltip title={t('button.includes_user_questions')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={userOutlinedButtonOnClick}>
<User size={18} style={{ color: includeUser ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</Tooltip>
<Tooltip title={t('button.case_sensitive')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={caseSensitiveButtonOnClick}>
<CaseSensitive
size={18}
style={{ color: isCaseSensitive ? 'var(--color-link)' : 'var(--color-icon)' }}
/>
</ToolbarButton>
</Tooltip>
<Tooltip title={t('button.whole_word')} mouseEnterDelay={0.8} placement="bottom">
<ToolbarButton type="text" onClick={wholeWordButtonOnClick}>
<WholeWord size={18} style={{ color: isWholeWord ? 'var(--color-link)' : 'var(--color-icon)' }} />
</ToolbarButton>
</Tooltip>
</ToolBar>
</InputWrapper>
<Separator></Separator>
<SearchResults>
{searchCompleted !== SearchCompletedState.NotSearched ? (
totalCount > 0 ? (
<>
<SearchResultCount>{searchResultIndex + 1}</SearchResultCount>
<SearchResultSeparator>/</SearchResultSeparator>
<SearchResultTotalCount>{totalCount}</SearchResultTotalCount>
</>
) : (
<NoResults>{t('common.no_results')}</NoResults>
)
) : (
<SearchResultsPlaceholder>0/0</SearchResultsPlaceholder>
)}
</SearchResults>
<ToolBar>
<ToolbarButton type="text" onClick={prevButtonOnClick} disabled={totalCount === 0}>
<ChevronUp size={18} />
</ToolbarButton>
<ToolbarButton type="text" onClick={nextButtonOnClick} disabled={totalCount === 0}>
<ChevronDown size={18} />
</ToolbarButton>
<ToolbarButton type="text" onClick={closeButtonOnClick}>
<X size={18} />
</ToolbarButton>
</ToolBar>
</SearchBarContainer>
</NarrowLayout>
<Placeholder />
</Container>
)
}
)
ContentSearch.displayName = 'ContentSearch'
const Container = styled.div`
display: flex;
flex-direction: row;
z-index: 2;
`
const SearchBarContainer = styled.div`
border: 1px solid var(--color-border);
border-radius: 10px;
transition: all 0.2s ease;
position: relative;
margin: 5px 20px;
margin-bottom: 0;
padding: 6px 15px 8px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-opacity);
flex: 1 1 auto; /* Take up input's previous space */
`
const Placeholder = styled.div`
width: 5px;
`
const InputWrapper = styled.div`
display: flex;
align-items: center;
flex: 1 1 auto; /* Take up input's previous space */
`
const Input = styled.input`
border: none;
color: var(--color-text);
background-color: transparent;
outline: none;
width: 100%;
padding: 0 5px; /* Adjust padding, wrapper will handle spacing */
flex: 1; /* Allow input to grow */
font-size: 14px;
font-family: Ubuntu;
`
const ToolBar = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: tpx;
`
const Separator = styled.div`
width: 1px;
height: 1.5em;
background-color: var(--color-border);
margin-left: 2px;
margin-right: 2px;
flex: 0 0 auto;
`
const SearchResults = styled.div`
display: flex;
justify-content: center;
width: 80px;
margin: 0 2px;
flex: 0 0 auto;
color: var(--color-text-secondary);
font-size: 14px;
font-family: Ubuntu;
`
const SearchResultsPlaceholder = styled.span`
color: var(--color-text-secondary);
opacity: 0.5;
`
const NoResults = styled.span`
color: var(--color-text-secondary);
`
const SearchResultCount = styled.span`
color: var(--color-text);
`
const SearchResultSeparator = styled.span`
color: var(--color-text);
margin: 0 4px;
`
const SearchResultTotalCount = styled.span`
color: var(--color-text);
`

View File

@ -149,15 +149,6 @@ export const BaseTypography = styled(Box)<{
text-align: ${(props) => props.textAlign || 'left'};
`
export const TypographyNormal = styled(BaseTypography)`
font-family: 'Ubuntu';
`
export const TypographyBold = styled(BaseTypography)`
font-family: 'Ubuntu';
font-weight: bold;
`
export const Container = styled.main<ContainerProps>`
display: flex;
flex-direction: column;

View File

@ -35,7 +35,6 @@ const ListItemContainer = styled.div`
flex-direction: column;
justify-content: space-between;
position: relative;
font-family: Ubuntu;
cursor: pointer;
border: 1px solid transparent;

View File

@ -57,7 +57,6 @@ const NameSpan = styled.span`
text-overflow: ellipsis;
white-space: nowrap;
cursor: help;
font-family: 'Ubuntu';
line-height: 30px;
`

View File

@ -1,6 +1,8 @@
import { backup } from '@renderer/services/BackupService'
import store from '@renderer/store'
import { IpcChannel } from '@shared/IpcChannel'
import { Modal, Progress } from 'antd'
import Logger from 'electron-log'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -20,6 +22,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const [progressData, setProgressData] = useState<ProgressData>()
const { t } = useTranslation()
const skipBackupFile = store.getState().settings.skipBackupFile
useEffect(() => {
const removeListener = window.electron.ipcRenderer.on(IpcChannel.BackupProgress, (_, data: ProgressData) => {
@ -32,7 +35,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
}, [])
const onOk = async () => {
await backup()
Logger.log('[BackupManager] ', skipBackupFile)
await backup(skipBackupFile)
setOpen(false)
}

View File

@ -647,7 +647,6 @@ const QuickPanelItem = styled.div`
border-radius: 6px;
cursor: pointer;
transition: background-color 0.1s ease;
font-family: Ubuntu;
&.selected {
background-color: var(--selected-color);
&.focused {

View File

@ -1,5 +1,6 @@
import { isLinux, isMac, isWindows } from '@renderer/config/constant'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react'
import styled from 'styled-components'
@ -25,7 +26,12 @@ export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
}
export const NavbarRight: FC<Props> = ({ children, ...props }) => {
return <NavbarRightContainer {...props}>{children}</NavbarRightContainer>
const isFullscreen = useFullscreen()
return (
<NavbarRightContainer {...props} $isFullscreen={isFullscreen}>
{children}
</NavbarRightContainer>
)
}
const NavbarContainer = styled.div`
@ -58,11 +64,11 @@ const NavbarCenterContainer = styled.div`
color: var(--color-text-1);
`
const NavbarRightContainer = styled.div`
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
min-width: var(--topic-list-width);
display: flex;
align-items: center;
padding: 0 12px;
padding-right: ${isWindows ? '140px' : isLinux ? '120px' : '12px'};
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWindows ? '140px' : isLinux ? '120px' : '12px')};
justify-content: flex-end;
`

View File

@ -3,6 +3,7 @@ import { isMac } from '@renderer/config/constant'
import { AppLogo, UserAvatar } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
@ -68,8 +69,13 @@ const Sidebar: FC = () => {
})
}
const isFullscreen = useFullscreen()
return (
<Container id="app-sidebar" style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}>
<Container
$isFullscreen={isFullscreen}
id="app-sidebar"
style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}>
{isEmoji(avatar) ? (
<EmojiAvatar onClick={onEditUser} className="sidebar-avatar" size={31} fontSize={18}>
{avatar}
@ -311,7 +317,7 @@ const PinnedApps: FC = () => {
)
}
const Container = styled.div`
const Container = styled.div<{ $isFullscreen: boolean }>`
display: flex;
flex-direction: column;
align-items: center;
@ -319,9 +325,9 @@ const Container = styled.div`
padding-bottom: 12px;
width: var(--sidebar-width);
min-width: var(--sidebar-width);
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
height: ${({ $isFullscreen }) => (isMac && !$isFullscreen ? 'calc(100vh - var(--navbar-height))' : '100vh')};
-webkit-app-region: drag !important;
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
margin-top: ${({ $isFullscreen }) => (isMac && !$isFullscreen ? 'var(--navbar-height)' : 0)};
.sidebar-avatar {
margin-bottom: ${isMac ? '12px' : '12px'};

View File

@ -3,8 +3,6 @@ export const DEFAULT_CONTEXTCOUNT = 5
export const DEFAULT_MAX_TOKENS = 4096
export const DEFAULT_KNOWLEDGE_DOCUMENT_COUNT = 6
export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0
export const FONT_FAMILY =
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
export const platform = window.electron?.process?.platform
export const isMac = platform === 'darwin'

View File

@ -163,7 +163,7 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
{
id: 'minimax',
name: '海螺',
url: 'https://hailuoai.com/',
url: 'https://chat.minimaxi.com/',
logo: HailuoModelLogo
},
{

View File

@ -37,10 +37,14 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
},
Collapse: {
headerBg: 'transparent'
},
Tooltip: {
fontSize: 13
}
},
token: {
colorPrimary: '#00b96b'
colorPrimary: '#00b96b',
fontFamily: 'var(--font-family)'
}
}}>
{children}

View File

@ -0,0 +1,18 @@
import { IpcChannel } from '@shared/IpcChannel'
import { useEffect, useState } from 'react'
export function useFullscreen() {
const [isFullscreen, setIsFullscreen] = useState(false)
useEffect(() => {
const cleanup = window.electron.ipcRenderer.on(IpcChannel.FullscreenStatusChanged, (_, fullscreen) => {
setIsFullscreen(fullscreen)
})
return () => {
cleanup()
}
}, [])
return isFullscreen
}

View File

@ -131,7 +131,10 @@
"manage": "Manage",
"select_model": "Select Model",
"show.all": "Show All",
"update_available": "Update Available"
"update_available": "Update Available",
"includes_user_questions": "Include Your Questions",
"case_sensitive": "Case Sensitive",
"whole_word": "Whole Word"
},
"chat": {
"add.assistant.title": "Add Assistant",
@ -163,6 +166,7 @@
"input.estimated_tokens.tip": "Estimated tokens",
"input.expand": "Expand",
"input.file_not_supported": "Model does not support this file type",
"input.file_error": "Error processing file",
"input.generate_image": "Generate image",
"input.generate_image_not_supported": "The model does not support generating images.",
"input.knowledge_base": "Knowledge Base",
@ -399,7 +403,8 @@
"pinyin": "Sort by Pinyin",
"pinyin.asc": "Sort by Pinyin (A-Z)",
"pinyin.desc": "Sort by Pinyin (Z-A)"
}
},
"no_results": "No results"
},
"docs": {
"title": "Docs"
@ -962,6 +967,8 @@
"app_knowledge.remove_all_confirm": "Deleting knowledge base files will reduce the storage space occupied, but will not delete the knowledge base vector data, after deletion, the source file will no longer be able to be opened. Continue?",
"app_knowledge.remove_all_success": "Files removed successfully",
"app_logs": "App Logs",
"backup.skip_file_data_title": "Slim Backup",
"backup.skip_file_data_help": "Skip backing up data files such as pictures and knowledge bases during backup, and only back up chat records and settings. Reduce space occupancy and speed up the backup speed.",
"clear_cache": {
"button": "Clear Cache",
"confirm": "Clearing the cache will delete application cache data, including minapp data. This action is irreversible, continue?",
@ -1558,6 +1565,7 @@
"reset_defaults_confirm": "Are you sure you want to reset all shortcuts?",
"reset_to_default": "Reset to Default",
"search_message": "Search Message",
"search_message_in_chat": "Search Message in Current Chat",
"show_app": "Show/Hide App",
"show_settings": "Open Settings",
"title": "Keyboard Shortcuts",

View File

@ -131,7 +131,10 @@
"manage": "管理",
"select_model": "モデルを選択",
"show.all": "すべて表示",
"update_available": "更新可能"
"update_available": "更新可能",
"includes_user_questions": "ユーザーからの質問を含む",
"case_sensitive": "大文字と小文字の区別",
"whole_word": "全語一致"
},
"chat": {
"add.assistant.title": "アシスタントを追加",
@ -163,6 +166,7 @@
"input.estimated_tokens.tip": "推定トークン数",
"input.expand": "展開",
"input.file_not_supported": "モデルはこのファイルタイプをサポートしません",
"input.file_error": "ファイル処理エラー",
"input.generate_image": "画像を生成する",
"input.generate_image_not_supported": "モデルは画像の生成をサポートしていません。",
"input.knowledge_base": "ナレッジベース",
@ -399,7 +403,8 @@
"pinyin": "ピンインでソート",
"pinyin.asc": "ピンインで昇順ソート",
"pinyin.desc": "ピンインで降順ソート"
}
},
"no_results": "検索結果なし"
},
"docs": {
"title": "ドキュメント"
@ -959,6 +964,8 @@
"app_knowledge.remove_all": "ナレッジベースファイルを削除",
"app_knowledge.remove_all_confirm": "ナレッジベースファイルを削除すると、ナレッジベース自体は削除されません。これにより、ストレージ容量を節約できます。続行しますか?",
"app_knowledge.remove_all_success": "ファイル削除成功",
"backup.skip_file_data_title": "精簡バックアップ",
"backup.skip_file_data_help": "バックアップ時に、画像や知識ベースなどのデータファイルをバックアップ対象から除外し、チャット履歴と設定のみをバックアップします。スペースの占有を減らし、バックアップ速度を向上させます。",
"app_logs": "アプリログ",
"clear_cache": {
"button": "キャッシュをクリア",
@ -1554,6 +1561,7 @@
"reset_defaults_confirm": "すべてのショートカットをリセットしてもよろしいですか?",
"reset_to_default": "デフォルトにリセット",
"search_message": "メッセージを検索",
"search_message_in_chat": "現在のチャットでメッセージを検索",
"show_app": "アプリを表示/非表示",
"show_settings": "設定を開く",
"title": "ショートカット",
@ -1648,7 +1656,9 @@
"title": "ページズーム",
"reset": "リセット"
},
"input.show_translate_confirm": "翻訳確認ダイアログを表示"
"input.show_translate_confirm": "翻訳確認ダイアログを表示",
"about.debug.title": "デバッグ",
"about.debug.open": "開く"
},
"translate": {
"any.language": "任意の言語",

View File

@ -131,7 +131,10 @@
"manage": "Редактировать",
"select_model": "Выбрать модель",
"show.all": "Показать все",
"update_available": "Доступно обновление"
"update_available": "Доступно обновление",
"includes_user_questions": "Включает вопросы пользователей",
"case_sensitive": "Чувствительность к регистру",
"whole_word": "Полное слово"
},
"chat": {
"add.assistant.title": "Добавить ассистента",
@ -163,6 +166,7 @@
"input.estimated_tokens.tip": "Затраты токенов",
"input.expand": "Развернуть",
"input.file_not_supported": "Модель не поддерживает этот тип файла",
"input.file_error": "Ошибка обработки файла",
"input.generate_image": "Сгенерировать изображение",
"input.generate_image_not_supported": "Модель не поддерживает генерацию изображений.",
"input.knowledge_base": "База знаний",
@ -399,7 +403,8 @@
"pinyin": "Сортировать по пиньинь",
"pinyin.asc": "Сортировать по пиньинь (А-Я)",
"pinyin.desc": "Сортировать по пиньинь (Я-А)"
}
},
"no_results": "Результатов не найдено"
},
"docs": {
"title": "Документация"
@ -910,6 +915,7 @@
"restore": {
"confirm": "Вы уверены, что хотите восстановить данные?",
"confirm.button": "Выбрать файл резервной копии",
"content": "Операция восстановления перезапишет все текущие данные приложения данными из резервной копии. Это может занять некоторое время.",
"progress": {
"completed": "Восстановление завершено",
@ -960,6 +966,8 @@
"app_knowledge.remove_all_confirm": "Удаление файлов базы знаний не удалит саму базу знаний, что позволит уменьшить занимаемый объем памяти, продолжить?",
"app_knowledge.remove_all_success": "Файлы удалены успешно",
"app_logs": "Логи приложения",
"backup.skip_file_data_title": "Упрощенная резервная копия",
"backup.skip_file_data_help": "Пропустить при резервном копировании такие данные, как изображения, базы знаний и другие файлы данных, и сделать резервную копию только переписки и настроек. Это уменьшает использование места на диске и ускоряет процесс резервного копирования.",
"clear_cache": {
"button": "Очистка кэша",
"confirm": "Очистка кэша удалит данные приложения. Это действие необратимо, продолжить?",
@ -1554,6 +1562,7 @@
"reset_defaults_confirm": "Вы уверены, что хотите сбросить все горячие клавиши?",
"reset_to_default": "Сбросить настройки по умолчанию",
"search_message": "Поиск сообщения",
"search_message_in_chat": "Поиск сообщения в текущем диалоге",
"show_app": "Показать/скрыть приложение",
"show_settings": "Открыть настройки",
"title": "Горячие клавиши",
@ -1648,7 +1657,9 @@
"title": "Масштаб страницы",
"reset": "Сбросить"
},
"input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода"
"input.show_translate_confirm": "Показать диалоговое окно подтверждения перевода",
"about.debug.title": "Отладка",
"about.debug.open": "Открыть"
},
"translate": {
"any.language": "Любой язык",

View File

@ -131,7 +131,10 @@
"manage": "管理",
"select_model": "选择模型",
"show.all": "显示全部",
"update_available": "有可用更新"
"update_available": "有可用更新",
"includes_user_questions": "包含用户提问",
"case_sensitive": "区分大小写",
"whole_word": "全字匹配"
},
"chat": {
"add.assistant.title": "添加助手",
@ -163,6 +166,7 @@
"input.estimated_tokens.tip": "预估 token 数",
"input.expand": "展开",
"input.file_not_supported": "模型不支持此文件类型",
"input.file_error": "文件处理出错",
"input.generate_image": "生成图片",
"input.generate_image_not_supported": "模型不支持生成图片",
"input.knowledge_base": "知识库",
@ -399,7 +403,8 @@
"pinyin": "按拼音排序",
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
}
},
"no_results": "无结果"
},
"docs": {
"title": "帮助文档"
@ -759,13 +764,13 @@
"type": {
"embedding": "嵌入",
"free": "免费",
"function_calling": "工具",
"reasoning": "推理",
"rerank": "重排",
"select": "选择模型类型",
"text": "文本",
"vision": "视觉",
"websearch": "联网"
"vision": "图像",
"function_calling": "函数调用",
"websearch": "[to be translated]:WebSearch"
}
},
"navbar": {
@ -962,6 +967,8 @@
"app_knowledge.remove_all_confirm": "删除知识库文件可以减少存储空间占用,但不会删除知识库向量化数据,删除之后将无法打开源文件,是否删除?",
"app_knowledge.remove_all_success": "文件删除成功",
"app_logs": "应用日志",
"backup.skip_file_data_title": "精简备份",
"backup.skip_file_data_help": "备份时跳过备份图片、知识库等数据文件,仅备份聊天记录和设置。减少空间占用, 加快备份速度。",
"clear_cache": {
"button": "清除缓存",
"confirm": "清除缓存将删除应用缓存的数据,包括小程序数据。此操作不可恢复,是否继续?",
@ -1558,6 +1565,7 @@
"reset_defaults_confirm": "确定要重置所有快捷键吗?",
"reset_to_default": "重置为默认",
"search_message": "搜索消息",
"search_message_in_chat": "在当前对话中搜索消息",
"show_app": "显示/隐藏应用",
"show_settings": "打开设置",
"title": "快捷方式",

View File

@ -131,7 +131,10 @@
"manage": "管理",
"select_model": "選擇模型",
"show.all": "顯示全部",
"update_available": "有可用更新"
"update_available": "有可用更新",
"includes_user_questions": "包含使用者提問",
"case_sensitive": "區分大小寫",
"whole_word": "全字匹配"
},
"chat": {
"add.assistant.title": "新增助手",
@ -163,6 +166,7 @@
"input.estimated_tokens.tip": "預估 Token 數",
"input.expand": "展開",
"input.file_not_supported": "模型不支援此檔案類型",
"input.file_error": "檔案處理錯誤",
"input.generate_image": "生成圖片",
"input.generate_image_not_supported": "模型不支援生成圖片",
"input.knowledge_base": "知識庫",
@ -399,7 +403,8 @@
"pinyin": "按拼音排序",
"pinyin.asc": "按拼音升序",
"pinyin.desc": "按拼音降序"
}
},
"no_results": "沒有結果"
},
"docs": {
"title": "說明文件"
@ -962,6 +967,8 @@
"app_knowledge.remove_all_confirm": "刪除知識庫文件可以減少儲存空間佔用,但不會刪除知識庫向量化資料,刪除之後將無法開啟原始檔,是否刪除?",
"app_knowledge.remove_all_success": "檔案刪除成功",
"app_logs": "應用程式日誌",
"backup.skip_file_data_title": "精簡備份",
"backup.skip_file_data_help": "備份時跳過備份圖片、知識庫等數據文件,僅備份聊天記錄和設置。減少空間佔用, 加快備份速度。",
"clear_cache": {
"button": "清除快取",
"confirm": "清除快取將刪除應用快取資料,包括小工具資料。此操作不可恢復,是否繼續?",
@ -1556,6 +1563,7 @@
"reset_defaults_confirm": "確定要重設所有快捷鍵嗎?",
"reset_to_default": "重設為預設",
"search_message": "搜尋訊息",
"search_message_in_chat": "在當前對話中搜尋訊息",
"show_app": "顯示/隱藏應用程式",
"show_settings": "開啟設定",
"title": "快速方式",
@ -1564,7 +1572,8 @@
"toggle_show_topics": "切換話題顯示",
"zoom_in": "放大介面",
"zoom_out": "縮小介面",
"zoom_reset": "重設縮放"
"zoom_reset": "重設縮放",
"exit_fullscreen": "退出全螢幕"
},
"theme.auto": "自動",
"theme.dark": "深色",

View File

@ -930,6 +930,8 @@
"app_knowledge.remove_all_confirm": "Η διαγραφή των αρχείων της βάσης γνώσεων μπορεί να μειώσει τη χρήση χώρου αποθήκευσης, αλλά δεν θα διαγράψει τα διανυσματωτικά δεδομένα της βάσης γνώσεων. Μετά τη διαγραφή, δεν θα μπορείτε να ανοίξετε τα αρχεία πηγή. Θέλετε να διαγράψετε;",
"app_knowledge.remove_all_success": "Τα αρχεία διαγράφηκαν με επιτυχία",
"app_logs": "Φάκελοι εφαρμογής",
"backup.skip_file_data_title": "Συμπυκνωμένο αντίγραφο ασφαλείας",
"backup.skip_file_data_help": "Κατά τη δημιουργία αντιγράφων ασφαλείας, παραλείψτε τις εικόνες, τις βάσεις γνώσεων και άλλα αρχεία δεδομένων. Δημιουργήστε αντίγραφα μόνο για το ιστορικό συνομιλιών και τις ρυθμίσεις. Αυτό θα μειώσει τη χρήση χώρου και θα επιταχύνει την ταχύτητα δημιουργίας αντιγράφων.",
"clear_cache": {
"button": "Καθαρισμός Μνήμης",
"confirm": "Η διαγραφή της μνήμης θα διαγράψει τα στοιχεία καθαρισμού της εφαρμογής, συμπεριλαμβανομένων των στοιχείων πρόσθετων εφαρμογών. Αυτή η ενέργεια δεν είναι αναστρέψιμη. Θέλετε να συνεχίσετε;",
@ -1655,4 +1657,4 @@
"visualization": "προβολή"
}
}
}
}

View File

@ -107,6 +107,7 @@
"backup": {
"confirm": "¿Está seguro de que desea realizar una copia de seguridad de los datos?",
"confirm.button": "Seleccionar ubicación de copia de seguridad",
"confirm.file_checkbox": "El tamaño del archivo es {{size}}, ¿desea elegir el archivo de copia de seguridad?",
"content": "Realizar una copia de seguridad de todos los datos, incluyendo registros de chat, configuraciones, bases de conocimiento y todos los demás datos. Tenga en cuenta que el proceso de copia de seguridad puede llevar algún tiempo, gracias por su paciencia.",
"progress": {
"completed": "Copia de seguridad completada",
@ -1655,4 +1656,4 @@
"visualization": "Visualización"
}
}
}
}

View File

@ -930,6 +930,8 @@
"app_knowledge.remove_all_confirm": "La suppression des fichiers de la base de connaissances libérera de l'espace de stockage, mais ne supprimera pas les données vectorisées de la base de connaissances. Après la suppression, vous ne pourrez plus ouvrir les fichiers sources. Souhaitez-vous continuer ?",
"app_knowledge.remove_all_success": "Fichiers supprimés avec succès",
"app_logs": "Journaux de l'application",
"backup.skip_file_data_title": "Sauvegarde réduite",
"backup.skip_file_data_help": "Passer outre les fichiers de données tels que les images et les bases de connaissances lors de la sauvegarde, et ne sauvegarder que les conversations et les paramètres. Cela réduit l'occupation d'espace et accélère la vitesse de sauvegarde.",
"clear_cache": {
"button": "Effacer le cache",
"confirm": "L'effacement du cache supprimera les données du cache de l'application, y compris les données des mini-programmes. Cette action ne peut pas être annulée, voulez-vous continuer ?",
@ -1655,4 +1657,4 @@
"visualization": "Visualisation"
}
}
}
}

View File

@ -107,6 +107,7 @@
"backup": {
"confirm": "Tem certeza de que deseja fazer backup dos dados?",
"confirm.button": "Escolher local de backup",
"confirm.file_checkbox": "Pule a cópia de segurança de arquivos de dados como imagens e banco de conhecimento e copie apenas as conversas e as configurações.",
"content": "Fazer backup de todos os dados, incluindo registros de chat, configurações, base de conhecimento e todos os outros dados. Por favor, note que o processo de backup pode levar algum tempo. Agradecemos sua paciência.",
"progress": {
"completed": "Backup concluído",
@ -931,6 +932,8 @@
"app_knowledge.remove_all_confirm": "A exclusão dos arquivos da base de conhecimento reduzirá o uso do espaço de armazenamento, mas não excluirá os dados vetoriais da base de conhecimento. Após a exclusão, os arquivos originais não poderão ser abertos. Deseja excluir?",
"app_knowledge.remove_all_success": "Arquivo excluído com sucesso",
"app_logs": "Logs do aplicativo",
"backup.skip_file_data_title": "Backup simplificado",
"backup.skip_file_data_help": "Pule arquivos de dados como imagens e bancos de conhecimento durante o backup e realize apenas o backup das conversas e configurações. Diminua o consumo de espaço e aumente a velocidade do backup.",
"clear_cache": {
"button": "Limpar cache",
"confirm": "Limpar cache removerá os dados armazenados em cache do aplicativo, incluindo dados de aplicativos minúsculos. Esta ação não pode ser desfeita, deseja continuar?",
@ -1656,4 +1659,4 @@
"visualization": "Visualização"
}
}
}
}

View File

@ -1,7 +1,7 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { Center } from '@renderer/components/Layout'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { Empty, Input } from 'antd'
import { Input } from 'antd'
import { isEmpty } from 'lodash'
import { Search } from 'lucide-react'
import React, { FC, useState } from 'react'
@ -53,7 +53,7 @@ const AppsPage: FC = () => {
<ContentContainer id="content-container">
{isEmpty(filteredApps) ? (
<Center>
<Empty />
<App isLast app={filteredApps[0]} />
</Center>
) : (
<AppsContainer style={{ height: containerHeight }}>

View File

@ -1,7 +1,9 @@
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { RootState } from '@renderer/store'
@ -9,7 +11,9 @@ import { messageBlocksSelectors } from '@renderer/store/messageBlock'
import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage'
import { Assistant, Topic } from '@renderer/types'
import { Flex, Modal } from 'antd'
import { FC, useEffect, useState } from 'react'
import { debounce } from 'lodash'
import React, { FC, useMemo, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import styled from 'styled-components'
@ -27,7 +31,7 @@ interface Props {
const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { topicPosition, messageStyle } = useSettings()
const { topicPosition, messageStyle, showAssistants } = useSettings()
const { showTopics } = useShowTopics()
const { t } = useTranslation()
const [isMultiSelectMode, setIsMultiSelectMode] = useState(false)
@ -40,6 +44,81 @@ const Chat: FC<Props> = (props) => {
// 获取所有消息块
const messageBlocks = useSelector(messageBlocksSelectors.selectEntities)
const mainRef = React.useRef<HTMLDivElement>(null)
const contentSearchRef = React.useRef<ContentSearchRef>(null)
const [filterIncludeUser, setFilterIncludeUser] = useState(false)
const maxWidth = useMemo(() => {
const showRightTopics = showTopics && topicPosition === 'right'
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth} - 5px)`
}, [showAssistants, showTopics, topicPosition])
useHotkeys('esc', () => {
contentSearchRef.current?.disable()
})
useShortcut('search_message_in_chat', () => {
try {
const selectedText = window.getSelection()?.toString().trim()
contentSearchRef.current?.enable(selectedText)
} catch (error) {
console.error('Error enabling content search:', error)
}
})
const contentSearchFilter = (node: Node): boolean => {
if (node.parentNode) {
let parentNode: HTMLElement | null = node.parentNode as HTMLElement
while (parentNode?.parentNode) {
if (parentNode.classList.contains('MessageFooter')) {
return false
}
if (filterIncludeUser) {
if (parentNode?.classList.contains('message-content-container')) {
return true
}
} else {
if (parentNode?.classList.contains('message-content-container-assistant')) {
return true
}
}
parentNode = parentNode.parentNode as HTMLElement
}
return false
} else {
return false
}
}
const userOutlinedItemClickHandler = () => {
setFilterIncludeUser(!filterIncludeUser)
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setTimeout(() => {
contentSearchRef.current?.search()
contentSearchRef.current?.focus()
}, 0)
})
})
}
let firstUpdateCompleted = false
const firstUpdateOrNoFirstUpdateHandler = debounce(() => {
contentSearchRef.current?.silentSearch()
}, 10)
const messagesComponentUpdateHandler = () => {
if (firstUpdateCompleted) {
firstUpdateOrNoFirstUpdateHandler()
}
}
const messagesComponentFirstUpdateHandler = () => {
setTimeout(() => (firstUpdateCompleted = true), 300)
firstUpdateOrNoFirstUpdateHandler()
}
useEffect(() => {
const handleToggleMultiSelect = (value: boolean) => {
setIsMultiSelectMode(value)
@ -142,13 +221,24 @@ const Chat: FC<Props> = (props) => {
return (
<Container id="chat" className={messageStyle}>
<Main id="chat-main" vertical flex={1} justify="space-between">
<Messages
key={props.activeTopic.id}
assistant={assistant}
topic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}>
<ContentSearch
ref={contentSearchRef}
searchTarget={mainRef as React.RefObject<HTMLElement>}
filter={contentSearchFilter}
includeUser={filterIncludeUser}
onIncludeUserChange={userOutlinedItemClickHandler}
/>
<MessagesContainer>
<Messages
key={props.activeTopic.id}
assistant={assistant}
topic={props.activeTopic}
setActiveTopic={props.setActiveTopic}
onComponentUpdate={messagesComponentUpdateHandler}
onFirstUpdate={messagesComponentFirstUpdateHandler}
/>
</MessagesContainer>
<QuickPanelProvider>
{isMultiSelectMode ? (
<MultiSelectActionPopup
@ -186,17 +276,24 @@ const Chat: FC<Props> = (props) => {
)
}
const MessagesContainer = styled.div`
display: flex;
flex-direction: column;
overflow: hidden;
flex: 1;
`
const Container = styled.div`
display: flex;
flex-direction: row;
height: 100%;
flex: 1;
justify-content: space-between;
`
const Main = styled(Flex)`
height: calc(100vh - var(--navbar-height));
transform: translateZ(0);
position: relative;
`
export default Chat

View File

@ -117,6 +117,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const [selectedKnowledgeBases, setSelectedKnowledgeBases] = useState<KnowledgeBase[]>([])
const [mentionModels, setMentionModels] = useState<Model[]>([])
const [isDragging, setIsDragging] = useState(false)
const [isFileDragging, setIsFileDragging] = useState(false)
const [textareaHeight, setTextareaHeight] = useState<number>()
const startDragY = useRef<number>(0)
const startHeight = useRef<number>(0)
@ -584,27 +585,33 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
if (event.clipboardData?.files && event.clipboardData.files.length > 0) {
event.preventDefault()
for (const file of event.clipboardData.files) {
if (file.path === '') {
// 图像生成也支持图像编辑
if (file.type.startsWith('image/') && (isVisionModel(model) || isGenerateImageModel(model))) {
const tempFilePath = await window.api.file.create(file.name)
const arrayBuffer = await file.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
await window.api.file.write(tempFilePath, uint8Array)
const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
break
} else {
window.message.info({
key: 'file_not_supported',
content: t('chat.input.file_not_supported')
})
}
}
try {
// 使用新的API获取文件路径
const filePath = window.api.file.getPathForFile(file)
if (file.path) {
if (supportExts.includes(getFileExtension(file.path))) {
const selectedFile = await window.api.file.get(file.path)
// 如果没有路径,可能是剪贴板中的图像数据
if (!filePath) {
// 图像生成也支持图像编辑
if (file.type.startsWith('image/') && (isVisionModel(model) || isGenerateImageModel(model))) {
const tempFilePath = await window.api.file.create(file.name)
const arrayBuffer = await file.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
await window.api.file.write(tempFilePath, uint8Array)
const selectedFile = await window.api.file.get(tempFilePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
break
} else {
window.message.info({
key: 'file_not_supported',
content: t('chat.input.file_not_supported')
})
}
continue
}
// 有路径的情况
if (supportExts.includes(getFileExtension(filePath))) {
const selectedFile = await window.api.file.get(filePath)
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
} else {
window.message.info({
@ -612,6 +619,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
content: t('chat.input.file_not_supported')
})
}
} catch (error) {
Logger.error('[src/renderer/src/pages/home/Inputbar/Inputbar.tsx] onPaste:', error)
window.message.error(t('chat.input.file_error'))
}
}
return
@ -637,14 +647,44 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
[model, pasteLongTextAsFile, pasteLongTextThreshold, resizeTextArea, supportExts, t, text]
)
// 添加全局粘贴事件处理
useEffect(() => {
const handleGlobalPaste = (event: ClipboardEvent) => {
if (document.activeElement === textareaRef.current?.resizableTextArea?.textArea) {
return
}
onPaste(event)
}
document.addEventListener('paste', handleGlobalPaste)
return () => {
document.removeEventListener('paste', handleGlobalPaste)
}
}, [onPaste])
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
setIsFileDragging(true)
}
const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
setIsFileDragging(true)
}
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
setIsFileDragging(false)
}
const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
setIsFileDragging(false)
const files = await getFilesFromDropEvent(e).catch((err) => {
Logger.error('[src/renderer/src/pages/home/Inputbar/Inputbar.tsx] handleDrop:', err)
@ -652,11 +692,22 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
})
if (files) {
let supportedFiles = 0
files.forEach((file) => {
if (supportExts.includes(getFileExtension(file.path))) {
setFiles((prevFiles) => [...prevFiles, file])
supportedFiles++
}
})
// 如果有文件,但都不支持
if (files.length > 0 && supportedFiles === 0) {
window.message.info({
key: 'file_not_supported',
content: t('chat.input.file_not_supported')
})
}
}
}
@ -872,12 +923,17 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
const showThinkingButton = isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)
return (
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
<Container
onDragOver={handleDragOver}
onDrop={handleDrop}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
className="inputbar">
<NarrowLayout style={{ width: '100%' }}>
<QuickPanelView setInputText={setText} />
<InputBarContainer
id="inputbar"
className={classNames('inputbar-container', inputFocus && 'focus')}
className={classNames('inputbar-container', inputFocus && 'focus', isFileDragging && 'file-dragging')}
ref={containerRef}>
{files.length > 0 && <AttachmentPreview files={files} setFiles={setFiles} />}
{selectedKnowledgeBases.length > 0 && (
@ -1058,6 +1114,23 @@ const InputBarContainer = styled.div`
border-radius: 15px;
padding-top: 6px; // 为拖动手柄留出空间
background-color: var(--color-background-opacity);
&.file-dragging {
border: 2px dashed #2ecc71;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(46, 204, 113, 0.03);
border-radius: 14px;
z-index: 5;
pointer-events: none;
}
}
`
const TextareaStyle: CSSProperties = {
@ -1070,7 +1143,6 @@ const Textarea = styled(TextArea)`
border-radius: 0;
display: flex;
flex: 1;
font-family: Ubuntu;
resize: none !important;
overflow: auto;
width: 100%;
@ -1097,7 +1169,7 @@ const ToolbarMenu = styled.div`
gap: 6px;
`
const ToolbarButton = styled(Button)`
export const ToolbarButton = styled(Button)`
width: 30px;
height: 30px;
font-size: 16px;

View File

@ -74,7 +74,6 @@ const Container = styled.div`
z-index: 10;
padding: 3px 10px;
user-select: none;
font-family: Ubuntu;
border: 0.5px solid var(--color-text-3);
border-radius: 20px;
display: flex;

View File

@ -5,7 +5,7 @@ import { lightbulbVariants } from '@renderer/utils/motionVariants'
import { Collapse, message as antdMessage, Tooltip } from 'antd'
import { Lightbulb } from 'lucide-react'
import { motion } from 'motion/react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -20,8 +20,6 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
const { t } = useTranslation()
const { messageFont, fontSize, thoughtAutoCollapse } = useSettings()
const [activeKey, setActiveKey] = useState<'thought' | ''>(thoughtAutoCollapse ? '' : 'thought')
const [thinkingTime, setThinkingTime] = useState(block.thinking_millsec || 0)
const intervalId = useRef<NodeJS.Timeout>(null)
const isThinking = useMemo(() => block.status === MessageBlockStatus.STREAMING, [block.status])
@ -55,28 +53,6 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
}
}, [block.content, t])
// FIXME: 这里统计的和请求处统计的有一定误差
useEffect(() => {
if (isThinking) {
intervalId.current = setInterval(() => {
setThinkingTime((prev) => prev + 100)
}, 100)
} else if (intervalId.current) {
// 立即清除计时器
clearInterval(intervalId.current)
intervalId.current = null
}
return () => {
if (intervalId.current) {
clearInterval(intervalId.current)
intervalId.current = null
}
}
}, [isThinking])
const thinkingTimeSeconds = useMemo(() => (thinkingTime / 1000).toFixed(1), [thinkingTime])
if (!block.content) {
return null
}
@ -101,9 +77,7 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
<Lightbulb size={18} />
</motion.span>
<ThinkingText>
{t(isThinking ? 'chat.thinking' : 'chat.deeply_thought', {
seconds: thinkingTimeSeconds
})}
<ThinkingTimeSeconds blockThinkingTime={block.thinking_millsec} isThinking={isThinking} />
</ThinkingText>
{/* {isThinking && <BarLoader color="#9254de" />} */}
{!isThinking && (
@ -134,6 +108,41 @@ const ThinkingBlock: React.FC<Props> = ({ block }) => {
)
}
const ThinkingTimeSeconds = memo(
({ blockThinkingTime, isThinking }: { blockThinkingTime?: number; isThinking: boolean }) => {
const { t } = useTranslation()
const [thinkingTime, setThinkingTime] = useState(blockThinkingTime || 0)
// FIXME: 这里统计的和请求处统计的有一定误差
useEffect(() => {
let timer: NodeJS.Timeout | null = null
if (isThinking) {
timer = setInterval(() => {
setThinkingTime((prev) => prev + 100)
}, 100)
} else if (timer) {
// 立即清除计时器
clearInterval(timer)
timer = null
}
return () => {
if (timer) {
clearInterval(timer)
timer = null
}
}
}, [isThinking])
const thinkingTimeSeconds = useMemo(() => (thinkingTime / 1000).toFixed(1), [thinkingTime])
return t(isThinking ? 'chat.thinking' : 'chat.deeply_thought', {
seconds: thinkingTimeSeconds
})
}
)
const CollapseContainer = styled(Collapse)`
margin-bottom: 15px;
`

View File

@ -1,5 +1,4 @@
import ContextMenu from '@renderer/components/ContextMenu'
import { FONT_FAMILY } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useModel } from '@renderer/hooks/useModel'
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
@ -10,7 +9,7 @@ import { Assistant, Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { classNames } from '@renderer/utils'
import { Divider } from 'antd'
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef } from 'react'
import React, { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -54,10 +53,6 @@ const MessageItem: FC<Props> = ({
const isAssistantMessage = message.role === 'assistant'
const showMenubar = !isStreaming && !message.status.includes('ing')
const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
}, [messageFont])
const messageBorder = showMessageDivider ? undefined : 'none'
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
@ -106,13 +101,25 @@ const MessageItem: FC<Props> = ({
<ContextMenu>
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} />
<MessageContentContainer
className="message-content-container"
style={{ fontFamily, fontSize, background: messageBackground, overflowY: 'visible' }}>
className={
message.role === 'user'
? 'message-content-container message-content-container-user'
: message.role === 'assistant'
? 'message-content-container message-content-container-assistant'
: 'message-content-container'
}
style={{
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
fontSize,
background: messageBackground,
overflowY: 'visible'
}}>
<MessageErrorBoundary>
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && (
<MessageFooter
className="MessageFooter"
style={{
border: messageBorder,
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined

View File

@ -143,7 +143,6 @@ const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>`
const MessageTime = styled.div`
font-size: 10px;
color: var(--color-text-3);
font-family: 'Ubuntu';
`
export default MessageHeader

View File

@ -26,7 +26,7 @@ import { updateCodeBlock } from '@renderer/utils/markdown'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { isTextLikeBlock } from '@renderer/utils/messageUtils/is'
import { last } from 'lodash'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import InfiniteScroll from 'react-infinite-scroll-component'
import styled from 'styled-components'
@ -41,9 +41,11 @@ interface MessagesProps {
assistant: Assistant
topic: Topic
setActiveTopic: (topic: Topic) => void
onComponentUpdate?(): void
onFirstUpdate?(): void
}
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
const Messages: FC<MessagesProps> = ({ assistant, topic, setActiveTopic, onComponentUpdate, onFirstUpdate }) => {
const { t } = useTranslation()
const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
const { updateTopic, addTopic } = useAssistant(assistant.id)
@ -267,8 +269,8 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
tokensCount: await estimateHistoryTokens(assistant, messages),
contextCount: getContextCount(assistant, messages)
})
})
}, [assistant, messages])
}).then(() => onFirstUpdate?.())
}, [assistant, messages, onFirstUpdate])
const loadMoreMessages = useCallback(() => {
if (!hasMore || isLoadingMore) return
@ -292,6 +294,10 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
}
})
useEffect(() => {
requestAnimationFrame(() => onComponentUpdate?.())
}, [])
const groupedMessages = useMemo(() => Object.entries(getGroupedMessages(displayMessages)), [displayMessages])
return (
<Container

View File

@ -241,7 +241,6 @@ const Container = styled.div`
padding: 0 10px;
height: 37px;
position: relative;
font-family: Ubuntu;
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);

View File

@ -89,7 +89,6 @@ const AssistantAddItem = styled.div`
padding: 7px 12px;
position: relative;
padding-right: 35px;
font-family: Ubuntu;
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
cursor: pointer;

View File

@ -457,13 +457,11 @@ const Container = styled(Scrollbar)`
const TopicListItem = styled.div`
padding: 7px 12px;
border-radius: var(--list-item-border-radius);
font-family: Ubuntu;
font-size: 13px;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
font-family: Ubuntu;
cursor: pointer;
border: 0.5px solid transparent;
position: relative;

View File

@ -155,6 +155,8 @@ const TabContent = styled.div`
`
const Segmented = styled(AntSegmented)`
font-family: var(--font-family);
&.ant-segmented {
background-color: transparent;
border-radius: 0 !important;

View File

@ -212,7 +212,6 @@ const AddKnowledgeItem = styled.div`
justify-content: space-between;
padding: 7px 12px;
position: relative;
font-family: Ubuntu;
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
cursor: pointer;

View File

@ -662,7 +662,6 @@ const Textarea = styled(TextArea)`
border-radius: 0;
display: flex;
flex: 1;
font-family: Ubuntu;
resize: none !important;
overflow: auto;
width: auto;

View File

@ -564,7 +564,6 @@ const Textarea = styled(TextArea)`
border-radius: 0;
display: flex;
flex: 1;
font-family: Ubuntu;
resize: none !important;
overflow: auto;
width: auto;

View File

@ -14,15 +14,25 @@ import RestorePopup from '@renderer/components/Popups/RestorePopup'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useKnowledgeFiles } from '@renderer/hooks/useKnowledgeFiles'
import { reset } from '@renderer/services/BackupService'
import store, { useAppDispatch } from '@renderer/store'
import { setSkipBackupFile as _setSkipBackupFile } from '@renderer/store/settings'
import { AppInfo } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Button, Typography } from 'antd'
import { Button, Switch, Typography } from 'antd'
import { FileText, FolderCog, FolderInput, Sparkle } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import {
SettingContainer,
SettingDivider,
SettingGroup,
SettingHelpText,
SettingRow,
SettingRowTitle,
SettingTitle
} from '..'
import AgentsSubscribeUrlSettings from './AgentsSubscribeUrlSettings'
import ExportMenuOptions from './ExportMenuSettings'
import JoplinSettings from './JoplinSettings'
@ -42,6 +52,11 @@ const DataSettings: FC = () => {
const { theme } = useTheme()
const [menu, setMenu] = useState<string>('data')
const _skipBackupFile = store.getState().settings.skipBackupFile
const [skipBackupFile, setSkipBackupFile] = useState<boolean>(_skipBackupFile)
const dispatch = useAppDispatch()
//joplin icon needs to be updated into iconfont
const JoplinIcon = () => (
<svg viewBox="0 0 24 24" width="16" height="16" fill="var(--color-icon)" xmlns="http://www.w3.org/2000/svg">
@ -164,6 +179,11 @@ const DataSettings: FC = () => {
})
}
const onSkipBackupFilesChange = (value: boolean) => {
setSkipBackupFile(value)
dispatch(_setSkipBackupFile(value))
}
return (
<Container>
<MenuList>
@ -208,6 +228,14 @@ const DataSettings: FC = () => {
</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={skipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.data.title')}</SettingTitle>

View File

@ -17,25 +17,31 @@ import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
setNutstoreAutoSync,
setNutstorePath,
setNutstoreSkipBackupFile,
setNutstoreSyncInterval,
setNutstoreToken
} from '@renderer/store/nutstore'
import { modalConfirm } from '@renderer/utils'
import { NUTSTORE_HOST } from '@shared/config/nutstore'
import { Button, Input, Select, Tooltip, Typography } from 'antd'
import { Button, Input, Select, Switch, Tooltip, Typography } from 'antd'
import dayjs from 'dayjs'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { type FileStat } from 'webdav'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const NutstoreSettings: FC = () => {
const { theme } = useTheme()
const { t } = useTranslation()
const { nutstoreToken, nutstorePath, nutstoreSyncInterval, nutstoreAutoSync, nutstoreSyncState } = useAppSelector(
(state) => state.nutstore
)
const {
nutstoreToken,
nutstorePath,
nutstoreSyncInterval,
nutstoreAutoSync,
nutstoreSyncState,
nutstoreSkipBackupFile
} = useAppSelector((state) => state.nutstore)
const dispatch = useAppDispatch()
@ -48,6 +54,8 @@ const NutstoreSettings: FC = () => {
const [syncInterval, setSyncInterval] = useState<number>(nutstoreSyncInterval)
const [nutSkipBackupFile, setNutSkipBackupFile] = useState<boolean>(nutstoreSkipBackupFile)
const nutstoreSSOHandler = useNutstoreSSO()
const [backupManagerVisible, setBackupManagerVisible] = useState(false)
@ -128,6 +136,11 @@ const NutstoreSettings: FC = () => {
}
}
const onSkipBackupFilesChange = (value: boolean) => {
setNutSkipBackupFile(value)
dispatch(setNutstoreSkipBackupFile(value))
}
const handleClickPathChange = async () => {
if (!nutstoreToken) {
return
@ -287,6 +300,14 @@ const NutstoreSettings: FC = () => {
</SettingRow>
</>
)}
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={nutSkipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
</>
)}
<>

View File

@ -12,15 +12,16 @@ import {
setWebdavMaxBackups as _setWebdavMaxBackups,
setWebdavPass as _setWebdavPass,
setWebdavPath as _setWebdavPath,
setWebdavSkipBackupFile as _setWebdavSkipBackupFile,
setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser
} from '@renderer/store/settings'
import { Button, Input, Select, Tooltip } from 'antd'
import { Button, Input, Select, Switch, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const WebDavSettings: FC = () => {
const {
@ -29,13 +30,15 @@ const WebDavSettings: FC = () => {
webdavPass: webDAVPass,
webdavPath: webDAVPath,
webdavSyncInterval: webDAVSyncInterval,
webdavMaxBackups: webDAVMaxBackups
webdavMaxBackups: webDAVMaxBackups,
webdavSkipBackupFile: webdDAVSkipBackupFile
} = useSettings()
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
const [webdavUser, setWebdavUser] = useState<string | undefined>(webDAVUser)
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
const [webdavSkipBackupFile, setWebdavSkipBackupFile] = useState<boolean>(webdDAVSkipBackupFile)
const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
@ -67,6 +70,11 @@ const WebDavSettings: FC = () => {
dispatch(_setWebdavMaxBackups(value))
}
const onSkipBackupFilesChange = (value: boolean) => {
setWebdavSkipBackupFile(value)
dispatch(_setWebdavSkipBackupFile(value))
}
const renderSyncStatus = () => {
if (!webdavHost) return null
@ -194,6 +202,14 @@ const WebDavSettings: FC = () => {
<Select.Option value={50}>50</Select.Option>
</Select>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={webdavSkipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
{webdavSync && syncInterval > 0 && (
<>
<SettingDivider />

View File

@ -237,6 +237,7 @@ const DisplaySettings: FC = () => {
minHeight: 200,
fontFamily: 'monospace'
}}
spellCheck={false}
/>
</SettingGroup>
</SettingContainer>

View File

@ -190,7 +190,6 @@ const ServerName = styled.div`
const ServerNameText = styled.span`
font-size: 15px;
font-weight: 500;
font-family: Ubuntu;
`
const StatusIndicator = styled.div`

View File

@ -1,6 +1,7 @@
import { NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import { isLinux, isWindows } from '@renderer/config/constant'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { Button, Dropdown, Menu, type MenuProps } from 'antd'
import { ChevronDown, Search } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@ -73,7 +74,7 @@ export const McpSettingsNavbar = () => {
}))
return (
<NavbarRight style={{ paddingRight: isWindows ? 150 : isLinux ? 120 : 12 }}>
<NavbarRight style={{ paddingRight: useFullscreen() ? '12px' : isWindows ? 150 : isLinux ? 120 : 12 }}>
<HStack alignItems="center" gap={5}>
<Button
size="small"

View File

@ -353,7 +353,6 @@ const ProviderLogo = styled(Avatar)`
const ProviderItemName = styled.div`
margin-left: 10px;
font-weight: 500;
font-family: Ubuntu;
`
const AddButtonWrapper = styled.div`

View File

@ -12,7 +12,6 @@ export const SettingContainer = styled.div<{ theme?: ThemeMode }>`
padding-top: 15px;
padding-bottom: 75px;
overflow-y: scroll;
font-family: Ubuntu;
background: ${(props) => (props.theme === 'dark' ? 'transparent' : 'var(--color-background-soft)')};
&::-webkit-scrollbar {

View File

@ -287,7 +287,8 @@ export default class GeminiProvider extends BaseProvider {
if (reasoningEffort === undefined) {
return {
thinkingConfig: {
includeThoughts: false
includeThoughts: false,
thinkingBudget: 0
} as ThinkingConfig
}
}
@ -921,7 +922,8 @@ export default class GeminiProvider extends BaseProvider {
config = {
...config,
thinkingConfig: {
includeThoughts: false
includeThoughts: false,
thinkingBudget: 0
} as ThinkingConfig
}
}

View File

@ -10,6 +10,7 @@ import {
isSupportedReasoningEffortModel,
isSupportedReasoningEffortOpenAIModel,
isSupportedThinkingTokenClaudeModel,
isSupportedThinkingTokenGeminiModel,
isSupportedThinkingTokenModel,
isSupportedThinkingTokenQwenModel,
isVisionModel,
@ -258,6 +259,19 @@ export default class OpenAIProvider extends BaseOpenAIProvider {
return { thinking: { type: 'disabled' } }
}
if (isSupportedThinkingTokenGeminiModel(model)) {
// openrouter没有提供一个不推理的选项先隐藏
if (this.provider.id === 'openrouter') {
return { reasoning: { maxTokens: 0, exclude: true } }
}
return {
thinkingConfig: {
includeThoughts: false,
thinkingBudget: 0
}
}
}
return {}
}
const effortRatio = EFFORT_RATIO[reasoningEffort]
@ -313,6 +327,16 @@ export default class OpenAIProvider extends BaseOpenAIProvider {
}
}
// Gemini models
if (isSupportedThinkingTokenGeminiModel(model)) {
return {
thinkingConfig: {
thinkingBudget: budgetTokens,
includeThoughts: true
}
}
}
// Default case: no special thinking settings
return {}
}
@ -718,9 +742,17 @@ export default class OpenAIProvider extends BaseOpenAIProvider {
const usage = chunk.usage
const originalFinishDelta = chunk.delta
const originalFinishRawChunk = chunk.chunk
if (!isEmpty(finishReason)) {
onChunk({ type: ChunkType.TEXT_COMPLETE, text: content })
if (content) {
onChunk({ type: ChunkType.TEXT_COMPLETE, text: content })
}
if (thinkingContent) {
onChunk({
type: ChunkType.THINKING_COMPLETE,
text: thinkingContent,
thinking_millsec: new Date().getTime() - time_first_token_millsec
})
}
if (usage) {
finalUsage.completion_tokens += usage.completion_tokens || 0
finalUsage.prompt_tokens += usage.prompt_tokens || 0
@ -812,7 +844,6 @@ export default class OpenAIProvider extends BaseOpenAIProvider {
if (toolResults.length) {
await processToolResults(toolResults, idx)
}
onChunk({
type: ChunkType.BLOCK_COMPLETE,
response: {

View File

@ -593,7 +593,7 @@ export abstract class BaseOpenAIProvider extends BaseProvider {
onChunk({
type: ChunkType.LLM_WEB_SEARCH_COMPLETE,
llm_web_search: {
source: WebSearchSource.OPENAI,
source: WebSearchSource.OPENAI_RESPONSE,
results: chunk.part.annotations
}
})

View File

@ -6,12 +6,12 @@ import store from '@renderer/store'
import { setWebDAVSyncState } from '@renderer/store/backup'
import dayjs from 'dayjs'
export async function backup() {
export async function backup(skipBackupFile: boolean) {
const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}.zip`
const fileContnet = await getBackupData()
const selectFolder = await window.api.file.selectFolder()
if (selectFolder) {
await window.api.backup.backup(filename, fileContnet, selectFolder)
await window.api.backup.backup(filename, fileContnet, selectFolder, skipBackupFile)
window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' })
}
}
@ -83,7 +83,8 @@ export async function backupToWebdav({
store.dispatch(setWebDAVSyncState({ syncing: true, lastSyncError: null }))
const { webdavHost, webdavUser, webdavPass, webdavPath, webdavMaxBackups } = store.getState().settings
const { webdavHost, webdavUser, webdavPass, webdavPath, webdavMaxBackups, webdavSkipBackupFile } =
store.getState().settings
let deviceType = 'unknown'
let hostname = 'unknown'
try {
@ -104,7 +105,8 @@ export async function backupToWebdav({
webdavUser,
webdavPass,
webdavPath,
fileName: finalFileName
fileName: finalFileName,
skipBackupFile: webdavSkipBackupFile
})
if (success) {
store.dispatch(

View File

@ -98,9 +98,13 @@ export async function backupToNutstore({
store.dispatch(setNutstoreSyncState({ syncing: true, lastSyncError: null }))
const backupData = await getBackupData()
const skipBackupFile = store.getState().nutstore.nutstoreSkipBackupFile
try {
const isSuccess = await window.api.backup.backupToWebdav(backupData, { ...config, fileName: finalFileName })
const isSuccess = await window.api.backup.backupToWebdav(backupData, {
...config,
fileName: finalFileName,
skipBackupFile: skipBackupFile
})
if (isSuccess) {
store.dispatch(

View File

@ -46,7 +46,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 102,
version: 103,
blacklist: ['runtime', 'messages', 'messageBlocks'],
migrate
},

View File

@ -1377,6 +1377,34 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'103': (state: RootState) => {
try {
if (state.shortcuts) {
if (!state.shortcuts.shortcuts.find((shortcut) => shortcut.key === 'search_message_in_chat')) {
state.shortcuts.shortcuts.push({
key: 'search_message_in_chat',
shortcut: [isMac ? 'Command' : 'Ctrl', 'F'],
editable: true,
enabled: true,
system: false
})
}
const searchMessageShortcut = state.shortcuts.shortcuts.find((shortcut) => shortcut.key === 'search_message')
const targetShortcut = [isMac ? 'Command' : 'Ctrl', 'F']
if (
searchMessageShortcut &&
Array.isArray(searchMessageShortcut.shortcut) &&
searchMessageShortcut.shortcut.length === targetShortcut.length &&
searchMessageShortcut.shortcut.every((v, i) => v === targetShortcut[i])
) {
searchMessageShortcut.shortcut = [isMac ? 'Command' : 'Ctrl', 'Shift', 'F']
}
}
return state
} catch (error) {
return state
}
}
}

View File

@ -10,6 +10,7 @@ export interface NutstoreState {
nutstoreAutoSync: boolean
nutstoreSyncInterval: number
nutstoreSyncState: NutstoreSyncState
nutstoreSkipBackupFile: boolean
}
const initialState: NutstoreState = {
@ -21,7 +22,8 @@ const initialState: NutstoreState = {
lastSyncTime: null,
syncing: false,
lastSyncError: null
}
},
nutstoreSkipBackupFile: false
}
const nutstoreSlice = createSlice({
@ -42,11 +44,20 @@ const nutstoreSlice = createSlice({
},
setNutstoreSyncState: (state, action: PayloadAction<Partial<WebDAVSyncState>>) => {
state.nutstoreSyncState = { ...state.nutstoreSyncState, ...action.payload }
},
setNutstoreSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.nutstoreSkipBackupFile = action.payload
}
}
})
export const { setNutstoreToken, setNutstorePath, setNutstoreAutoSync, setNutstoreSyncInterval, setNutstoreSyncState } =
nutstoreSlice.actions
export const {
setNutstoreToken,
setNutstorePath,
setNutstoreAutoSync,
setNutstoreSyncInterval,
setNutstoreSyncState,
setNutstoreSkipBackupFile
} = nutstoreSlice.actions
export default nutstoreSlice.reducer

View File

@ -77,6 +77,8 @@ export interface SettingsState {
gridColumns: number
gridPopoverTrigger: 'hover' | 'click'
messageNavigation: 'none' | 'buttons' | 'anchor'
// 数据目录设置
skipBackupFile: boolean
// webdav 配置 host, user, pass, path
webdavHost: string
webdavUser: string
@ -85,6 +87,7 @@ export interface SettingsState {
webdavAutoSync: boolean
webdavSyncInterval: number
webdavMaxBackups: number
webdavSkipBackupFile: boolean
translateModelPrompt: string
autoTranslateWithSpace: boolean
showTranslateConfirm: boolean
@ -202,6 +205,7 @@ export const initialState: SettingsState = {
gridColumns: 2,
gridPopoverTrigger: 'click',
messageNavigation: 'none',
skipBackupFile: false,
webdavHost: '',
webdavUser: '',
webdavPass: '',
@ -209,6 +213,7 @@ export const initialState: SettingsState = {
webdavAutoSync: false,
webdavSyncInterval: 0,
webdavMaxBackups: 0,
webdavSkipBackupFile: false,
translateModelPrompt: TRANSLATE_PROMPT,
autoTranslateWithSpace: false,
showTranslateConfirm: true,
@ -356,6 +361,9 @@ const settingsSlice = createSlice({
setClickAssistantToShowTopic: (state, action: PayloadAction<boolean>) => {
state.clickAssistantToShowTopic = action.payload
},
setSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.skipBackupFile = action.payload
},
setWebdavHost: (state, action: PayloadAction<string>) => {
state.webdavHost = action.payload
},
@ -377,6 +385,9 @@ const settingsSlice = createSlice({
setWebdavMaxBackups: (state, action: PayloadAction<number>) => {
state.webdavMaxBackups = action.payload
},
setWebdavSkipBackupFile: (state, action: PayloadAction<boolean>) => {
state.webdavSkipBackupFile = action.payload
},
setCodeExecution: (state, action: PayloadAction<{ enabled?: boolean; timeoutMinutes?: number }>) => {
if (action.payload.enabled !== undefined) {
state.codeExecution.enabled = action.payload.enabled
@ -611,6 +622,7 @@ export const {
setAutoCheckUpdate,
setRenderInputMessageAsMarkdown,
setClickAssistantToShowTopic,
setSkipBackupFile,
setWebdavHost,
setWebdavUser,
setWebdavPass,
@ -618,6 +630,7 @@ export const {
setWebdavAutoSync,
setWebdavSyncInterval,
setWebdavMaxBackups,
setWebdavSkipBackupFile,
setCodeExecution,
setCodeEditor,
setCodePreview,

View File

@ -60,12 +60,19 @@ const initialState: ShortcutsState = {
system: false
},
{
key: 'search_message',
key: 'search_message_in_chat',
shortcut: [isMac ? 'Command' : 'Ctrl', 'F'],
editable: true,
enabled: true,
system: false
},
{
key: 'search_message',
shortcut: [isMac ? 'Command' : 'Ctrl', 'Shift', 'F'],
editable: true,
enabled: true,
system: false
},
{
key: 'clear_topic',
shortcut: [isMac ? 'Command' : 'Ctrl', 'L'],

View File

@ -622,6 +622,14 @@ const fetchAndProcessAssistantResponseImpl = async (
const contextForUsage = userMsgIndex !== -1 ? orderedMsgs.slice(0, userMsgIndex + 1) : []
const finalContextWithAssistant = [...contextForUsage, finalAssistantMsg]
if (lastBlockId) {
const changes: Partial<MessageBlock> = {
status: MessageBlockStatus.SUCCESS
}
dispatch(updateOneBlock({ id: lastBlockId, changes }))
saveUpdatedBlockToDB(lastBlockId, assistantMsgId, topicId, getState)
}
// 更新topic的name
autoRenameTopic(assistant, topicId)
@ -734,7 +742,6 @@ export const loadTopicMessagesThunk =
try {
const topic = await db.topics.get(topicId)
if (!topic) {
await db.topics.add({ id: topicId, messages: [] })
}

View File

@ -313,6 +313,7 @@ export type WebDavConfig = {
webdavPass: string
webdavPath: string
fileName?: string
skipBackupFile?: boolean
}
export type AppInfo = {

View File

@ -3,14 +3,27 @@ import { FileType } from '@renderer/types'
export const getFilesFromDropEvent = async (e: React.DragEvent<HTMLDivElement>): Promise<FileType[]> => {
if (e.dataTransfer.files.length > 0) {
const results = await Promise.allSettled([...e.dataTransfer.files].map((file) => window.api.file.get(file.path)))
// 使用新的API获取文件路径
const filePromises = [...e.dataTransfer.files].map(async (file) => {
try {
// 使用新的webUtils.getPathForFile API获取文件路径
const filePath = window.api.file.getPathForFile(file)
if (filePath) {
return window.api.file.get(filePath)
}
return null
} catch (error) {
Logger.error('[src/renderer/src/utils/input.ts] getFilesFromDropEvent - getPathForFile error:', error)
return null
}
})
const results = await Promise.allSettled(filePromises)
const list: FileType[] = []
for (const result of results) {
if (result.status === 'fulfilled') {
if (result.value !== null) {
list.push(result.value)
}
} else {
if (result.status === 'fulfilled' && result.value !== null) {
list.push(result.value)
} else if (result.status === 'rejected') {
Logger.error('[src/renderer/src/utils/input.ts] getFilesFromDropEvent:', result.reason)
}
}

View File

@ -412,6 +412,7 @@ export function upsertMCPToolResponse(
const cur = {
...results[index],
response: resp.response,
arguments: resp.arguments,
status: resp.status
}
results[index] = cur

View File

@ -1,11 +1,10 @@
import { FONT_FAMILY } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings'
// import MessageContent from './MessageContent'
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
import MessageErrorBoundary from '@renderer/pages/home/Messages/MessageErrorBoundary'
// import { LegacyMessage } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { FC, memo, useMemo, useRef } from 'react'
import { FC, memo, useRef } from 'react'
import styled from 'styled-components'
interface Props {
@ -28,10 +27,6 @@ const MessageItem: FC<Props> = ({ message, index, total, route }) => {
const isAssistantMessage = message.role === 'assistant'
const fontFamily = useMemo(() => {
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
}, [messageFont])
const messageBackground = getMessageBackground(true, isAssistantMessage)
const maxWidth = '800px'
@ -48,7 +43,7 @@ const MessageItem: FC<Props> = ({ message, index, total, route }) => {
<MessageContentContainer
className="message-content-container"
style={{
fontFamily,
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
fontSize,
background: messageBackground,
...(isAssistantMessage ? { paddingLeft: 5, paddingRight: 5 } : {})

View File

@ -90,6 +90,9 @@ const HomeWindow: FC = () => {
// 例子,中文输入法候选词过程使用`Enter`直接上屏字母,日文输入法候选词过程使用`Enter`输入假名
// 输入法可以`Esc`终止候选词过程
// 这两个例子的`Enter`和`Esc`快捷助手都不应该响应
if (e.nativeEvent.isComposing) {
return
}
if (e.key === 'Process') {
return
}

442
yarn.lock
View File

@ -67,7 +67,7 @@ __metadata:
languageName: node
linkType: hard
"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0":
"@ampproject/remapping@npm:^2.2.0":
version: 2.3.0
resolution: "@ampproject/remapping@npm:2.3.0"
dependencies:
@ -275,15 +275,6 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-annotate-as-pure@npm:^7.22.5":
version: 7.25.9
resolution: "@babel/helper-annotate-as-pure@npm:7.25.9"
dependencies:
"@babel/types": "npm:^7.25.9"
checksum: 10c0/095b6ba50489d797733abebc4596a81918316a99e3632755c9f02508882912b00c2ae5e468532a25a5c2108d109ddbe9b7da78333ee7cc13817fc50c00cf06fe
languageName: node
linkType: hard
"@babel/helper-compilation-targets@npm:^7.26.5":
version: 7.27.0
resolution: "@babel/helper-compilation-targets@npm:7.27.0"
@ -297,7 +288,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.25.9":
"@babel/helper-module-imports@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-module-imports@npm:7.25.9"
dependencies:
@ -358,7 +349,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.25.4, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0":
"@babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/parser@npm:7.27.0"
dependencies:
@ -433,7 +424,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/types@npm:^7.25.4, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0":
"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/types@npm:7.27.0"
dependencies:
@ -2099,13 +2090,6 @@ __metadata:
languageName: node
linkType: hard
"@istanbuljs/schema@npm:^0.1.2":
version: 0.1.3
resolution: "@istanbuljs/schema@npm:0.1.3"
checksum: 10c0/61c5286771676c9ca3eb2bd8a7310a9c063fb6e0e9712225c8471c582d157392c88f5353581c8c9adbe0dff98892317d2fdfc56c3499aa42e0194405206a963a
languageName: node
linkType: hard
"@jimp/bmp@npm:^0.16.13":
version: 0.16.13
resolution: "@jimp/bmp@npm:0.16.13"
@ -2562,7 +2546,7 @@ __metadata:
languageName: node
linkType: hard
"@jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25":
version: 0.3.25
resolution: "@jridgewell/trace-mapping@npm:0.3.25"
dependencies:
@ -3204,13 +3188,13 @@ __metadata:
linkType: hard
"@lezer/sass@npm:^1.0.0":
version: 1.0.7
resolution: "@lezer/sass@npm:1.0.7"
version: 1.1.0
resolution: "@lezer/sass@npm:1.1.0"
dependencies:
"@lezer/common": "npm:^1.2.0"
"@lezer/highlight": "npm:^1.0.0"
"@lezer/lr": "npm:^1.0.0"
checksum: 10c0/6308873c74e10e2f075945f85068333e1cd95f65743a9bc054456e4f55b0a68563ccdfefa9f372d9c234640eca41455877d6a8d4db9c0a09eea5beb71f065009
checksum: 10c0/1b40310ad861b183fb525b1bcdfa27e2bd06e8ad913df4c044be4bfcda5bc5ddbb1d1b3675016bd7792ceaa934548ceb3dc9ad3d3d57b72a31babfe389ab9bab
languageName: node
linkType: hard
@ -4011,13 +3995,6 @@ __metadata:
languageName: node
linkType: hard
"@sec-ant/readable-stream@npm:^0.4.1":
version: 0.4.1
resolution: "@sec-ant/readable-stream@npm:0.4.1"
checksum: 10c0/64e9e9cf161e848067a5bf60cdc04d18495dc28bb63a8d9f8993e4dd99b91ad34e4b563c85de17d91ffb177ec17a0664991d2e115f6543e73236a906068987af
languageName: node
linkType: hard
"@selderee/plugin-htmlparser2@npm:^0.11.0":
version: 0.11.0
resolution: "@selderee/plugin-htmlparser2@npm:0.11.0"
@ -4111,27 +4088,20 @@ __metadata:
languageName: node
linkType: hard
"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.2.0":
"@sindresorhus/is@npm:^4.0.0":
version: 4.6.0
resolution: "@sindresorhus/is@npm:4.6.0"
checksum: 10c0/33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e
languageName: node
linkType: hard
"@sindresorhus/is@npm:^5.2.0, @sindresorhus/is@npm:^5.3.0":
"@sindresorhus/is@npm:^5.2.0":
version: 5.6.0
resolution: "@sindresorhus/is@npm:5.6.0"
checksum: 10c0/66727344d0c92edde5760b5fd1f8092b717f2298a162a5f7f29e4953e001479927402d9d387e245fb9dc7d3b37c72e335e93ed5875edfc5203c53be8ecba1b52
languageName: node
linkType: hard
"@sindresorhus/is@npm:^7.0.1":
version: 7.0.1
resolution: "@sindresorhus/is@npm:7.0.1"
checksum: 10c0/6d43a916d70d9b64066394c272883869b22faf21f4748aaf399c1b691ea704ea607d1668ff2eb5704e5be8809c4a7faafe16be048ce5e1a2ba6e8928b8e3461c
languageName: node
linkType: hard
"@strongtz/win32-arm64-msvc@npm:^0.4.7":
version: 0.4.7
resolution: "@strongtz/win32-arm64-msvc@npm:0.4.7"
@ -4263,12 +4233,12 @@ __metadata:
languageName: node
linkType: hard
"@swc/plugin-styled-components@npm:^7.1.3":
version: 7.1.3
resolution: "@swc/plugin-styled-components@npm:7.1.3"
"@swc/plugin-styled-components@npm:^7.1.5":
version: 7.1.5
resolution: "@swc/plugin-styled-components@npm:7.1.5"
dependencies:
"@swc/counter": "npm:^0.1.3"
checksum: 10c0/5567c95038e26f69d1a5ab0fb430badffbda1b8fb4eea27430442486589410dfd6805ceda28b91c88502fbba4319f82d0534c6f8f9ff581e4ace5fb031ec4917
checksum: 10c0/abffb0030aeb65bd0ba5be62debd35588e621d50414bd882773d32d8b63b839083cc0c089b1311c84e2068df82d697206d254a1e72ebd1be7a86523dabab98a9
languageName: node
linkType: hard
@ -4317,26 +4287,6 @@ __metadata:
languageName: node
linkType: hard
"@tavily/core@npm:0.3.1":
version: 0.3.1
resolution: "@tavily/core@npm:0.3.1"
dependencies:
axios: "npm:^1.7.7"
js-tiktoken: "npm:^1.0.14"
checksum: 10c0/ddf711848f09c9dfe7f094ffdf4ea1291f7af980a8335a52e5c534a62ed9fbd234b3405e3b7baa598926cdd241425e0a4890badc85f883281c03840145de6d98
languageName: node
linkType: hard
"@tavily/core@patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch":
version: 0.3.1
resolution: "@tavily/core@patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch::version=0.3.1&hash=d1f3ab"
dependencies:
axios: "npm:^1.7.7"
js-tiktoken: "npm:^1.0.14"
checksum: 10c0/e25a76960491a8a463ae8f38a058b8b48d2d72314aa12d922aea4bf8d2cb1231c5357d1b0f7ed9650c48a756fc68617f0de3e1ff929aaca3fc5319dc250a545c
languageName: node
linkType: hard
"@tokenizer/token@npm:^0.3.0":
version: 0.3.0
resolution: "@tokenizer/token@npm:0.3.0"
@ -4371,15 +4321,6 @@ __metadata:
languageName: node
linkType: hard
"@types/adm-zip@npm:^0":
version: 0.5.7
resolution: "@types/adm-zip@npm:0.5.7"
dependencies:
"@types/node": "npm:*"
checksum: 10c0/6ba62bd8f4a6e7ffdad08d951c65c4f69161c2b96cc34249dcf3c448dca85749409407b297472d5c66b711325de89012e8607b74c87d99ec23e9b7a44b6c71c6
languageName: node
linkType: hard
"@types/cacheable-request@npm:^6.0.1":
version: 6.0.3
resolution: "@types/cacheable-request@npm:6.0.3"
@ -4741,7 +4682,7 @@ __metadata:
languageName: node
linkType: hard
"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2, @types/http-cache-semantics@npm:^4.0.4":
"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2":
version: 4.0.4
resolution: "@types/http-cache-semantics@npm:4.0.4"
checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6
@ -5026,7 +4967,7 @@ __metadata:
languageName: node
linkType: hard
"@types/ws@npm:^8, @types/ws@npm:^8.5.4":
"@types/ws@npm:^8.5.4":
version: 8.18.1
resolution: "@types/ws@npm:8.18.1"
dependencies:
@ -5631,32 +5572,6 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:^3.1.1":
version: 3.1.1
resolution: "@vitest/coverage-v8@npm:3.1.1"
dependencies:
"@ampproject/remapping": "npm:^2.3.0"
"@bcoe/v8-coverage": "npm:^1.0.2"
debug: "npm:^4.4.0"
istanbul-lib-coverage: "npm:^3.2.2"
istanbul-lib-report: "npm:^3.0.1"
istanbul-lib-source-maps: "npm:^5.0.6"
istanbul-reports: "npm:^3.1.7"
magic-string: "npm:^0.30.17"
magicast: "npm:^0.3.5"
std-env: "npm:^3.8.1"
test-exclude: "npm:^7.0.1"
tinyrainbow: "npm:^2.0.0"
peerDependencies:
"@vitest/browser": 3.1.1
vitest: 3.1.1
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10c0/0f852d8a438d27605955f2a1177e017f48b0dcdc7069318b2b1e031e3561d02a54e4d9a108afacbc8365c8b42f4bcb13282ae7cfaf380bce27741991321e83d9
languageName: node
linkType: hard
"@vitest/expect@npm:3.1.1":
version: 3.1.1
resolution: "@vitest/expect@npm:3.1.1"
@ -5856,7 +5771,6 @@ __metadata:
"@eslint/js": "npm:^9.22.0"
"@google/genai": "npm:^0.13.0"
"@hello-pangea/dnd": "npm:^16.6.0"
"@iconify-json/svg-spinners": "npm:^1.2.2"
"@kangfenmao/keyv-storage": "npm:^0.1.0"
"@langchain/community": "npm:^0.3.36"
"@modelcontextprotocol/sdk": "npm:^1.11.3"
@ -5865,11 +5779,9 @@ __metadata:
"@reduxjs/toolkit": "npm:^2.2.5"
"@shikijs/markdown-it": "npm:^3.2.2"
"@strongtz/win32-arm64-msvc": "npm:^0.4.7"
"@swc/plugin-styled-components": "npm:^7.1.3"
"@swc/plugin-styled-components": "npm:^7.1.5"
"@tanstack/react-query": "npm:^5.27.0"
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch"
"@tryfabric/martian": "npm:^1.2.4"
"@types/adm-zip": "npm:^0"
"@types/diff": "npm:^7"
"@types/fs-extra": "npm:^11"
"@types/lodash": "npm:^4.17.5"
@ -5887,16 +5799,13 @@ __metadata:
"@uiw/codemirror-themes-all": "npm:^4.23.12"
"@uiw/react-codemirror": "npm:^4.23.12"
"@vitejs/plugin-react-swc": "npm:^3.9.0"
"@vitest/coverage-v8": "npm:^3.1.1"
"@vitest/ui": "npm:^3.1.1"
"@vitest/web-worker": "npm:^3.1.3"
"@xyflow/react": "npm:^12.4.4"
antd: "npm:^5.22.5"
applescript: "npm:^1.0.0"
archiver: "npm:^7.0.1"
async-mutex: "npm:^0.5.0"
axios: "npm:^1.7.3"
babel-plugin-styled-components: "npm:^2.1.4"
browser-image-compression: "npm:^2.0.2"
color: "npm:^5.0.0"
dayjs: "npm:^1.11.11"
@ -5967,16 +5876,14 @@ __metadata:
remark-gfm: "npm:^4.0.0"
remark-math: "npm:^6.0.0"
rollup-plugin-visualizer: "npm:^5.12.0"
sass: "npm:^1.77.2"
sass: "npm:^1.88.0"
shiki: "npm:^3.2.2"
string-width: "npm:^7.2.0"
styled-components: "npm:^6.1.11"
tar: "npm:^7.4.3"
tiny-pinyin: "npm:^1.3.2"
tinycolor2: "npm:^1.6.0"
tokenx: "npm:^0.4.1"
turndown: "npm:^7.2.0"
turndown-plugin-gfm: "npm:^1.0.2"
typescript: "npm:^5.6.2"
uuid: "npm:^10.0.0"
vite: "npm:6.2.6"
@ -6038,13 +5945,6 @@ __metadata:
languageName: node
linkType: hard
"adm-zip@npm:^0.5.9":
version: 0.5.16
resolution: "adm-zip@npm:0.5.16"
checksum: 10c0/6f10119d4570c7ba76dcf428abb8d3f69e63f92e51f700a542b43d4c0130373dd2ddfc8f85059f12d4a843703a90c3970cfd17876844b4f3f48bf042bfa6b49f
languageName: node
linkType: hard
"agent-base@npm:6, agent-base@npm:^6.0.2":
version: 6.0.2
resolution: "agent-base@npm:6.0.2"
@ -6554,7 +6454,7 @@ __metadata:
languageName: node
linkType: hard
"axios@npm:^1.7.3, axios@npm:^1.7.7":
"axios@npm:^1.7.3":
version: 1.8.4
resolution: "axios@npm:1.8.4"
dependencies:
@ -6572,21 +6472,6 @@ __metadata:
languageName: node
linkType: hard
"babel-plugin-styled-components@npm:^2.1.4":
version: 2.1.4
resolution: "babel-plugin-styled-components@npm:2.1.4"
dependencies:
"@babel/helper-annotate-as-pure": "npm:^7.22.5"
"@babel/helper-module-imports": "npm:^7.22.5"
"@babel/plugin-syntax-jsx": "npm:^7.22.5"
lodash: "npm:^4.17.21"
picomatch: "npm:^2.3.1"
peerDependencies:
styled-components: ">= 2"
checksum: 10c0/553f35f5feb4b51fda9c9aeef8a31c1b66f430687ab17830b7cdacfe7e93f912aef55bf59e402f4e0a1fa7ad039768ab3626512bbb9bf1f76fcc67ba47e7a56e
languageName: node
linkType: hard
"bail@npm:^1.0.0":
version: 1.0.5
resolution: "bail@npm:1.0.5"
@ -6762,7 +6647,7 @@ __metadata:
languageName: node
linkType: hard
"browserslist@npm:^4.21.1, browserslist@npm:^4.24.0":
"browserslist@npm:^4.24.0":
version: 4.24.4
resolution: "browserslist@npm:4.24.4"
dependencies:
@ -6966,21 +6851,6 @@ __metadata:
languageName: node
linkType: hard
"cacheable-request@npm:^12.0.1":
version: 12.0.1
resolution: "cacheable-request@npm:12.0.1"
dependencies:
"@types/http-cache-semantics": "npm:^4.0.4"
get-stream: "npm:^9.0.1"
http-cache-semantics: "npm:^4.1.1"
keyv: "npm:^4.5.4"
mimic-response: "npm:^4.0.0"
normalize-url: "npm:^8.0.1"
responselike: "npm:^3.0.0"
checksum: 10c0/3ccc26519c8dd0821fcb21fa00781e55f05ab6e1da1487fbbee9c8c03435a3cf72c29a710a991cebe398fb9a5274e2a772fc488546d402db8dc21310764ed83a
languageName: node
linkType: hard
"cacheable-request@npm:^7.0.2":
version: 7.0.4
resolution: "cacheable-request@npm:7.0.4"
@ -7016,20 +6886,13 @@ __metadata:
languageName: node
linkType: hard
"callsites@npm:^3.0.0, callsites@npm:^3.1.0":
"callsites@npm:^3.0.0":
version: 3.1.0
resolution: "callsites@npm:3.1.0"
checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301
languageName: node
linkType: hard
"callsites@npm:^4.0.0":
version: 4.2.0
resolution: "callsites@npm:4.2.0"
checksum: 10c0/8f7e269ec09fc0946bb22d838a8bc7932e1909ab4a833b964749f4d0e8bdeaa1f253287c4f911f61781f09620b6925ccd19a5ea4897489c4e59442c660c312a3
languageName: node
linkType: hard
"camelcase@npm:5.0.0":
version: 5.0.0
resolution: "camelcase@npm:5.0.0"
@ -8786,15 +8649,6 @@ __metadata:
languageName: node
linkType: hard
"dot-prop@npm:^7.2.0":
version: 7.2.0
resolution: "dot-prop@npm:7.2.0"
dependencies:
type-fest: "npm:^2.11.2"
checksum: 10c0/2621702a01e7a47730e3a8e2938a406afc79b62fbb77bd1394e786ff13776673904bf0a4fc6b812eb9849ec71034e9fc1019a9e0bbe91f84010d8a8088cd41a9
languageName: node
linkType: hard
"dotenv-cli@npm:^7.4.2":
version: 7.4.4
resolution: "dotenv-cli@npm:7.4.4"
@ -10401,13 +10255,6 @@ __metadata:
languageName: node
linkType: hard
"form-data-encoder@npm:^4.0.2":
version: 4.0.2
resolution: "form-data-encoder@npm:4.0.2"
checksum: 10c0/559d3130e265316452434eaf68d68560fb36392ff4d04614683419de4fb43c3dbe152dc303599fae382ce24d3451a6d3d289d3bcc182ae3d8ad32e7ce8e35e53
languageName: node
linkType: hard
"form-data@npm:^4.0.0":
version: 4.0.2
resolution: "form-data@npm:4.0.2"
@ -10647,16 +10494,6 @@ __metadata:
languageName: node
linkType: hard
"generative-bayesian-network@npm:^2.1.63":
version: 2.1.63
resolution: "generative-bayesian-network@npm:2.1.63"
dependencies:
adm-zip: "npm:^0.5.9"
tslib: "npm:^2.4.0"
checksum: 10c0/d0b886663f14f7b8a43ea7f03fdac4e5d83f692a7829c5ed6af2ac6777b30b1c560dc1c55525bd7f50b0d0bf6ad28109151e85741b6f8dd4f0eb87f0dce42ea8
languageName: node
linkType: hard
"gensync@npm:^1.0.0-beta.2":
version: 1.0.0-beta.2
resolution: "gensync@npm:1.0.0-beta.2"
@ -10746,16 +10583,6 @@ __metadata:
languageName: node
linkType: hard
"get-stream@npm:^9.0.1":
version: 9.0.1
resolution: "get-stream@npm:9.0.1"
dependencies:
"@sec-ant/readable-stream": "npm:^0.4.1"
is-stream: "npm:^4.0.1"
checksum: 10c0/d70e73857f2eea1826ac570c3a912757dcfbe8a718a033fa0c23e12ac8e7d633195b01710e0559af574cbb5af101009b42df7b6f6b29ceec8dbdf7291931b948
languageName: node
linkType: hard
"get-uri@npm:^6.0.1":
version: 6.0.4
resolution: "get-uri@npm:6.0.4"
@ -10811,7 +10638,7 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^10.0.0, glob@npm:^10.3.12, glob@npm:^10.3.7, glob@npm:^10.4.1":
"glob@npm:^10.0.0, glob@npm:^10.3.12, glob@npm:^10.3.7":
version: 10.4.5
resolution: "glob@npm:10.4.5"
dependencies:
@ -10958,21 +10785,6 @@ __metadata:
languageName: node
linkType: hard
"got-scraping@npm:^4.1.1":
version: 4.1.1
resolution: "got-scraping@npm:4.1.1"
dependencies:
got: "npm:^14.2.1"
header-generator: "npm:^2.1.41"
http2-wrapper: "npm:^2.2.0"
mimic-response: "npm:^4.0.0"
ow: "npm:^1.1.1"
quick-lru: "npm:^7.0.0"
tslib: "npm:^2.6.2"
checksum: 10c0/66b9bd88fea1c7a1248fec6e9c9757300b70e6039d2b2e0cf1c70e44e88be80f02a26e2e36d5f9c3acb4ec963558d72b0d236a7f11a7a6c87b39b5615afcf7db
languageName: node
linkType: hard
"got@npm:13.0.0":
version: 13.0.0
resolution: "got@npm:13.0.0"
@ -11011,25 +10823,6 @@ __metadata:
languageName: node
linkType: hard
"got@npm:^14.2.1":
version: 14.4.7
resolution: "got@npm:14.4.7"
dependencies:
"@sindresorhus/is": "npm:^7.0.1"
"@szmarczak/http-timer": "npm:^5.0.1"
cacheable-lookup: "npm:^7.0.0"
cacheable-request: "npm:^12.0.1"
decompress-response: "npm:^6.0.0"
form-data-encoder: "npm:^4.0.2"
http2-wrapper: "npm:^2.2.1"
lowercase-keys: "npm:^3.0.0"
p-cancelable: "npm:^4.0.1"
responselike: "npm:^3.0.0"
type-fest: "npm:^4.26.1"
checksum: 10c0/9b5b8dbc0642c78dbc64ab5ff6f12f6edab3e0cb80e89a3a69623a79ba3986f0ff0066a116fba47c0aacce4b0ba1eccf72f923f7fac13a31ce852bf9e2cb8f81
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.10, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
@ -11336,18 +11129,6 @@ __metadata:
languageName: node
linkType: hard
"header-generator@npm:^2.1.41":
version: 2.1.63
resolution: "header-generator@npm:2.1.63"
dependencies:
browserslist: "npm:^4.21.1"
generative-bayesian-network: "npm:^2.1.63"
ow: "npm:^0.28.1"
tslib: "npm:^2.4.0"
checksum: 10c0/a6f49019d77df53dfaa0f6e4ad7f7b99a8ee5d598eeed5956d0adbd32a22846d2c7dfb4e912cdc0a06eb954b7c39a285bd1b329e479ae9d6f122e6c544425297
languageName: node
linkType: hard
"hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
@ -11389,13 +11170,6 @@ __metadata:
languageName: node
linkType: hard
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0
languageName: node
linkType: hard
"html-parse-stringify@npm:^3.0.1":
version: 3.0.1
resolution: "html-parse-stringify@npm:3.0.1"
@ -11451,13 +11225,20 @@ __metadata:
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1":
"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0":
version: 4.1.1
resolution: "http-cache-semantics@npm:4.1.1"
checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.1.1":
version: 4.2.0
resolution: "http-cache-semantics@npm:4.2.0"
checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37
languageName: node
linkType: hard
"http-errors@npm:2.0.0, http-errors@npm:^2.0.0":
version: 2.0.0
resolution: "http-errors@npm:2.0.0"
@ -11513,7 +11294,7 @@ __metadata:
languageName: node
linkType: hard
"http2-wrapper@npm:^2.1.10, http2-wrapper@npm:^2.2.0, http2-wrapper@npm:^2.2.1":
"http2-wrapper@npm:^2.1.10":
version: 2.2.1
resolution: "http2-wrapper@npm:2.2.1"
dependencies:
@ -11650,9 +11431,9 @@ __metadata:
linkType: hard
"immutable@npm:^5.0.2":
version: 5.1.1
resolution: "immutable@npm:5.1.1"
checksum: 10c0/5fd129ee9e448884003cc4f9e43bb91bab3b39dfeb3b49ddfb8bd563e0620eb47ae1f5b3ef96615d3ec38b52ab9a966dcacf9e39df00ed1a8ad062ddfba01cdf
version: 5.1.2
resolution: "immutable@npm:5.1.2"
checksum: 10c0/da5af92d2c70323c1f9a0e418832c9eef441feadaf6a295a4e07764bd2400c85186872e016071d9253549d58d364160d55dca8dcdf59fd4a6a06c6756fe61657
languageName: node
linkType: hard
@ -12042,13 +11823,6 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^4.0.1":
version: 4.0.1
resolution: "is-stream@npm:4.0.1"
checksum: 10c0/2706c7f19b851327ba374687bc4a3940805e14ca496dc672b9629e744d143b1ad9c6f1b162dece81c7bfbc0f83b32b61ccc19ad2e05aad2dd7af347408f60c7f
languageName: node
linkType: hard
"is-typedarray@npm:~1.0.0":
version: 1.0.0
resolution: "is-typedarray@npm:1.0.0"
@ -12114,45 +11888,6 @@ __metadata:
languageName: node
linkType: hard
"istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.2":
version: 3.2.2
resolution: "istanbul-lib-coverage@npm:3.2.2"
checksum: 10c0/6c7ff2106769e5f592ded1fb418f9f73b4411fd5a084387a5410538332b6567cd1763ff6b6cadca9b9eb2c443cce2f7ea7d7f1b8d315f9ce58539793b1e0922b
languageName: node
linkType: hard
"istanbul-lib-report@npm:^3.0.0, istanbul-lib-report@npm:^3.0.1":
version: 3.0.1
resolution: "istanbul-lib-report@npm:3.0.1"
dependencies:
istanbul-lib-coverage: "npm:^3.0.0"
make-dir: "npm:^4.0.0"
supports-color: "npm:^7.1.0"
checksum: 10c0/84323afb14392de8b6a5714bd7e9af845cfbd56cfe71ed276cda2f5f1201aea673c7111901227ee33e68e4364e288d73861eb2ed48f6679d1e69a43b6d9b3ba7
languageName: node
linkType: hard
"istanbul-lib-source-maps@npm:^5.0.6":
version: 5.0.6
resolution: "istanbul-lib-source-maps@npm:5.0.6"
dependencies:
"@jridgewell/trace-mapping": "npm:^0.3.23"
debug: "npm:^4.1.1"
istanbul-lib-coverage: "npm:^3.0.0"
checksum: 10c0/ffe75d70b303a3621ee4671554f306e0831b16f39ab7f4ab52e54d356a5d33e534d97563e318f1333a6aae1d42f91ec49c76b6cd3f3fb378addcb5c81da0255f
languageName: node
linkType: hard
"istanbul-reports@npm:^3.1.7":
version: 3.1.7
resolution: "istanbul-reports@npm:3.1.7"
dependencies:
html-escaper: "npm:^2.0.0"
istanbul-lib-report: "npm:^3.0.0"
checksum: 10c0/a379fadf9cf8dc5dfe25568115721d4a7eb82fbd50b005a6672aff9c6989b20cc9312d7865814e0859cd8df58cbf664482e1d3604be0afde1f7fc3ccc1394a51
languageName: node
linkType: hard
"jackspeak@npm:^3.1.2":
version: 3.4.3
resolution: "jackspeak@npm:3.4.3"
@ -12207,7 +11942,7 @@ __metadata:
languageName: node
linkType: hard
"js-tiktoken@npm:^1.0.12, js-tiktoken@npm:^1.0.14":
"js-tiktoken@npm:^1.0.12":
version: 1.0.19
resolution: "js-tiktoken@npm:1.0.19"
dependencies:
@ -13064,17 +12799,6 @@ __metadata:
languageName: node
linkType: hard
"magicast@npm:^0.3.5":
version: 0.3.5
resolution: "magicast@npm:0.3.5"
dependencies:
"@babel/parser": "npm:^7.25.4"
"@babel/types": "npm:^7.25.4"
source-map-js: "npm:^1.2.0"
checksum: 10c0/a6cacc0a848af84f03e3f5bda7b0de75e4d0aa9ddce5517fd23ed0f31b5ddd51b2d0ff0b7e09b51f7de0f4053c7a1107117edda6b0732dca3e9e39e6c5a68c64
languageName: node
linkType: hard
"make-dir@npm:^1.0.0":
version: 1.3.0
resolution: "make-dir@npm:1.3.0"
@ -13084,15 +12808,6 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^4.0.0":
version: 4.0.0
resolution: "make-dir@npm:4.0.0"
dependencies:
semver: "npm:^7.5.3"
checksum: 10c0/69b98a6c0b8e5c4fe9acb61608a9fbcfca1756d910f51e5dbe7a9e5cfb74fca9b8a0c8a0ffdf1294a740826c1ab4871d5bf3f62f72a3049e5eac6541ddffed68
languageName: node
linkType: hard
"make-fetch-happen@npm:^10.0.3, make-fetch-happen@npm:^10.2.1":
version: 10.2.1
resolution: "make-fetch-happen@npm:10.2.1"
@ -14198,7 +13913,7 @@ __metadata:
languageName: node
linkType: hard
"mime@npm:^4.0.4, mime@npm:^4.0.6":
"mime@npm:^4.0.6":
version: 4.0.7
resolution: "mime@npm:4.0.7"
bin:
@ -14776,7 +14491,7 @@ __metadata:
languageName: node
linkType: hard
"normalize-url@npm:^8.0.0, normalize-url@npm:^8.0.1":
"normalize-url@npm:^8.0.0":
version: 8.0.1
resolution: "normalize-url@npm:8.0.1"
checksum: 10c0/eb439231c4b84430f187530e6fdac605c5048ef4ec556447a10c00a91fc69b52d8d8298d9d608e68d3e0f7dc2d812d3455edf425e0f215993667c3183bcab1ef
@ -15102,32 +14817,6 @@ __metadata:
languageName: node
linkType: hard
"ow@npm:^0.28.1":
version: 0.28.2
resolution: "ow@npm:0.28.2"
dependencies:
"@sindresorhus/is": "npm:^4.2.0"
callsites: "npm:^3.1.0"
dot-prop: "npm:^6.0.1"
lodash.isequal: "npm:^4.5.0"
vali-date: "npm:^1.0.0"
checksum: 10c0/8d0de10fd3aa1ab69dd844ace087718c31ceb1a25cf79d38a5be4d0a5da46f960b6bc15a95405747899b882fb51dcf5a502d7e6508005d1c57e157d12fa17cdd
languageName: node
linkType: hard
"ow@npm:^1.1.1":
version: 1.1.1
resolution: "ow@npm:1.1.1"
dependencies:
"@sindresorhus/is": "npm:^5.3.0"
callsites: "npm:^4.0.0"
dot-prop: "npm:^7.2.0"
lodash.isequal: "npm:^4.5.0"
vali-date: "npm:^1.0.0"
checksum: 10c0/3973f9d6245f2e468a0f1d614ece96f1289632f7425094e8b266b50ddbe79471f2e6cba447b80e90b54bbeb13c20e83671edfb5ef4c0b13c15546ba0710554e1
languageName: node
linkType: hard
"p-cancelable@npm:^2.0.0":
version: 2.1.1
resolution: "p-cancelable@npm:2.1.1"
@ -15142,13 +14831,6 @@ __metadata:
languageName: node
linkType: hard
"p-cancelable@npm:^4.0.1":
version: 4.0.1
resolution: "p-cancelable@npm:4.0.1"
checksum: 10c0/12636623f46784ba962b6fe7a1f34d021f1d9a2cc12c43e270baa715ea872d5c8c7d9f086ed420b8b9817e91d9bbe92c14c90e5dddd4a9968c81a2a7aef7089d
languageName: node
linkType: hard
"p-finally@npm:^1.0.0":
version: 1.0.0
resolution: "p-finally@npm:1.0.0"
@ -16043,13 +15725,6 @@ __metadata:
languageName: node
linkType: hard
"quick-lru@npm:^7.0.0":
version: 7.0.1
resolution: "quick-lru@npm:7.0.1"
checksum: 10c0/631d031d9aba116311b1db57fbf8637874f2b72731f435a9d015cc0405aae5d18206336953563627ca7c9ed971a3824f11cb4dc1575d03283252a8cea22ac8e1
languageName: node
linkType: hard
"raf-schd@npm:^4.0.3":
version: 4.0.3
resolution: "raf-schd@npm:4.0.3"
@ -17577,9 +17252,9 @@ __metadata:
languageName: node
linkType: hard
"sass@npm:^1.77.2":
version: 1.86.3
resolution: "sass@npm:1.86.3"
"sass@npm:^1.88.0":
version: 1.88.0
resolution: "sass@npm:1.88.0"
dependencies:
"@parcel/watcher": "npm:^2.4.1"
chokidar: "npm:^4.0.0"
@ -17590,7 +17265,7 @@ __metadata:
optional: true
bin:
sass: sass.js
checksum: 10c0/ba819a0828f732adf7a94cd8ca017bce92bc299ffb878836ed1da80a30612bfbbf56a5e42d6dff3ad80d919c2025afb42948fc7b54a7bc61a9a2d58e1e0c558a
checksum: 10c0/dcb16dc29116bfa5a90485d24fd8020d2b0d95155bd2e31285901588729343b59fefe44365c5f146b2ba5a9ebadef90b23a7220b902507bdbd91ca2ba0a0b688
languageName: node
linkType: hard
@ -17984,7 +17659,7 @@ __metadata:
languageName: node
linkType: hard
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.0, source-map-js@npm:^1.2.1":
"source-map-js@npm:>=0.6.2 <2.0.0, source-map-js@npm:^1.2.1":
version: 1.2.1
resolution: "source-map-js@npm:1.2.1"
checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf
@ -18581,17 +18256,6 @@ __metadata:
languageName: node
linkType: hard
"test-exclude@npm:^7.0.1":
version: 7.0.1
resolution: "test-exclude@npm:7.0.1"
dependencies:
"@istanbuljs/schema": "npm:^0.1.2"
glob: "npm:^10.4.1"
minimatch: "npm:^9.0.4"
checksum: 10c0/6d67b9af4336a2e12b26a68c83308c7863534c65f27ed4ff7068a56f5a58f7ac703e8fc80f698a19bb154fd8f705cdf7ec347d9512b2c522c737269507e7b263
languageName: node
linkType: hard
"text-decoder@npm:^1.1.0":
version: 1.2.3
resolution: "text-decoder@npm:1.2.3"
@ -18689,7 +18353,7 @@ __metadata:
languageName: node
linkType: hard
"tinycolor2@npm:^1.4.1, tinycolor2@npm:^1.6.0":
"tinycolor2@npm:^1.4.1":
version: 1.6.0
resolution: "tinycolor2@npm:1.6.0"
checksum: 10c0/9aa79a36ba2c2a87cb221453465cabacd04b9e35f9694373e846fdc78b1c768110f81e581ea41440106c0f24d9a023891d0887e8075885e790ac40eb0e74a5c1
@ -18945,7 +18609,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.1":
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62
@ -18961,13 +18625,6 @@ __metadata:
languageName: node
linkType: hard
"turndown-plugin-gfm@npm:^1.0.2":
version: 1.0.2
resolution: "turndown-plugin-gfm@npm:1.0.2"
checksum: 10c0/eb9bc20dbb08d5335231f9617d7440f14b35781f14a3a393d8f13fc8205afeb11a0a632d52da4548ab0fa353f315ca265462b24d368faf23258dccbe439182b9
languageName: node
linkType: hard
"turndown@npm:^7.2.0":
version: 7.2.0
resolution: "turndown@npm:7.2.0"
@ -19000,14 +18657,14 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^2.11.2, type-fest@npm:^2.17.0":
"type-fest@npm:^2.17.0":
version: 2.19.0
resolution: "type-fest@npm:2.19.0"
checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb
languageName: node
linkType: hard
"type-fest@npm:^4.26.1, type-fest@npm:^4.39.1":
"type-fest@npm:^4.39.1":
version: 4.40.0
resolution: "type-fest@npm:4.40.0"
checksum: 10c0/b39d4da6f9a154e3db7e714cd05ccf56b53f4f0bbf74dd294cb6be4921b16ecca5cb00cb81b53ab621a31c8e8509c74b5101895ada47af9de368a317d24538a3
@ -19433,13 +19090,6 @@ __metadata:
languageName: node
linkType: hard
"vali-date@npm:^1.0.0":
version: 1.0.0
resolution: "vali-date@npm:1.0.0"
checksum: 10c0/5755215f6734caab535f60af0a32bbbf2052c61b1a40668d773df78fd3754e4fe9da2ea5466731505f3e0a599acc209d5578c4b70488ed120fb03f0c2ab06449
languageName: node
linkType: hard
"validate-npm-package-license@npm:^3.0.1":
version: 3.0.4
resolution: "validate-npm-package-license@npm:3.0.4"