Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02e9214c20 |
+6
-7
@@ -81,17 +81,17 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch",
|
"@anthropic-ai/claude-agent-sdk": "patch:@anthropic-ai/claude-agent-sdk@npm%3A0.1.30#~/.yarn/patches/@anthropic-ai-claude-agent-sdk-npm-0.1.30-b50a299674.patch",
|
||||||
"@libsql/client": "0.14.0",
|
"@libsql/client": "0.15.15",
|
||||||
"@libsql/win32-x64-msvc": "^0.4.7",
|
"@libsql/win32-x64-msvc": "^0.5.22",
|
||||||
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
"@napi-rs/system-ocr": "patch:@napi-rs/system-ocr@npm%3A1.0.2#~/.yarn/patches/@napi-rs-system-ocr-npm-1.0.2-59e7a78e8b.patch",
|
||||||
"@paymoapp/electron-shutdown-handler": "^1.1.2",
|
"@paymoapp/electron-shutdown-handler": "^1.1.2",
|
||||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"font-list": "^2.0.0",
|
"font-list": "^2.0.0",
|
||||||
"graceful-fs": "^4.2.11",
|
"graceful-fs": "^4.2.11",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsdom": "26.1.0",
|
"jsdom": "26.1.0",
|
||||||
|
"libsql": "^0.5.22",
|
||||||
"node-stream-zip": "^1.15.0",
|
"node-stream-zip": "^1.15.0",
|
||||||
"officeparser": "^4.2.0",
|
"officeparser": "^4.2.0",
|
||||||
"os-proxy-config": "^1.1.2",
|
"os-proxy-config": "^1.1.2",
|
||||||
@@ -127,7 +127,6 @@
|
|||||||
"@biomejs/biome": "2.2.4",
|
"@biomejs/biome": "2.2.4",
|
||||||
"@cherrystudio/ai-core": "workspace:^1.0.9",
|
"@cherrystudio/ai-core": "workspace:^1.0.9",
|
||||||
"@cherrystudio/embedjs": "^0.1.31",
|
"@cherrystudio/embedjs": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-libsql": "^0.1.31",
|
|
||||||
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
"@cherrystudio/embedjs-loader-csv": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-image": "^0.1.31",
|
"@cherrystudio/embedjs-loader-image": "^0.1.31",
|
||||||
"@cherrystudio/embedjs-loader-markdown": "^0.1.31",
|
"@cherrystudio/embedjs-loader-markdown": "^0.1.31",
|
||||||
@@ -140,6 +139,7 @@
|
|||||||
"@cherrystudio/embedjs-openai": "^0.1.31",
|
"@cherrystudio/embedjs-openai": "^0.1.31",
|
||||||
"@cherrystudio/extension-table-plus": "workspace:^",
|
"@cherrystudio/extension-table-plus": "workspace:^",
|
||||||
"@cherrystudio/openai": "^6.9.0",
|
"@cherrystudio/openai": "^6.9.0",
|
||||||
|
"@defi-failure/embedjs-libsql": "0.1.33",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -215,8 +215,8 @@
|
|||||||
"@types/mime-types": "^3",
|
"@types/mime-types": "^3",
|
||||||
"@types/node": "^22.17.1",
|
"@types/node": "^22.17.1",
|
||||||
"@types/pako": "^1.0.2",
|
"@types/pako": "^1.0.2",
|
||||||
"@types/react": "^19.2.6",
|
"@types/react": "^19.0.12",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
"@types/react-transition-group": "^4.4.12",
|
"@types/react-transition-group": "^4.4.12",
|
||||||
"@types/react-window": "^1",
|
"@types/react-window": "^1",
|
||||||
@@ -389,7 +389,6 @@
|
|||||||
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
|
"atomically@npm:^1.7.0": "patch:atomically@npm%3A1.7.0#~/.yarn/patches/atomically-npm-1.7.0-e742e5293b.patch",
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
|
"file-stream-rotator@npm:^0.6.1": "patch:file-stream-rotator@npm%3A0.6.1#~/.yarn/patches/file-stream-rotator-npm-0.6.1-eab45fb13d.patch",
|
||||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
|
||||||
"node-abi": "4.24.0",
|
"node-abi": "4.24.0",
|
||||||
"openai@npm:^4.77.0": "npm:@cherrystudio/openai@6.5.0",
|
"openai@npm:^4.77.0": "npm:@cherrystudio/openai@6.5.0",
|
||||||
"openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.5.0",
|
"openai@npm:^4.87.3": "npm:@cherrystudio/openai@6.5.0",
|
||||||
|
|||||||
@@ -196,9 +196,6 @@ export enum IpcChannel {
|
|||||||
File_ValidateNotesDirectory = 'file:validateNotesDirectory',
|
File_ValidateNotesDirectory = 'file:validateNotesDirectory',
|
||||||
File_StartWatcher = 'file:startWatcher',
|
File_StartWatcher = 'file:startWatcher',
|
||||||
File_StopWatcher = 'file:stopWatcher',
|
File_StopWatcher = 'file:stopWatcher',
|
||||||
File_PauseWatcher = 'file:pauseWatcher',
|
|
||||||
File_ResumeWatcher = 'file:resumeWatcher',
|
|
||||||
File_BatchUploadMarkdown = 'file:batchUploadMarkdown',
|
|
||||||
File_ShowInFolder = 'file:showInFolder',
|
File_ShowInFolder = 'file:showInFolder',
|
||||||
|
|
||||||
// file service
|
// file service
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type LoaderReturn = {
|
|||||||
messageSource?: 'preprocess' | 'embedding' | 'validation'
|
messageSource?: 'preprocess' | 'embedding' | 'validation'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir' | 'refresh'
|
export type FileChangeEventType = 'add' | 'change' | 'unlink' | 'addDir' | 'unlinkDir'
|
||||||
|
|
||||||
export type FileChangeEvent = {
|
export type FileChangeEvent = {
|
||||||
eventType: FileChangeEventType
|
eventType: FileChangeEventType
|
||||||
|
|||||||
@@ -557,9 +557,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.File_ValidateNotesDirectory, fileManager.validateNotesDirectory.bind(fileManager))
|
ipcMain.handle(IpcChannel.File_ValidateNotesDirectory, fileManager.validateNotesDirectory.bind(fileManager))
|
||||||
ipcMain.handle(IpcChannel.File_StartWatcher, fileManager.startFileWatcher.bind(fileManager))
|
ipcMain.handle(IpcChannel.File_StartWatcher, fileManager.startFileWatcher.bind(fileManager))
|
||||||
ipcMain.handle(IpcChannel.File_StopWatcher, fileManager.stopFileWatcher.bind(fileManager))
|
ipcMain.handle(IpcChannel.File_StopWatcher, fileManager.stopFileWatcher.bind(fileManager))
|
||||||
ipcMain.handle(IpcChannel.File_PauseWatcher, fileManager.pauseFileWatcher.bind(fileManager))
|
|
||||||
ipcMain.handle(IpcChannel.File_ResumeWatcher, fileManager.resumeFileWatcher.bind(fileManager))
|
|
||||||
ipcMain.handle(IpcChannel.File_BatchUploadMarkdown, fileManager.batchUploadMarkdownFiles.bind(fileManager))
|
|
||||||
ipcMain.handle(IpcChannel.File_ShowInFolder, fileManager.showInFolder.bind(fileManager))
|
ipcMain.handle(IpcChannel.File_ShowInFolder, fileManager.showInFolder.bind(fileManager))
|
||||||
|
|
||||||
// file service
|
// file service
|
||||||
|
|||||||
@@ -1605,164 +1605,6 @@ class FileStorage {
|
|||||||
logger.error('Failed to show item in folder:', error as Error)
|
logger.error('Failed to show item in folder:', error as Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch upload markdown files from native File objects
|
|
||||||
* This handles all I/O operations in the Main process to avoid blocking Renderer
|
|
||||||
*/
|
|
||||||
public batchUploadMarkdownFiles = async (
|
|
||||||
_: Electron.IpcMainInvokeEvent,
|
|
||||||
filePaths: string[],
|
|
||||||
targetPath: string
|
|
||||||
): Promise<{
|
|
||||||
fileCount: number
|
|
||||||
folderCount: number
|
|
||||||
skippedFiles: number
|
|
||||||
}> => {
|
|
||||||
try {
|
|
||||||
logger.info('Starting batch upload', { fileCount: filePaths.length, targetPath })
|
|
||||||
|
|
||||||
const basePath = path.resolve(targetPath)
|
|
||||||
const MARKDOWN_EXTS = ['.md', '.markdown']
|
|
||||||
|
|
||||||
// Filter markdown files
|
|
||||||
const markdownFiles = filePaths.filter((filePath) => {
|
|
||||||
const ext = path.extname(filePath).toLowerCase()
|
|
||||||
return MARKDOWN_EXTS.includes(ext)
|
|
||||||
})
|
|
||||||
|
|
||||||
const skippedFiles = filePaths.length - markdownFiles.length
|
|
||||||
|
|
||||||
if (markdownFiles.length === 0) {
|
|
||||||
return { fileCount: 0, folderCount: 0, skippedFiles }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect unique folders needed
|
|
||||||
const foldersSet = new Set<string>()
|
|
||||||
const fileOperations: Array<{ sourcePath: string; targetPath: string }> = []
|
|
||||||
|
|
||||||
for (const filePath of markdownFiles) {
|
|
||||||
try {
|
|
||||||
// Get relative path if file is from a directory upload
|
|
||||||
const fileName = path.basename(filePath)
|
|
||||||
const relativePath = path.dirname(filePath)
|
|
||||||
|
|
||||||
// Determine target directory structure
|
|
||||||
let targetDir = basePath
|
|
||||||
const folderParts: string[] = []
|
|
||||||
|
|
||||||
// Extract folder structure from file path for nested uploads
|
|
||||||
// This is a simplified version - in real scenario we'd need the original directory structure
|
|
||||||
if (relativePath && relativePath !== '.') {
|
|
||||||
const parts = relativePath.split(path.sep)
|
|
||||||
// Get the last few parts that represent the folder structure within upload
|
|
||||||
const relevantParts = parts.slice(Math.max(0, parts.length - 3))
|
|
||||||
folderParts.push(...relevantParts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build target directory path
|
|
||||||
for (const part of folderParts) {
|
|
||||||
targetDir = path.join(targetDir, part)
|
|
||||||
foldersSet.add(targetDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine final file name
|
|
||||||
const nameWithoutExt = fileName.endsWith('.md')
|
|
||||||
? fileName.slice(0, -3)
|
|
||||||
: fileName.endsWith('.markdown')
|
|
||||||
? fileName.slice(0, -9)
|
|
||||||
: fileName
|
|
||||||
|
|
||||||
const { safeName } = await this.fileNameGuard(_, targetDir, nameWithoutExt, true)
|
|
||||||
const finalPath = path.join(targetDir, safeName + '.md')
|
|
||||||
|
|
||||||
fileOperations.push({ sourcePath: filePath, targetPath: finalPath })
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Failed to prepare file operation:', error as Error, { filePath })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create folders in order (shallow to deep)
|
|
||||||
const sortedFolders = Array.from(foldersSet).sort((a, b) => a.length - b.length)
|
|
||||||
for (const folder of sortedFolders) {
|
|
||||||
try {
|
|
||||||
if (!fs.existsSync(folder)) {
|
|
||||||
await fs.promises.mkdir(folder, { recursive: true })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug('Folder already exists or creation failed', { folder, error: (error as Error).message })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process files in batches
|
|
||||||
const BATCH_SIZE = 10 // Higher batch size since we're in Main process
|
|
||||||
let successCount = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < fileOperations.length; i += BATCH_SIZE) {
|
|
||||||
const batch = fileOperations.slice(i, i + BATCH_SIZE)
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
|
||||||
batch.map(async (op) => {
|
|
||||||
// Read from source and write to target in Main process
|
|
||||||
const content = await fs.promises.readFile(op.sourcePath, 'utf-8')
|
|
||||||
await fs.promises.writeFile(op.targetPath, content, 'utf-8')
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
results.forEach((result, index) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
successCount++
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to upload file:', result.reason, {
|
|
||||||
file: batch[index].sourcePath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('Batch upload completed', {
|
|
||||||
successCount,
|
|
||||||
folderCount: foldersSet.size,
|
|
||||||
skippedFiles
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
fileCount: successCount,
|
|
||||||
folderCount: foldersSet.size,
|
|
||||||
skippedFiles
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Batch upload failed:', error as Error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause file watcher to prevent events during batch operations
|
|
||||||
*/
|
|
||||||
public pauseFileWatcher = async (): Promise<void> => {
|
|
||||||
if (this.watcher) {
|
|
||||||
logger.debug('Pausing file watcher')
|
|
||||||
// Chokidar doesn't have pause, so we temporarily set a flag
|
|
||||||
// We'll handle this by clearing the debounce timer
|
|
||||||
if (this.debounceTimer) {
|
|
||||||
clearTimeout(this.debounceTimer)
|
|
||||||
this.debounceTimer = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resume file watcher and trigger a refresh
|
|
||||||
*/
|
|
||||||
public resumeFileWatcher = async (): Promise<void> => {
|
|
||||||
if (this.watcher && this.currentWatchPath) {
|
|
||||||
logger.debug('Resuming file watcher')
|
|
||||||
// Send a synthetic refresh event to trigger tree reload
|
|
||||||
this.notifyChange('refresh', this.currentWatchPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fileStorage = new FileStorage()
|
export const fileStorage = new FileStorage()
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import path from 'node:path'
|
|||||||
|
|
||||||
import type { RAGApplication } from '@cherrystudio/embedjs'
|
import type { RAGApplication } from '@cherrystudio/embedjs'
|
||||||
import { RAGApplicationBuilder } from '@cherrystudio/embedjs'
|
import { RAGApplicationBuilder } from '@cherrystudio/embedjs'
|
||||||
import { LibSqlDb } from '@cherrystudio/embedjs-libsql'
|
|
||||||
import { SitemapLoader } from '@cherrystudio/embedjs-loader-sitemap'
|
import { SitemapLoader } from '@cherrystudio/embedjs-loader-sitemap'
|
||||||
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
|
import { WebLoader } from '@cherrystudio/embedjs-loader-web'
|
||||||
|
import { LibSqlDb } from '@defi-failure/embedjs-libsql'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import Embeddings from '@main/knowledge/embedjs/embeddings/Embeddings'
|
import Embeddings from '@main/knowledge/embedjs/embeddings/Embeddings'
|
||||||
import { addFileLoader } from '@main/knowledge/embedjs/loader'
|
import { addFileLoader } from '@main/knowledge/embedjs/loader'
|
||||||
|
|||||||
@@ -220,10 +220,6 @@ const api = {
|
|||||||
startFileWatcher: (dirPath: string, config?: any) =>
|
startFileWatcher: (dirPath: string, config?: any) =>
|
||||||
ipcRenderer.invoke(IpcChannel.File_StartWatcher, dirPath, config),
|
ipcRenderer.invoke(IpcChannel.File_StartWatcher, dirPath, config),
|
||||||
stopFileWatcher: () => ipcRenderer.invoke(IpcChannel.File_StopWatcher),
|
stopFileWatcher: () => ipcRenderer.invoke(IpcChannel.File_StopWatcher),
|
||||||
pauseFileWatcher: () => ipcRenderer.invoke(IpcChannel.File_PauseWatcher),
|
|
||||||
resumeFileWatcher: () => ipcRenderer.invoke(IpcChannel.File_ResumeWatcher),
|
|
||||||
batchUploadMarkdown: (filePaths: string[], targetPath: string) =>
|
|
||||||
ipcRenderer.invoke(IpcChannel.File_BatchUploadMarkdown, filePaths, targetPath),
|
|
||||||
onFileChange: (callback: (data: FileChangeEvent) => void) => {
|
onFileChange: (callback: (data: FileChangeEvent) => void) => {
|
||||||
const listener = (_event: Electron.IpcRendererEvent, data: any) => {
|
const listener = (_event: Electron.IpcRendererEvent, data: any) => {
|
||||||
if (data && typeof data === 'object') {
|
if (data && typeof data === 'object') {
|
||||||
|
|||||||
@@ -140,11 +140,11 @@ describe('DynamicVirtualList', () => {
|
|||||||
// Should call isSticky function during rendering
|
// Should call isSticky function during rendering
|
||||||
expect(isSticky).toHaveBeenCalled()
|
expect(isSticky).toHaveBeenCalled()
|
||||||
|
|
||||||
// Sticky items within visible range should have proper z-index but may be absolute until scrolled
|
// Should apply sticky styles to sticky items
|
||||||
const stickyItem = document.querySelector('[data-index="0"]') as HTMLElement
|
const stickyItem = document.querySelector('[data-index="0"]') as HTMLElement
|
||||||
expect(stickyItem).toBeInTheDocument()
|
expect(stickyItem).toBeInTheDocument()
|
||||||
// When sticky item is in visible range, it gets z-index but may not be sticky yet
|
expect(stickyItem).toHaveStyle('position: sticky')
|
||||||
expect(stickyItem).toHaveStyle('z-index: 999')
|
expect(stickyItem).toHaveStyle('z-index: 1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should apply absolute positioning to non-sticky items', () => {
|
it('should apply absolute positioning to non-sticky items', () => {
|
||||||
|
|||||||
+3
-3
@@ -24,7 +24,7 @@ exports[`DynamicVirtualList > basic rendering > snapshot test 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-index="0"
|
data-index="0"
|
||||||
style="position: absolute; top: 0px; left: 0px; z-index: 0; pointer-events: auto; transform: translateY(0px); width: 100%;"
|
style="position: absolute; top: 0px; left: 0px; transform: translateY(0px); width: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="item-0"
|
data-testid="item-0"
|
||||||
@@ -34,7 +34,7 @@ exports[`DynamicVirtualList > basic rendering > snapshot test 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-index="1"
|
data-index="1"
|
||||||
style="position: absolute; top: 0px; left: 0px; z-index: 0; pointer-events: auto; transform: translateY(50px); width: 100%;"
|
style="position: absolute; top: 0px; left: 0px; transform: translateY(50px); width: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="item-1"
|
data-testid="item-1"
|
||||||
@@ -44,7 +44,7 @@ exports[`DynamicVirtualList > basic rendering > snapshot test 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
data-index="2"
|
data-index="2"
|
||||||
style="position: absolute; top: 0px; left: 0px; z-index: 0; pointer-events: auto; transform: translateY(100px); width: 100%;"
|
style="position: absolute; top: 0px; left: 0px; transform: translateY(100px); width: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-testid="item-2"
|
data-testid="item-2"
|
||||||
|
|||||||
@@ -62,12 +62,6 @@ export interface DynamicVirtualListProps<T> extends InheritedVirtualizerOptions
|
|||||||
*/
|
*/
|
||||||
isSticky?: (index: number) => boolean
|
isSticky?: (index: number) => boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the depth/level of an item for hierarchical sticky positioning
|
|
||||||
* Used with isSticky to determine ancestor relationships
|
|
||||||
*/
|
|
||||||
getItemDepth?: (index: number) => number
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Range extractor function, cannot be used with isSticky
|
* Range extractor function, cannot be used with isSticky
|
||||||
*/
|
*/
|
||||||
@@ -107,7 +101,6 @@ function DynamicVirtualList<T>(props: DynamicVirtualListProps<T>) {
|
|||||||
size,
|
size,
|
||||||
estimateSize,
|
estimateSize,
|
||||||
isSticky,
|
isSticky,
|
||||||
getItemDepth,
|
|
||||||
rangeExtractor: customRangeExtractor,
|
rangeExtractor: customRangeExtractor,
|
||||||
itemContainerStyle,
|
itemContainerStyle,
|
||||||
scrollerStyle,
|
scrollerStyle,
|
||||||
@@ -122,7 +115,7 @@ function DynamicVirtualList<T>(props: DynamicVirtualListProps<T>) {
|
|||||||
const internalScrollerRef = useRef<HTMLDivElement>(null)
|
const internalScrollerRef = useRef<HTMLDivElement>(null)
|
||||||
const scrollerRef = internalScrollerRef
|
const scrollerRef = internalScrollerRef
|
||||||
|
|
||||||
const activeStickyIndexesRef = useRef<number[]>([])
|
const activeStickyIndexRef = useRef(0)
|
||||||
|
|
||||||
const stickyIndexes = useMemo(() => {
|
const stickyIndexes = useMemo(() => {
|
||||||
if (!isSticky) return []
|
if (!isSticky) return []
|
||||||
@@ -131,54 +124,21 @@ function DynamicVirtualList<T>(props: DynamicVirtualListProps<T>) {
|
|||||||
|
|
||||||
const internalStickyRangeExtractor = useCallback(
|
const internalStickyRangeExtractor = useCallback(
|
||||||
(range: Range) => {
|
(range: Range) => {
|
||||||
const activeStickies: number[] = []
|
// The active sticky index is the last one that is before or at the start of the visible range
|
||||||
|
const newActiveStickyIndex =
|
||||||
|
[...stickyIndexes].reverse().find((index) => range.startIndex >= index) ?? stickyIndexes[0] ?? 0
|
||||||
|
|
||||||
if (getItemDepth) {
|
if (newActiveStickyIndex !== activeStickyIndexRef.current) {
|
||||||
// With depth information, we can build a proper ancestor chain
|
activeStickyIndexRef.current = newActiveStickyIndex
|
||||||
// Find all sticky items before the visible range
|
|
||||||
const stickiesBeforeRange = stickyIndexes.filter((index) => index < range.startIndex)
|
|
||||||
|
|
||||||
if (stickiesBeforeRange.length > 0) {
|
|
||||||
// Find the depth of the first visible item (or last sticky before it)
|
|
||||||
const firstVisibleIndex = range.startIndex
|
|
||||||
const referenceDepth = getItemDepth(firstVisibleIndex)
|
|
||||||
|
|
||||||
// Build ancestor chain: include all sticky parents
|
|
||||||
const ancestorChain: number[] = []
|
|
||||||
let minDepth = referenceDepth
|
|
||||||
|
|
||||||
// Walk backwards from the last sticky before visible range
|
|
||||||
for (let i = stickiesBeforeRange.length - 1; i >= 0; i--) {
|
|
||||||
const stickyIndex = stickiesBeforeRange[i]
|
|
||||||
const stickyDepth = getItemDepth(stickyIndex)
|
|
||||||
|
|
||||||
// Include this sticky if it's a parent (smaller depth) of our reference
|
|
||||||
if (stickyDepth < minDepth) {
|
|
||||||
ancestorChain.unshift(stickyIndex)
|
|
||||||
minDepth = stickyDepth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeStickies.push(...ancestorChain)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: without depth info, just use the last sticky before range
|
|
||||||
const lastStickyBeforeRange = [...stickyIndexes].reverse().find((index) => index < range.startIndex)
|
|
||||||
if (lastStickyBeforeRange !== undefined) {
|
|
||||||
activeStickies.push(lastStickyBeforeRange)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the ref with current active stickies
|
// Merge the active sticky index and the default range extractor
|
||||||
activeStickyIndexesRef.current = activeStickies
|
const next = new Set([activeStickyIndexRef.current, ...defaultRangeExtractor(range)])
|
||||||
|
|
||||||
// Merge the active sticky indexes and the default range extractor
|
|
||||||
const next = new Set([...activeStickyIndexesRef.current, ...defaultRangeExtractor(range)])
|
|
||||||
|
|
||||||
// Sort the set to maintain proper order
|
// Sort the set to maintain proper order
|
||||||
return [...next].sort((a, b) => a - b)
|
return [...next].sort((a, b) => a - b)
|
||||||
},
|
},
|
||||||
[stickyIndexes, getItemDepth]
|
[stickyIndexes]
|
||||||
)
|
)
|
||||||
|
|
||||||
const rangeExtractor = customRangeExtractor ?? (isSticky ? internalStickyRangeExtractor : undefined)
|
const rangeExtractor = customRangeExtractor ?? (isSticky ? internalStickyRangeExtractor : undefined)
|
||||||
@@ -261,47 +221,14 @@ function DynamicVirtualList<T>(props: DynamicVirtualListProps<T>) {
|
|||||||
}}>
|
}}>
|
||||||
{virtualItems.map((virtualItem) => {
|
{virtualItems.map((virtualItem) => {
|
||||||
const isItemSticky = stickyIndexes.includes(virtualItem.index)
|
const isItemSticky = stickyIndexes.includes(virtualItem.index)
|
||||||
const isItemActiveSticky = isItemSticky && activeStickyIndexesRef.current.includes(virtualItem.index)
|
const isItemActiveSticky = isItemSticky && activeStickyIndexRef.current === virtualItem.index
|
||||||
|
|
||||||
// Calculate the sticky offset for multi-level sticky headers
|
|
||||||
const activeStickyIndex = isItemActiveSticky ? activeStickyIndexesRef.current.indexOf(virtualItem.index) : -1
|
|
||||||
|
|
||||||
// Calculate cumulative offset based on actual sizes of previous sticky items
|
|
||||||
let stickyOffset = 0
|
|
||||||
if (activeStickyIndex >= 0) {
|
|
||||||
for (let i = 0; i < activeStickyIndex; i++) {
|
|
||||||
const prevStickyIndex = activeStickyIndexesRef.current[i]
|
|
||||||
stickyOffset += estimateSize(prevStickyIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this item is visually covered by sticky items
|
|
||||||
// If covered, disable pointer events to prevent hover/click bleeding through
|
|
||||||
const isCoveredBySticky = (() => {
|
|
||||||
if (!activeStickyIndexesRef.current.length) return false
|
|
||||||
if (isItemActiveSticky) return false // Sticky items themselves are not covered
|
|
||||||
|
|
||||||
// Calculate if this item's visual position is under any sticky header
|
|
||||||
const itemVisualTop = virtualItem.start
|
|
||||||
let totalStickyHeight = 0
|
|
||||||
for (const stickyIdx of activeStickyIndexesRef.current) {
|
|
||||||
totalStickyHeight += estimateSize(stickyIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If item starts within the sticky area, it's covered
|
|
||||||
return itemVisualTop < totalStickyHeight
|
|
||||||
})()
|
|
||||||
|
|
||||||
const style: React.CSSProperties = {
|
const style: React.CSSProperties = {
|
||||||
...itemContainerStyle,
|
...itemContainerStyle,
|
||||||
position: isItemActiveSticky ? 'sticky' : 'absolute',
|
position: isItemActiveSticky ? 'sticky' : 'absolute',
|
||||||
top: isItemActiveSticky ? stickyOffset : 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
zIndex: isItemActiveSticky ? 1000 + (100 - activeStickyIndex) : isItemSticky ? 999 : 0,
|
zIndex: isItemSticky ? 1 : undefined,
|
||||||
pointerEvents: isCoveredBySticky ? 'none' : 'auto',
|
|
||||||
...(isItemActiveSticky && {
|
|
||||||
backgroundColor: 'var(--color-background)'
|
|
||||||
}),
|
|
||||||
...(horizontal
|
...(horizontal
|
||||||
? {
|
? {
|
||||||
transform: isItemActiveSticky ? undefined : `translateX(${virtualItem.start}px)`,
|
transform: isItemActiveSticky ? undefined : `translateX(${virtualItem.start}px)`,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { loggerService } from '@logger'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
|
|
||||||
|
import { useTimer } from './useTimer'
|
||||||
|
|
||||||
const logger = loggerService.withContext('useInPlaceEdit')
|
|
||||||
export interface UseInPlaceEditOptions {
|
export interface UseInPlaceEditOptions {
|
||||||
onSave: ((value: string) => void) | ((value: string) => Promise<void>)
|
onSave: ((value: string) => void) | ((value: string) => Promise<void>)
|
||||||
onCancel?: () => void
|
onCancel?: () => void
|
||||||
@@ -12,10 +12,14 @@ export interface UseInPlaceEditOptions {
|
|||||||
export interface UseInPlaceEditReturn {
|
export interface UseInPlaceEditReturn {
|
||||||
isEditing: boolean
|
isEditing: boolean
|
||||||
isSaving: boolean
|
isSaving: boolean
|
||||||
|
editValue: string
|
||||||
|
inputRef: React.RefObject<HTMLInputElement | null>
|
||||||
startEdit: (initialValue: string) => void
|
startEdit: (initialValue: string) => void
|
||||||
saveEdit: () => void
|
saveEdit: () => void
|
||||||
cancelEdit: () => void
|
cancelEdit: () => void
|
||||||
inputProps: React.InputHTMLAttributes<HTMLInputElement> & { ref: React.RefObject<HTMLInputElement | null> }
|
handleKeyDown: (e: React.KeyboardEvent) => void
|
||||||
|
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
|
handleValueChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,55 +37,58 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe
|
|||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [editValue, setEditValue] = useState('')
|
const [editValue, setEditValue] = useState('')
|
||||||
const originalValueRef = useRef('')
|
const [originalValue, setOriginalValue] = useState('')
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const { setTimeoutTimer } = useTimer()
|
||||||
|
|
||||||
const startEdit = useCallback((initialValue: string) => {
|
const startEdit = useCallback(
|
||||||
setIsEditing(true)
|
(initialValue: string) => {
|
||||||
setEditValue(initialValue)
|
setIsEditing(true)
|
||||||
originalValueRef.current = initialValue
|
setEditValue(initialValue)
|
||||||
}, [])
|
setOriginalValue(initialValue)
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
setTimeoutTimer(
|
||||||
if (isEditing) {
|
'startEdit',
|
||||||
inputRef.current?.focus()
|
() => {
|
||||||
if (autoSelectOnStart) {
|
inputRef.current?.focus()
|
||||||
inputRef.current?.select()
|
if (autoSelectOnStart) {
|
||||||
}
|
inputRef.current?.select()
|
||||||
}
|
}
|
||||||
}, [autoSelectOnStart, isEditing])
|
},
|
||||||
|
0
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[autoSelectOnStart, setTimeoutTimer]
|
||||||
|
)
|
||||||
|
|
||||||
const saveEdit = useCallback(async () => {
|
const saveEdit = useCallback(async () => {
|
||||||
if (isSaving) return
|
if (isSaving) return
|
||||||
|
|
||||||
const finalValue = trimOnSave ? editValue.trim() : editValue
|
|
||||||
if (finalValue === originalValueRef.current) {
|
|
||||||
setIsEditing(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onSave(finalValue)
|
const finalValue = trimOnSave ? editValue.trim() : editValue
|
||||||
|
if (finalValue !== originalValue) {
|
||||||
|
await onSave(finalValue)
|
||||||
|
}
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
setEditValue('')
|
setEditValue('')
|
||||||
} catch (error) {
|
setOriginalValue('')
|
||||||
logger.error('Error saving in-place edit', { error })
|
} finally {
|
||||||
setIsSaving(false)
|
setIsSaving(false)
|
||||||
}
|
}
|
||||||
}, [isSaving, trimOnSave, editValue, onSave])
|
}, [isSaving, trimOnSave, editValue, originalValue, onSave])
|
||||||
|
|
||||||
const cancelEdit = useCallback(() => {
|
const cancelEdit = useCallback(() => {
|
||||||
setIsEditing(false)
|
setIsEditing(false)
|
||||||
setEditValue('')
|
setEditValue('')
|
||||||
|
setOriginalValue('')
|
||||||
onCancel?.()
|
onCancel?.()
|
||||||
}, [onCancel])
|
}, [onCancel])
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
if (e.nativeEvent.isComposing) return
|
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
saveEdit()
|
saveEdit()
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === 'Escape') {
|
||||||
@@ -97,29 +104,37 @@ export function useInPlaceEdit(options: UseInPlaceEditOptions): UseInPlaceEditRe
|
|||||||
setEditValue(e.target.value)
|
setEditValue(e.target.value)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleValueChange = useCallback((value: string) => {
|
||||||
// 这里的逻辑需要注意:
|
setEditValue(value)
|
||||||
// 如果点击了“取消”按钮,可能会先触发 Blur 保存。
|
}, [])
|
||||||
// 通常 InPlaceEdit 的逻辑是 Blur 即 Save。
|
|
||||||
// 如果不想 Blur 保存,可以去掉这一行,或者判断 relatedTarget。
|
// Handle clicks outside the input to save
|
||||||
if (!isSaving) {
|
useEffect(() => {
|
||||||
saveEdit()
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (isEditing && inputRef.current && !inputRef.current.contains(event.target as Node)) {
|
||||||
|
saveEdit()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [saveEdit, isSaving])
|
|
||||||
|
if (isEditing) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}, [isEditing, saveEdit])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEditing,
|
isEditing,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
editValue,
|
||||||
|
inputRef,
|
||||||
startEdit,
|
startEdit,
|
||||||
saveEdit,
|
saveEdit,
|
||||||
cancelEdit,
|
cancelEdit,
|
||||||
inputProps: {
|
handleKeyDown,
|
||||||
ref: inputRef,
|
handleInputChange,
|
||||||
value: editValue,
|
handleValueChange
|
||||||
onChange: handleInputChange,
|
|
||||||
onKeyDown: handleKeyDown,
|
|
||||||
onBlur: handleBlur,
|
|
||||||
disabled: isSaving // 保存时禁用输入
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "New Folder",
|
"untitled_folder": "New Folder",
|
||||||
"untitled_note": "Untitled Note",
|
"untitled_note": "Untitled Note",
|
||||||
"upload_failed": "Note upload failed",
|
"upload_failed": "Note upload failed",
|
||||||
"upload_files": "Upload Files",
|
"upload_success": "Note uploaded success"
|
||||||
"upload_folder": "Upload Folder",
|
|
||||||
"upload_success": "Note uploaded success",
|
|
||||||
"uploading_files": "Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "Assistant Response",
|
"assistant": "Assistant Response",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "新文件夹",
|
"untitled_folder": "新文件夹",
|
||||||
"untitled_note": "无标题笔记",
|
"untitled_note": "无标题笔记",
|
||||||
"upload_failed": "笔记上传失败",
|
"upload_failed": "笔记上传失败",
|
||||||
"upload_files": "上传文件",
|
"upload_success": "笔记上传成功"
|
||||||
"upload_folder": "上传文件夹",
|
|
||||||
"upload_success": "笔记上传成功",
|
|
||||||
"uploading_files": "正在上传 {{count}} 个文件..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "助手响应",
|
"assistant": "助手响应",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "新資料夾",
|
"untitled_folder": "新資料夾",
|
||||||
"untitled_note": "無標題筆記",
|
"untitled_note": "無標題筆記",
|
||||||
"upload_failed": "筆記上傳失敗",
|
"upload_failed": "筆記上傳失敗",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "筆記上傳成功"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "筆記上傳成功",
|
|
||||||
"uploading_files": "正在上傳 {{count}} 個檔案..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "助手回應",
|
"assistant": "助手回應",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "Neuer Ordner",
|
"untitled_folder": "Neuer Ordner",
|
||||||
"untitled_note": "Unbenannte Notiz",
|
"untitled_note": "Unbenannte Notiz",
|
||||||
"upload_failed": "Notizen-Upload fehlgeschlagen",
|
"upload_failed": "Notizen-Upload fehlgeschlagen",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "Notizen erfolgreich hochgeladen"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "Notizen erfolgreich hochgeladen",
|
|
||||||
"uploading_files": "[to be translated]:Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "Assistenten-Antwort",
|
"assistant": "Assistenten-Antwort",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "Νέος φάκελος",
|
"untitled_folder": "Νέος φάκελος",
|
||||||
"untitled_note": "σημείωση χωρίς τίτλο",
|
"untitled_note": "σημείωση χωρίς τίτλο",
|
||||||
"upload_failed": "Η σημείωση δεν ανέβηκε",
|
"upload_failed": "Η σημείωση δεν ανέβηκε",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "Οι σημειώσεις μεταφορτώθηκαν με επιτυχία"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "Οι σημειώσεις μεταφορτώθηκαν με επιτυχία",
|
|
||||||
"uploading_files": "[to be translated]:Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "Απάντηση Βοηθού",
|
"assistant": "Απάντηση Βοηθού",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "Nueva carpeta",
|
"untitled_folder": "Nueva carpeta",
|
||||||
"untitled_note": "Nota sin título",
|
"untitled_note": "Nota sin título",
|
||||||
"upload_failed": "Error al cargar la nota",
|
"upload_failed": "Error al cargar la nota",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "Nota cargada con éxito"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "Nota cargada con éxito",
|
|
||||||
"uploading_files": "[to be translated]:Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "Respuesta del asistente",
|
"assistant": "Respuesta del asistente",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "nouveau dossier",
|
"untitled_folder": "nouveau dossier",
|
||||||
"untitled_note": "Note sans titre",
|
"untitled_note": "Note sans titre",
|
||||||
"upload_failed": "Échec du téléchargement de la note",
|
"upload_failed": "Échec du téléchargement de la note",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "Note téléchargée avec succès"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "Note téléchargée avec succès",
|
|
||||||
"uploading_files": "[to be translated]:Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "Réponse de l'assistant",
|
"assistant": "Réponse de l'assistant",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "新ファイル夹",
|
"untitled_folder": "新ファイル夹",
|
||||||
"untitled_note": "無題のメモ",
|
"untitled_note": "無題のメモ",
|
||||||
"upload_failed": "ノートのアップロードに失敗しました",
|
"upload_failed": "ノートのアップロードに失敗しました",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "ノートのアップロードが成功しました"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "ノートのアップロードが成功しました",
|
|
||||||
"uploading_files": "[to be translated]:Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "助手回應",
|
"assistant": "助手回應",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "Nova pasta",
|
"untitled_folder": "Nova pasta",
|
||||||
"untitled_note": "Nota sem título",
|
"untitled_note": "Nota sem título",
|
||||||
"upload_failed": "Falha ao carregar a nota",
|
"upload_failed": "Falha ao carregar a nota",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "Nota carregada com sucesso"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "Nota carregada com sucesso",
|
|
||||||
"uploading_files": "[to be translated]:Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "Resposta do assistente",
|
"assistant": "Resposta do assistente",
|
||||||
|
|||||||
@@ -2175,10 +2175,7 @@
|
|||||||
"untitled_folder": "Новая папка",
|
"untitled_folder": "Новая папка",
|
||||||
"untitled_note": "Незаглавленная заметка",
|
"untitled_note": "Незаглавленная заметка",
|
||||||
"upload_failed": "Не удалось загрузить заметку",
|
"upload_failed": "Не удалось загрузить заметку",
|
||||||
"upload_files": "[to be translated]:Upload Files",
|
"upload_success": "Заметка успешно загружена"
|
||||||
"upload_folder": "[to be translated]:Upload Folder",
|
|
||||||
"upload_success": "Заметка успешно загружена",
|
|
||||||
"uploading_files": "[to be translated]:Uploading {{count}} files..."
|
|
||||||
},
|
},
|
||||||
"notification": {
|
"notification": {
|
||||||
"assistant": "Ответ ассистента",
|
"assistant": "Ответ ассистента",
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
|||||||
const targetSession = useDeferredValue(_targetSession)
|
const targetSession = useDeferredValue(_targetSession)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const { isEditing, isSaving, startEdit, inputProps } = useInPlaceEdit({
|
const { isEditing, isSaving, editValue, inputRef, startEdit, handleKeyDown, handleValueChange } = useInPlaceEdit({
|
||||||
onSave: async (value) => {
|
onSave: async (value) => {
|
||||||
if (value !== session.name) {
|
if (value !== session.name) {
|
||||||
await updateSession({ id: session.id, name: value })
|
await updateSession({ id: session.id, name: value })
|
||||||
@@ -179,7 +179,14 @@ const SessionItem: FC<SessionItemProps> = ({ session, agentId, onDelete, onPress
|
|||||||
{isFulfilled && !isActive && <FulfilledIndicator />}
|
{isFulfilled && !isActive && <FulfilledIndicator />}
|
||||||
<SessionNameContainer>
|
<SessionNameContainer>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<SessionEditInput {...inputProps} style={{ opacity: isSaving ? 0.5 : 1 }} />
|
<SessionEditInput
|
||||||
|
ref={inputRef}
|
||||||
|
value={editValue}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleValueChange(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
||||||
|
style={{ opacity: isSaving ? 0.5 : 1 }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<SessionName>
|
<SessionName>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
|||||||
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
const deleteTimerRef = useRef<NodeJS.Timeout>(null)
|
||||||
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
const [editingTopicId, setEditingTopicId] = useState<string | null>(null)
|
||||||
|
|
||||||
const { startEdit, isEditing, inputProps } = useInPlaceEdit({
|
const topicEdit = useInPlaceEdit({
|
||||||
onSave: (name: string) => {
|
onSave: (name: string) => {
|
||||||
const topic = assistant.topics.find((t) => t.id === editingTopicId)
|
const topic = assistant.topics.find((t) => t.id === editingTopicId)
|
||||||
if (topic && name !== topic.name) {
|
if (topic && name !== topic.name) {
|
||||||
@@ -520,23 +520,29 @@ export const Topics: React.FC<Props> = ({ assistant: _assistant, activeTopic, se
|
|||||||
<TopicListItem
|
<TopicListItem
|
||||||
onContextMenu={() => setTargetTopic(topic)}
|
onContextMenu={() => setTargetTopic(topic)}
|
||||||
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
className={classNames(isActive ? 'active' : '', singlealone ? 'singlealone' : '')}
|
||||||
onClick={editingTopicId === topic.id && isEditing ? undefined : () => onSwitchTopic(topic)}
|
onClick={editingTopicId === topic.id && topicEdit.isEditing ? undefined : () => onSwitchTopic(topic)}
|
||||||
style={{
|
style={{
|
||||||
borderRadius,
|
borderRadius,
|
||||||
cursor: editingTopicId === topic.id && isEditing ? 'default' : 'pointer'
|
cursor: editingTopicId === topic.id && topicEdit.isEditing ? 'default' : 'pointer'
|
||||||
}}>
|
}}>
|
||||||
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
{isPending(topic.id) && !isActive && <PendingIndicator />}
|
||||||
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
{isFulfilled(topic.id) && !isActive && <FulfilledIndicator />}
|
||||||
<TopicNameContainer>
|
<TopicNameContainer>
|
||||||
{editingTopicId === topic.id && isEditing ? (
|
{editingTopicId === topic.id && topicEdit.isEditing ? (
|
||||||
<TopicEditInput {...inputProps} onClick={(e) => e.stopPropagation()} />
|
<TopicEditInput
|
||||||
|
ref={topicEdit.inputRef}
|
||||||
|
value={topicEdit.editValue}
|
||||||
|
onChange={topicEdit.handleInputChange}
|
||||||
|
onKeyDown={topicEdit.handleKeyDown}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TopicName
|
<TopicName
|
||||||
className={getTopicNameClassName()}
|
className={getTopicNameClassName()}
|
||||||
title={topicName}
|
title={topicName}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
setEditingTopicId(topic.id)
|
setEditingTopicId(topic.id)
|
||||||
startEdit(topic.name)
|
topicEdit.startEdit(topic.name)
|
||||||
}}>
|
}}>
|
||||||
{topicName}
|
{topicName}
|
||||||
</TopicName>
|
</TopicName>
|
||||||
|
|||||||
@@ -295,16 +295,6 @@ const NotesPage: FC = () => {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'refresh': {
|
|
||||||
// 批量操作完成后的单次刷新
|
|
||||||
logger.debug('Received refresh event, triggering tree refresh')
|
|
||||||
const refresh = refreshTreeRef.current
|
|
||||||
if (refresh) {
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'add':
|
case 'add':
|
||||||
case 'addDir':
|
case 'addDir':
|
||||||
case 'unlink':
|
case 'unlink':
|
||||||
@@ -631,27 +621,7 @@ const NotesPage: FC = () => {
|
|||||||
throw new Error('No folder path selected')
|
throw new Error('No folder path selected')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate uploadNotes function is available
|
const result = await uploadNotes(files, targetFolderPath)
|
||||||
if (typeof uploadNotes !== 'function') {
|
|
||||||
logger.error('uploadNotes function is not available', { uploadNotes })
|
|
||||||
window.toast.error(t('notes.upload_failed'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let result: Awaited<ReturnType<typeof uploadNotes>>
|
|
||||||
try {
|
|
||||||
result = await uploadNotes(files, targetFolderPath)
|
|
||||||
} catch (uploadError) {
|
|
||||||
logger.error('Upload operation failed:', uploadError as Error)
|
|
||||||
throw uploadError
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate result object
|
|
||||||
if (!result || typeof result !== 'object') {
|
|
||||||
logger.error('Invalid upload result:', { result })
|
|
||||||
window.toast.error(t('notes.upload_failed'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查上传结果
|
// 检查上传结果
|
||||||
if (result.fileCount === 0) {
|
if (result.fileCount === 0) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,498 +0,0 @@
|
|||||||
import HighlightText from '@renderer/components/HighlightText'
|
|
||||||
import {
|
|
||||||
useNotesActions,
|
|
||||||
useNotesDrag,
|
|
||||||
useNotesEditing,
|
|
||||||
useNotesSearch,
|
|
||||||
useNotesSelection,
|
|
||||||
useNotesUI
|
|
||||||
} from '@renderer/pages/notes/context/NotesContexts'
|
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import type { SearchMatch, SearchResult } from '@renderer/services/NotesSearchService'
|
|
||||||
import type { NotesTreeNode } from '@renderer/types/note'
|
|
||||||
import { Dropdown } from 'antd'
|
|
||||||
import { ChevronDown, ChevronRight, File, FilePlus, Folder, FolderOpen } from 'lucide-react'
|
|
||||||
import { memo, useCallback, useMemo, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
interface TreeNodeProps {
|
|
||||||
node: NotesTreeNode | SearchResult
|
|
||||||
depth: number
|
|
||||||
renderChildren?: boolean
|
|
||||||
onHintClick?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const TreeNode = memo<TreeNodeProps>(({ node, depth, renderChildren = true, onHintClick }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
// Use split contexts - only subscribe to what this node needs
|
|
||||||
const { selectedFolderId, activeNodeId } = useNotesSelection()
|
|
||||||
const { editingNodeId, renamingNodeIds, newlyRenamedNodeIds, inPlaceEdit } = useNotesEditing()
|
|
||||||
const { draggedNodeId, dragOverNodeId, dragPosition, onDragStart, onDragOver, onDragLeave, onDrop, onDragEnd } =
|
|
||||||
useNotesDrag()
|
|
||||||
const { searchKeyword, showMatches } = useNotesSearch()
|
|
||||||
const { openDropdownKey } = useNotesUI()
|
|
||||||
const { getMenuItems, onSelectNode, onToggleExpanded, onDropdownOpenChange } = useNotesActions()
|
|
||||||
|
|
||||||
const [showAllMatches, setShowAllMatches] = useState(false)
|
|
||||||
const { isEditing: isInputEditing, inputProps } = inPlaceEdit
|
|
||||||
|
|
||||||
// 检查是否是 hint 节点
|
|
||||||
const isHintNode = node.type === 'hint'
|
|
||||||
|
|
||||||
// 检查是否是搜索结果
|
|
||||||
const searchResult = 'matchType' in node ? (node as SearchResult) : null
|
|
||||||
const hasMatches = searchResult && searchResult.matches && searchResult.matches.length > 0
|
|
||||||
|
|
||||||
// 处理匹配项点击
|
|
||||||
const handleMatchClick = useCallback(
|
|
||||||
(match: SearchMatch) => {
|
|
||||||
// 发送定位事件
|
|
||||||
EventEmitter.emit(EVENT_NAMES.LOCATE_NOTE_LINE, {
|
|
||||||
noteId: node.id,
|
|
||||||
lineNumber: match.lineNumber,
|
|
||||||
lineContent: match.lineContent
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[node]
|
|
||||||
)
|
|
||||||
|
|
||||||
const isActive = selectedFolderId ? node.type === 'folder' && node.id === selectedFolderId : node.id === activeNodeId
|
|
||||||
const isEditing = editingNodeId === node.id && isInputEditing
|
|
||||||
const isRenaming = renamingNodeIds.has(node.id)
|
|
||||||
const isNewlyRenamed = newlyRenamedNodeIds.has(node.id)
|
|
||||||
const hasChildren = node.children && node.children.length > 0
|
|
||||||
const isDragging = draggedNodeId === node.id
|
|
||||||
const isDragOver = dragOverNodeId === node.id
|
|
||||||
const isDragBefore = isDragOver && dragPosition === 'before'
|
|
||||||
const isDragInside = isDragOver && dragPosition === 'inside'
|
|
||||||
const isDragAfter = isDragOver && dragPosition === 'after'
|
|
||||||
|
|
||||||
const getNodeNameClassName = () => {
|
|
||||||
if (isRenaming) return 'shimmer'
|
|
||||||
if (isNewlyRenamed) return 'typing'
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayName = useMemo(() => {
|
|
||||||
if (!searchKeyword) {
|
|
||||||
return node.name
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = node.name ?? ''
|
|
||||||
if (!name) {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
const keyword = searchKeyword
|
|
||||||
const nameLower = name.toLowerCase()
|
|
||||||
const keywordLower = keyword.toLowerCase()
|
|
||||||
const matchStart = nameLower.indexOf(keywordLower)
|
|
||||||
|
|
||||||
if (matchStart === -1) {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchEnd = matchStart + keyword.length
|
|
||||||
const beforeMatch = Math.min(2, matchStart)
|
|
||||||
const contextStart = matchStart - beforeMatch
|
|
||||||
const contextLength = 50
|
|
||||||
const contextEnd = Math.min(name.length, matchEnd + contextLength)
|
|
||||||
|
|
||||||
const prefix = contextStart > 0 ? '...' : ''
|
|
||||||
const suffix = contextEnd < name.length ? '...' : ''
|
|
||||||
|
|
||||||
return prefix + name.substring(contextStart, contextEnd) + suffix
|
|
||||||
}, [node.name, searchKeyword])
|
|
||||||
|
|
||||||
// Special render for hint nodes
|
|
||||||
if (isHintNode) {
|
|
||||||
return (
|
|
||||||
<div key={node.id}>
|
|
||||||
<TreeNodeContainer active={false} depth={depth}>
|
|
||||||
<TreeNodeContent>
|
|
||||||
<NodeIcon>
|
|
||||||
<FilePlus size={16} />
|
|
||||||
</NodeIcon>
|
|
||||||
<DropHintText onClick={onHintClick}>{t('notes.drop_markdown_hint')}</DropHintText>
|
|
||||||
</TreeNodeContent>
|
|
||||||
</TreeNodeContainer>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={node.id}>
|
|
||||||
<Dropdown
|
|
||||||
menu={{ items: getMenuItems(node as NotesTreeNode) }}
|
|
||||||
trigger={['contextMenu']}
|
|
||||||
open={openDropdownKey === node.id}
|
|
||||||
onOpenChange={(open) => onDropdownOpenChange(open ? node.id : null)}>
|
|
||||||
<div onContextMenu={(e) => e.stopPropagation()}>
|
|
||||||
<TreeNodeContainer
|
|
||||||
active={isActive}
|
|
||||||
depth={depth}
|
|
||||||
isDragging={isDragging}
|
|
||||||
isDragOver={isDragOver}
|
|
||||||
isDragBefore={isDragBefore}
|
|
||||||
isDragInside={isDragInside}
|
|
||||||
isDragAfter={isDragAfter}
|
|
||||||
draggable={!isEditing}
|
|
||||||
data-node-id={node.id}
|
|
||||||
onDragStart={(e) => onDragStart(e, node as NotesTreeNode)}
|
|
||||||
onDragOver={(e) => onDragOver(e, node as NotesTreeNode)}
|
|
||||||
onDragLeave={onDragLeave}
|
|
||||||
onDrop={(e) => onDrop(e, node as NotesTreeNode)}
|
|
||||||
onDragEnd={onDragEnd}>
|
|
||||||
<TreeNodeContent onClick={() => onSelectNode(node as NotesTreeNode)}>
|
|
||||||
<NodeIndent depth={depth} />
|
|
||||||
|
|
||||||
{node.type === 'folder' && (
|
|
||||||
<ExpandIcon
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
onToggleExpanded(node.id)
|
|
||||||
}}
|
|
||||||
title={node.expanded ? t('notes.collapse') : t('notes.expand')}>
|
|
||||||
{node.expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
|
||||||
</ExpandIcon>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<NodeIcon>
|
|
||||||
{node.type === 'folder' ? (
|
|
||||||
node.expanded ? (
|
|
||||||
<FolderOpen size={16} />
|
|
||||||
) : (
|
|
||||||
<Folder size={16} />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<File size={16} />
|
|
||||||
)}
|
|
||||||
</NodeIcon>
|
|
||||||
|
|
||||||
{isEditing ? (
|
|
||||||
<EditInput {...inputProps} onClick={(e) => e.stopPropagation()} autoFocus />
|
|
||||||
) : (
|
|
||||||
<NodeNameContainer>
|
|
||||||
<NodeName className={getNodeNameClassName()}>
|
|
||||||
{searchKeyword ? <HighlightText text={displayName} keyword={searchKeyword} /> : node.name}
|
|
||||||
</NodeName>
|
|
||||||
{searchResult && searchResult.matchType && searchResult.matchType !== 'filename' && (
|
|
||||||
<MatchBadge matchType={searchResult.matchType}>
|
|
||||||
{searchResult.matchType === 'both' ? t('notes.search.both') : t('notes.search.content')}
|
|
||||||
</MatchBadge>
|
|
||||||
)}
|
|
||||||
</NodeNameContainer>
|
|
||||||
)}
|
|
||||||
</TreeNodeContent>
|
|
||||||
</TreeNodeContainer>
|
|
||||||
</div>
|
|
||||||
</Dropdown>
|
|
||||||
|
|
||||||
{showMatches && hasMatches && (
|
|
||||||
<SearchMatchesContainer depth={depth}>
|
|
||||||
{(showAllMatches ? searchResult!.matches! : searchResult!.matches!.slice(0, 3)).map((match, idx) => (
|
|
||||||
<MatchItem key={idx} onClick={() => handleMatchClick(match)}>
|
|
||||||
<MatchLineNumber>{match.lineNumber}</MatchLineNumber>
|
|
||||||
<MatchContext>
|
|
||||||
<HighlightText text={match.context} keyword={searchKeyword} />
|
|
||||||
</MatchContext>
|
|
||||||
</MatchItem>
|
|
||||||
))}
|
|
||||||
{searchResult!.matches!.length > 3 && (
|
|
||||||
<MoreMatches
|
|
||||||
depth={depth}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setShowAllMatches(!showAllMatches)
|
|
||||||
}}>
|
|
||||||
{showAllMatches ? (
|
|
||||||
<>
|
|
||||||
<ChevronDown size={12} style={{ marginRight: 4 }} />
|
|
||||||
{t('notes.search.show_less')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ChevronRight size={12} style={{ marginRight: 4 }} />+{searchResult!.matches!.length - 3}{' '}
|
|
||||||
{t('notes.search.more_matches')}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</MoreMatches>
|
|
||||||
)}
|
|
||||||
</SearchMatchesContainer>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{renderChildren && node.type === 'folder' && node.expanded && hasChildren && (
|
|
||||||
<div>
|
|
||||||
{node.children!.map((child) => (
|
|
||||||
<TreeNode key={child.id} node={child} depth={depth + 1} renderChildren={renderChildren} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
export const TreeNodeContainer = styled.div<{
|
|
||||||
active: boolean
|
|
||||||
depth: number
|
|
||||||
isDragging?: boolean
|
|
||||||
isDragOver?: boolean
|
|
||||||
isDragBefore?: boolean
|
|
||||||
isDragInside?: boolean
|
|
||||||
isDragAfter?: boolean
|
|
||||||
}>`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 4px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
/* CRITICAL: Must have fully opaque background for sticky to work properly */
|
|
||||||
/* Transparent/semi-transparent backgrounds will show content bleeding through when sticky */
|
|
||||||
background-color: ${(props) => {
|
|
||||||
if (props.isDragInside) return 'var(--color-primary-background)'
|
|
||||||
// Use hover color for active state - it's guaranteed to be opaque
|
|
||||||
if (props.active) return 'var(--color-hover, var(--color-background-mute))'
|
|
||||||
return 'var(--color-background)'
|
|
||||||
}};
|
|
||||||
border: 0.5px solid
|
|
||||||
${(props) => {
|
|
||||||
if (props.isDragInside) return 'var(--color-primary)'
|
|
||||||
if (props.active) return 'var(--color-border)'
|
|
||||||
return 'transparent'
|
|
||||||
}};
|
|
||||||
opacity: ${(props) => (props.isDragging ? 0.5 : 1)};
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
|
|
||||||
.node-actions {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 添加拖拽指示线 */
|
|
||||||
${(props) =>
|
|
||||||
props.isDragBefore &&
|
|
||||||
`
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -2px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
|
|
||||||
${(props) =>
|
|
||||||
props.isDragAfter &&
|
|
||||||
`
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: -2px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 2px;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
border-radius: 1px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const TreeNodeContent = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const NodeIndent = styled.div<{ depth: number }>`
|
|
||||||
width: ${(props) => props.depth * 16}px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const ExpandIcon = styled.div`
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
margin-right: 4px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const NodeIcon = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-right: 8px;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
flex-shrink: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const NodeName = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--color-text);
|
|
||||||
position: relative;
|
|
||||||
will-change: background-position, width;
|
|
||||||
|
|
||||||
--color-shimmer-mid: var(--color-text-1);
|
|
||||||
--color-shimmer-end: color-mix(in srgb, var(--color-text-1) 25%, transparent);
|
|
||||||
|
|
||||||
&.shimmer {
|
|
||||||
background: linear-gradient(to left, var(--color-shimmer-end), var(--color-shimmer-mid), var(--color-shimmer-end));
|
|
||||||
background-size: 200% 100%;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
animation: shimmer 3s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.typing {
|
|
||||||
display: block;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
animation: typewriter 0.5s steps(40, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes typewriter {
|
|
||||||
from {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const SearchMatchesContainer = styled.div<{ depth: number }>`
|
|
||||||
margin-left: ${(props) => props.depth * 16 + 40}px;
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
background-color: var(--color-background-mute);
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 2px solid var(--color-primary-soft);
|
|
||||||
`
|
|
||||||
|
|
||||||
export const NodeNameContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const MatchBadge = styled.span<{ matchType: string }>`
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 4px;
|
|
||||||
height: 16px;
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 1;
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: ${(props) =>
|
|
||||||
props.matchType === 'both' ? 'var(--color-primary-soft)' : 'var(--color-background-mute)'};
|
|
||||||
color: ${(props) => (props.matchType === 'both' ? 'var(--color-primary)' : 'var(--color-text-3)')};
|
|
||||||
font-weight: 500;
|
|
||||||
flex-shrink: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const MatchItem = styled.div`
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-right: -6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
transform: translateX(2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: var(--color-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export const MatchLineNumber = styled.span`
|
|
||||||
color: var(--color-text-3);
|
|
||||||
font-family: monospace;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 30px;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const MatchContext = styled.div`
|
|
||||||
color: var(--color-text-2);
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
font-family: monospace;
|
|
||||||
`
|
|
||||||
|
|
||||||
export const MoreMatches = styled.div<{ depth: number }>`
|
|
||||||
margin-top: 4px;
|
|
||||||
padding: 4px 6px;
|
|
||||||
margin-left: -6px;
|
|
||||||
margin-right: -6px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-text-2);
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const EditInput = styled.input`
|
|
||||||
flex: 1;
|
|
||||||
font-size: 13px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const DropHintText = styled.div`
|
|
||||||
color: var(--color-text-3);
|
|
||||||
font-size: 12px;
|
|
||||||
font-style: italic;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default TreeNode
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import type { UseInPlaceEditReturn } from '@renderer/hooks/useInPlaceEdit'
|
|
||||||
import type { NotesTreeNode } from '@renderer/types/note'
|
|
||||||
import type { MenuProps } from 'antd'
|
|
||||||
import { createContext, use } from 'react'
|
|
||||||
|
|
||||||
// ==================== 1. Actions Context (Static, rarely changes) ====================
|
|
||||||
export interface NotesActionsContextType {
|
|
||||||
getMenuItems: (node: NotesTreeNode) => MenuProps['items']
|
|
||||||
onSelectNode: (node: NotesTreeNode) => void
|
|
||||||
onToggleExpanded: (nodeId: string) => void
|
|
||||||
onDropdownOpenChange: (key: string | null) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotesActionsContext = createContext<NotesActionsContextType | null>(null)
|
|
||||||
|
|
||||||
export const useNotesActions = () => {
|
|
||||||
const context = use(NotesActionsContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useNotesActions must be used within NotesActionsContext.Provider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 2. Selection Context (Low frequency updates) ====================
|
|
||||||
export interface NotesSelectionContextType {
|
|
||||||
selectedFolderId?: string | null
|
|
||||||
activeNodeId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotesSelectionContext = createContext<NotesSelectionContextType | null>(null)
|
|
||||||
|
|
||||||
export const useNotesSelection = () => {
|
|
||||||
const context = use(NotesSelectionContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useNotesSelection must be used within NotesSelectionContext.Provider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 3. Editing Context (Medium frequency updates) ====================
|
|
||||||
export interface NotesEditingContextType {
|
|
||||||
editingNodeId: string | null
|
|
||||||
renamingNodeIds: Set<string>
|
|
||||||
newlyRenamedNodeIds: Set<string>
|
|
||||||
inPlaceEdit: UseInPlaceEditReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotesEditingContext = createContext<NotesEditingContextType | null>(null)
|
|
||||||
|
|
||||||
export const useNotesEditing = () => {
|
|
||||||
const context = use(NotesEditingContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useNotesEditing must be used within NotesEditingContext.Provider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 4. Drag Context (High frequency updates) ====================
|
|
||||||
export interface NotesDragContextType {
|
|
||||||
draggedNodeId: string | null
|
|
||||||
dragOverNodeId: string | null
|
|
||||||
dragPosition: 'before' | 'inside' | 'after'
|
|
||||||
onDragStart: (e: React.DragEvent, node: NotesTreeNode) => void
|
|
||||||
onDragOver: (e: React.DragEvent, node: NotesTreeNode) => void
|
|
||||||
onDragLeave: () => void
|
|
||||||
onDrop: (e: React.DragEvent, node: NotesTreeNode) => void
|
|
||||||
onDragEnd: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotesDragContext = createContext<NotesDragContextType | null>(null)
|
|
||||||
|
|
||||||
export const useNotesDrag = () => {
|
|
||||||
const context = use(NotesDragContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useNotesDrag must be used within NotesDragContext.Provider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 5. Search Context (Medium frequency updates) ====================
|
|
||||||
export interface NotesSearchContextType {
|
|
||||||
searchKeyword: string
|
|
||||||
showMatches: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotesSearchContext = createContext<NotesSearchContextType | null>(null)
|
|
||||||
|
|
||||||
export const useNotesSearch = () => {
|
|
||||||
const context = use(NotesSearchContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useNotesSearch must be used within NotesSearchContext.Provider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== 6. UI Context (Medium frequency updates) ====================
|
|
||||||
export interface NotesUIContextType {
|
|
||||||
openDropdownKey: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotesUIContext = createContext<NotesUIContextType | null>(null)
|
|
||||||
|
|
||||||
export const useNotesUI = () => {
|
|
||||||
const context = use(NotesUIContext)
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useNotesUI must be used within NotesUIContext.Provider')
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import type { NotesTreeNode } from '@renderer/types/note'
|
|
||||||
import { useCallback, useRef, useState } from 'react'
|
|
||||||
|
|
||||||
interface UseNotesDragAndDropProps {
|
|
||||||
onMoveNode: (sourceNodeId: string, targetNodeId: string, position: 'before' | 'after' | 'inside') => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNotesDragAndDrop = ({ onMoveNode }: UseNotesDragAndDropProps) => {
|
|
||||||
const [draggedNodeId, setDraggedNodeId] = useState<string | null>(null)
|
|
||||||
const [dragOverNodeId, setDragOverNodeId] = useState<string | null>(null)
|
|
||||||
const [dragPosition, setDragPosition] = useState<'before' | 'inside' | 'after'>('inside')
|
|
||||||
const dragNodeRef = useRef<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: React.DragEvent, node: NotesTreeNode) => {
|
|
||||||
setDraggedNodeId(node.id)
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
|
||||||
e.dataTransfer.setData('text/plain', node.id)
|
|
||||||
|
|
||||||
dragNodeRef.current = e.currentTarget as HTMLDivElement
|
|
||||||
|
|
||||||
// Create ghost element
|
|
||||||
if (e.currentTarget.parentElement) {
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
|
||||||
const ghostElement = e.currentTarget.cloneNode(true) as HTMLElement
|
|
||||||
ghostElement.style.width = `${rect.width}px`
|
|
||||||
ghostElement.style.opacity = '0.7'
|
|
||||||
ghostElement.style.position = 'absolute'
|
|
||||||
ghostElement.style.top = '-1000px'
|
|
||||||
document.body.appendChild(ghostElement)
|
|
||||||
e.dataTransfer.setDragImage(ghostElement, 10, 10)
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.removeChild(ghostElement)
|
|
||||||
}, 0)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDragOver = useCallback(
|
|
||||||
(e: React.DragEvent, node: NotesTreeNode) => {
|
|
||||||
e.preventDefault()
|
|
||||||
e.dataTransfer.dropEffect = 'move'
|
|
||||||
|
|
||||||
if (draggedNodeId === node.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setDragOverNodeId(node.id)
|
|
||||||
|
|
||||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
|
|
||||||
const mouseY = e.clientY
|
|
||||||
const thresholdTop = rect.top + rect.height * 0.3
|
|
||||||
const thresholdBottom = rect.bottom - rect.height * 0.3
|
|
||||||
|
|
||||||
if (mouseY < thresholdTop) {
|
|
||||||
setDragPosition('before')
|
|
||||||
} else if (mouseY > thresholdBottom) {
|
|
||||||
setDragPosition('after')
|
|
||||||
} else {
|
|
||||||
setDragPosition(node.type === 'folder' ? 'inside' : 'after')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[draggedNodeId]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDragLeave = useCallback(() => {
|
|
||||||
setDragOverNodeId(null)
|
|
||||||
setDragPosition('inside')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
|
||||||
(e: React.DragEvent, targetNode: NotesTreeNode) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const draggedId = e.dataTransfer.getData('text/plain')
|
|
||||||
|
|
||||||
if (draggedId && draggedId !== targetNode.id) {
|
|
||||||
onMoveNode(draggedId, targetNode.id, dragPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
setDraggedNodeId(null)
|
|
||||||
setDragOverNodeId(null)
|
|
||||||
setDragPosition('inside')
|
|
||||||
},
|
|
||||||
[onMoveNode, dragPosition]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDragEnd = useCallback(() => {
|
|
||||||
setDraggedNodeId(null)
|
|
||||||
setDragOverNodeId(null)
|
|
||||||
setDragPosition('inside')
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return {
|
|
||||||
draggedNodeId,
|
|
||||||
dragOverNodeId,
|
|
||||||
dragPosition,
|
|
||||||
handleDragStart,
|
|
||||||
handleDragOver,
|
|
||||||
handleDragLeave,
|
|
||||||
handleDrop,
|
|
||||||
handleDragEnd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { loggerService } from '@logger'
|
|
||||||
import { useInPlaceEdit } from '@renderer/hooks/useInPlaceEdit'
|
|
||||||
import { fetchNoteSummary } from '@renderer/services/ApiService'
|
|
||||||
import type { NotesTreeNode } from '@renderer/types/note'
|
|
||||||
import { useCallback, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
|
|
||||||
const logger = loggerService.withContext('UseNotesEditing')
|
|
||||||
|
|
||||||
interface UseNotesEditingProps {
|
|
||||||
onRenameNode: (nodeId: string, newName: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNotesEditing = ({ onRenameNode }: UseNotesEditingProps) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [editingNodeId, setEditingNodeId] = useState<string | null>(null)
|
|
||||||
const [renamingNodeIds, setRenamingNodeIds] = useState<Set<string>>(new Set())
|
|
||||||
const [newlyRenamedNodeIds, setNewlyRenamedNodeIds] = useState<Set<string>>(new Set())
|
|
||||||
|
|
||||||
const inPlaceEdit = useInPlaceEdit({
|
|
||||||
onSave: (newName: string) => {
|
|
||||||
if (editingNodeId && newName) {
|
|
||||||
onRenameNode(editingNodeId, newName)
|
|
||||||
window.toast.success(t('common.saved'))
|
|
||||||
logger.debug(`Renamed node ${editingNodeId} to "${newName}"`)
|
|
||||||
}
|
|
||||||
setEditingNodeId(null)
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
setEditingNodeId(null)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleStartEdit = useCallback(
|
|
||||||
(node: NotesTreeNode) => {
|
|
||||||
setEditingNodeId(node.id)
|
|
||||||
inPlaceEdit.startEdit(node.name)
|
|
||||||
},
|
|
||||||
[inPlaceEdit]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleAutoRename = useCallback(
|
|
||||||
async (note: NotesTreeNode) => {
|
|
||||||
if (note.type !== 'file') return
|
|
||||||
|
|
||||||
setRenamingNodeIds((prev) => new Set(prev).add(note.id))
|
|
||||||
try {
|
|
||||||
const content = await window.api.file.readExternal(note.externalPath)
|
|
||||||
if (!content || content.trim().length === 0) {
|
|
||||||
window.toast.warning(t('notes.auto_rename.empty_note'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const summaryText = await fetchNoteSummary({ content })
|
|
||||||
if (summaryText) {
|
|
||||||
onRenameNode(note.id, summaryText)
|
|
||||||
window.toast.success(t('notes.auto_rename.success'))
|
|
||||||
} else {
|
|
||||||
window.toast.error(t('notes.auto_rename.failed'))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
window.toast.error(t('notes.auto_rename.failed'))
|
|
||||||
logger.error(`Failed to auto-rename note: ${error}`)
|
|
||||||
} finally {
|
|
||||||
setRenamingNodeIds((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.delete(note.id)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
|
|
||||||
setNewlyRenamedNodeIds((prev) => new Set(prev).add(note.id))
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
setNewlyRenamedNodeIds((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.delete(note.id)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}, 700)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onRenameNode, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
editingNodeId,
|
|
||||||
renamingNodeIds,
|
|
||||||
newlyRenamedNodeIds,
|
|
||||||
inPlaceEdit,
|
|
||||||
handleStartEdit,
|
|
||||||
handleAutoRename,
|
|
||||||
setEditingNodeId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { useCallback } from 'react'
|
|
||||||
|
|
||||||
interface UseNotesFileUploadProps {
|
|
||||||
onUploadFiles: (files: File[]) => void
|
|
||||||
setIsDragOverSidebar: (isDragOver: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNotesFileUpload = ({ onUploadFiles, setIsDragOverSidebar }: UseNotesFileUploadProps) => {
|
|
||||||
const handleDropFiles = useCallback(
|
|
||||||
async (e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setIsDragOverSidebar(false)
|
|
||||||
|
|
||||||
// 处理文件夹拖拽:从 dataTransfer.items 获取完整文件路径信息
|
|
||||||
const items = Array.from(e.dataTransfer.items)
|
|
||||||
const files: File[] = []
|
|
||||||
|
|
||||||
const processEntry = async (entry: FileSystemEntry, path: string = '') => {
|
|
||||||
if (entry.isFile) {
|
|
||||||
const fileEntry = entry as FileSystemFileEntry
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
fileEntry.file((file) => {
|
|
||||||
// 手动设置 webkitRelativePath 以保持文件夹结构
|
|
||||||
Object.defineProperty(file, 'webkitRelativePath', {
|
|
||||||
value: path + file.name,
|
|
||||||
writable: false
|
|
||||||
})
|
|
||||||
files.push(file)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else if (entry.isDirectory) {
|
|
||||||
const dirEntry = entry as FileSystemDirectoryEntry
|
|
||||||
const reader = dirEntry.createReader()
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
reader.readEntries(async (entries) => {
|
|
||||||
const promises = entries.map((subEntry) => processEntry(subEntry, path + entry.name + '/'))
|
|
||||||
await Promise.all(promises)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果支持 DataTransferItem API(文件夹拖拽)
|
|
||||||
if (items.length > 0 && items[0].webkitGetAsEntry()) {
|
|
||||||
const promises = items.map((item) => {
|
|
||||||
const entry = item.webkitGetAsEntry()
|
|
||||||
return entry ? processEntry(entry) : Promise.resolve()
|
|
||||||
})
|
|
||||||
|
|
||||||
await Promise.all(promises)
|
|
||||||
|
|
||||||
if (files.length > 0) {
|
|
||||||
onUploadFiles(files)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const regularFiles = Array.from(e.dataTransfer.files)
|
|
||||||
if (regularFiles.length > 0) {
|
|
||||||
onUploadFiles(regularFiles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onUploadFiles, setIsDragOverSidebar]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSelectFiles = useCallback(() => {
|
|
||||||
const fileInput = document.createElement('input')
|
|
||||||
fileInput.type = 'file'
|
|
||||||
fileInput.multiple = true
|
|
||||||
fileInput.accept = '.md,.markdown'
|
|
||||||
fileInput.webkitdirectory = false
|
|
||||||
|
|
||||||
fileInput.onchange = (e) => {
|
|
||||||
const target = e.target as HTMLInputElement
|
|
||||||
if (target.files && target.files.length > 0) {
|
|
||||||
const selectedFiles = Array.from(target.files)
|
|
||||||
onUploadFiles(selectedFiles)
|
|
||||||
}
|
|
||||||
fileInput.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInput.click()
|
|
||||||
}, [onUploadFiles])
|
|
||||||
|
|
||||||
const handleSelectFolder = useCallback(() => {
|
|
||||||
const folderInput = document.createElement('input')
|
|
||||||
folderInput.type = 'file'
|
|
||||||
// @ts-ignore - webkitdirectory is a non-standard attribute
|
|
||||||
folderInput.webkitdirectory = true
|
|
||||||
// @ts-ignore - directory is a non-standard attribute
|
|
||||||
folderInput.directory = true
|
|
||||||
folderInput.multiple = true
|
|
||||||
|
|
||||||
folderInput.onchange = (e) => {
|
|
||||||
const target = e.target as HTMLInputElement
|
|
||||||
if (target.files && target.files.length > 0) {
|
|
||||||
const selectedFiles = Array.from(target.files)
|
|
||||||
onUploadFiles(selectedFiles)
|
|
||||||
}
|
|
||||||
folderInput.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
folderInput.click()
|
|
||||||
}, [onUploadFiles])
|
|
||||||
|
|
||||||
return {
|
|
||||||
handleDropFiles,
|
|
||||||
handleSelectFiles,
|
|
||||||
handleSelectFolder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
import { loggerService } from '@logger'
|
|
||||||
import { DeleteIcon } from '@renderer/components/Icons'
|
|
||||||
import SaveToKnowledgePopup from '@renderer/components/Popups/SaveToKnowledgePopup'
|
|
||||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
|
||||||
import type { RootState } from '@renderer/store'
|
|
||||||
import type { NotesTreeNode } from '@renderer/types/note'
|
|
||||||
import { exportNote } from '@renderer/utils/export'
|
|
||||||
import type { MenuProps } from 'antd'
|
|
||||||
import type { ItemType, MenuItemType } from 'antd/es/menu/interface'
|
|
||||||
import { Edit3, FilePlus, FileSearch, Folder, FolderOpen, Sparkles, Star, StarOff, UploadIcon } from 'lucide-react'
|
|
||||||
import { useCallback } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
|
|
||||||
const logger = loggerService.withContext('UseNotesMenu')
|
|
||||||
|
|
||||||
interface UseNotesMenuProps {
|
|
||||||
renamingNodeIds: Set<string>
|
|
||||||
onCreateNote: (name: string, targetFolderId?: string) => void
|
|
||||||
onCreateFolder: (name: string, targetFolderId?: string) => void
|
|
||||||
onRenameNode: (nodeId: string, newName: string) => void
|
|
||||||
onToggleStar: (nodeId: string) => void
|
|
||||||
onDeleteNode: (nodeId: string) => void
|
|
||||||
onSelectNode: (node: NotesTreeNode) => void
|
|
||||||
handleStartEdit: (node: NotesTreeNode) => void
|
|
||||||
handleAutoRename: (node: NotesTreeNode) => void
|
|
||||||
activeNode?: NotesTreeNode | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNotesMenu = ({
|
|
||||||
renamingNodeIds,
|
|
||||||
onCreateNote,
|
|
||||||
onCreateFolder,
|
|
||||||
onToggleStar,
|
|
||||||
onDeleteNode,
|
|
||||||
onSelectNode,
|
|
||||||
handleStartEdit,
|
|
||||||
handleAutoRename,
|
|
||||||
activeNode
|
|
||||||
}: UseNotesMenuProps) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { bases } = useKnowledgeBases()
|
|
||||||
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions)
|
|
||||||
|
|
||||||
const handleExportKnowledge = useCallback(
|
|
||||||
async (note: NotesTreeNode) => {
|
|
||||||
try {
|
|
||||||
if (bases.length === 0) {
|
|
||||||
window.toast.warning(t('chat.save.knowledge.empty.no_knowledge_base'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await SaveToKnowledgePopup.showForNote(note)
|
|
||||||
|
|
||||||
if (result?.success) {
|
|
||||||
window.toast.success(t('notes.export_success', { count: result.savedCount }))
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
window.toast.error(t('notes.export_failed'))
|
|
||||||
logger.error(`Failed to export note to knowledge base: ${error}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[bases.length, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleImageAction = useCallback(
|
|
||||||
async (node: NotesTreeNode, platform: 'copyImage' | 'exportImage') => {
|
|
||||||
try {
|
|
||||||
if (activeNode?.id !== node.id) {
|
|
||||||
onSelectNode(node)
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
}
|
|
||||||
|
|
||||||
await exportNote({ node, platform })
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Failed to ${platform === 'copyImage' ? 'copy' : 'export'} as image:`, error as Error)
|
|
||||||
window.toast.error(t('common.copy_failed'))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[activeNode, onSelectNode, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleDeleteNodeWrapper = useCallback(
|
|
||||||
(node: NotesTreeNode) => {
|
|
||||||
const confirmText =
|
|
||||||
node.type === 'folder'
|
|
||||||
? t('notes.delete_folder_confirm', { name: node.name })
|
|
||||||
: t('notes.delete_note_confirm', { name: node.name })
|
|
||||||
|
|
||||||
window.modal.confirm({
|
|
||||||
title: t('notes.delete'),
|
|
||||||
content: confirmText,
|
|
||||||
centered: true,
|
|
||||||
okButtonProps: { danger: true },
|
|
||||||
onOk: () => {
|
|
||||||
onDeleteNode(node.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[onDeleteNode, t]
|
|
||||||
)
|
|
||||||
|
|
||||||
const getMenuItems = useCallback(
|
|
||||||
(node: NotesTreeNode) => {
|
|
||||||
const baseMenuItems: MenuProps['items'] = []
|
|
||||||
|
|
||||||
// only show auto rename for file for now
|
|
||||||
if (node.type !== 'folder') {
|
|
||||||
baseMenuItems.push({
|
|
||||||
label: t('notes.auto_rename.label'),
|
|
||||||
key: 'auto-rename',
|
|
||||||
icon: <Sparkles size={14} />,
|
|
||||||
disabled: renamingNodeIds.has(node.id),
|
|
||||||
onClick: () => {
|
|
||||||
handleAutoRename(node)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.type === 'folder') {
|
|
||||||
baseMenuItems.push(
|
|
||||||
{
|
|
||||||
label: t('notes.new_note'),
|
|
||||||
key: 'new_note',
|
|
||||||
icon: <FilePlus size={14} />,
|
|
||||||
onClick: () => {
|
|
||||||
onCreateNote(t('notes.untitled_note'), node.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('notes.new_folder'),
|
|
||||||
key: 'new_folder',
|
|
||||||
icon: <Folder size={14} />,
|
|
||||||
onClick: () => {
|
|
||||||
onCreateFolder(t('notes.untitled_folder'), node.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 'divider' }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseMenuItems.push(
|
|
||||||
{
|
|
||||||
label: t('notes.rename'),
|
|
||||||
key: 'rename',
|
|
||||||
icon: <Edit3 size={14} />,
|
|
||||||
onClick: () => {
|
|
||||||
handleStartEdit(node)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('notes.open_outside'),
|
|
||||||
key: 'open_outside',
|
|
||||||
icon: <FolderOpen size={14} />,
|
|
||||||
onClick: () => {
|
|
||||||
window.api.openPath(node.externalPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (node.type !== 'folder') {
|
|
||||||
baseMenuItems.push(
|
|
||||||
{
|
|
||||||
label: node.isStarred ? t('notes.unstar') : t('notes.star'),
|
|
||||||
key: 'star',
|
|
||||||
icon: node.isStarred ? <StarOff size={14} /> : <Star size={14} />,
|
|
||||||
onClick: () => {
|
|
||||||
onToggleStar(node.id)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('notes.export_knowledge'),
|
|
||||||
key: 'export_knowledge',
|
|
||||||
icon: <FileSearch size={14} />,
|
|
||||||
onClick: () => {
|
|
||||||
handleExportKnowledge(node)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('chat.topics.export.title'),
|
|
||||||
key: 'export',
|
|
||||||
icon: <UploadIcon size={14} />,
|
|
||||||
children: [
|
|
||||||
exportMenuOptions.image && {
|
|
||||||
label: t('chat.topics.copy.image'),
|
|
||||||
key: 'copy-image',
|
|
||||||
onClick: () => handleImageAction(node, 'copyImage')
|
|
||||||
},
|
|
||||||
exportMenuOptions.image && {
|
|
||||||
label: t('chat.topics.export.image'),
|
|
||||||
key: 'export-image',
|
|
||||||
onClick: () => handleImageAction(node, 'exportImage')
|
|
||||||
},
|
|
||||||
exportMenuOptions.markdown && {
|
|
||||||
label: t('chat.topics.export.md.label'),
|
|
||||||
key: 'markdown',
|
|
||||||
onClick: () => exportNote({ node, platform: 'markdown' })
|
|
||||||
},
|
|
||||||
exportMenuOptions.docx && {
|
|
||||||
label: t('chat.topics.export.word'),
|
|
||||||
key: 'word',
|
|
||||||
onClick: () => exportNote({ node, platform: 'docx' })
|
|
||||||
},
|
|
||||||
exportMenuOptions.notion && {
|
|
||||||
label: t('chat.topics.export.notion'),
|
|
||||||
key: 'notion',
|
|
||||||
onClick: () => exportNote({ node, platform: 'notion' })
|
|
||||||
},
|
|
||||||
exportMenuOptions.yuque && {
|
|
||||||
label: t('chat.topics.export.yuque'),
|
|
||||||
key: 'yuque',
|
|
||||||
onClick: () => exportNote({ node, platform: 'yuque' })
|
|
||||||
},
|
|
||||||
exportMenuOptions.obsidian && {
|
|
||||||
label: t('chat.topics.export.obsidian'),
|
|
||||||
key: 'obsidian',
|
|
||||||
onClick: () => exportNote({ node, platform: 'obsidian' })
|
|
||||||
},
|
|
||||||
exportMenuOptions.joplin && {
|
|
||||||
label: t('chat.topics.export.joplin'),
|
|
||||||
key: 'joplin',
|
|
||||||
onClick: () => exportNote({ node, platform: 'joplin' })
|
|
||||||
},
|
|
||||||
exportMenuOptions.siyuan && {
|
|
||||||
label: t('chat.topics.export.siyuan'),
|
|
||||||
key: 'siyuan',
|
|
||||||
onClick: () => exportNote({ node, platform: 'siyuan' })
|
|
||||||
}
|
|
||||||
].filter(Boolean) as ItemType<MenuItemType>[]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
baseMenuItems.push(
|
|
||||||
{ type: 'divider' },
|
|
||||||
{
|
|
||||||
label: t('notes.delete'),
|
|
||||||
danger: true,
|
|
||||||
key: 'delete',
|
|
||||||
icon: <DeleteIcon size={14} className="lucide-custom" />,
|
|
||||||
onClick: () => {
|
|
||||||
handleDeleteNodeWrapper(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return baseMenuItems
|
|
||||||
},
|
|
||||||
[
|
|
||||||
t,
|
|
||||||
handleStartEdit,
|
|
||||||
onToggleStar,
|
|
||||||
handleExportKnowledge,
|
|
||||||
handleImageAction,
|
|
||||||
handleDeleteNodeWrapper,
|
|
||||||
renamingNodeIds,
|
|
||||||
handleAutoRename,
|
|
||||||
exportMenuOptions,
|
|
||||||
onCreateNote,
|
|
||||||
onCreateFolder
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return { getMenuItems }
|
|
||||||
}
|
|
||||||
@@ -83,68 +83,6 @@ export async function renameNode(node: NotesTreeNode, newName: string): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadNotes(files: File[], targetPath: string): Promise<UploadResult> {
|
export async function uploadNotes(files: File[], targetPath: string): Promise<UploadResult> {
|
||||||
const basePath = normalizePath(targetPath)
|
|
||||||
const totalFiles = files.length
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
return {
|
|
||||||
uploadedNodes: [],
|
|
||||||
totalFiles: 0,
|
|
||||||
skippedFiles: 0,
|
|
||||||
fileCount: 0,
|
|
||||||
folderCount: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Get file paths from File objects
|
|
||||||
// For browser File objects from drag-and-drop, we need to use FileReader to save temporarily
|
|
||||||
// However, for directory uploads, the files already have paths
|
|
||||||
const filePaths: string[] = []
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
// @ts-ignore - webkitRelativePath exists on File objects from directory uploads
|
|
||||||
if (file.path) {
|
|
||||||
// @ts-ignore - Electron File objects have .path property
|
|
||||||
filePaths.push(file.path)
|
|
||||||
} else {
|
|
||||||
// For browser File API, we'd need to use FileReader and create temp files
|
|
||||||
// For now, fall back to the old method for these cases
|
|
||||||
logger.warn('File without path detected, using fallback method')
|
|
||||||
return uploadNotesLegacy(files, targetPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause file watcher to prevent N refresh events
|
|
||||||
await window.api.file.pauseFileWatcher()
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Use the new optimized batch upload API that runs in Main process
|
|
||||||
const result = await window.api.file.batchUploadMarkdown(filePaths, basePath)
|
|
||||||
|
|
||||||
return {
|
|
||||||
uploadedNodes: [],
|
|
||||||
totalFiles,
|
|
||||||
skippedFiles: result.skippedFiles,
|
|
||||||
fileCount: result.fileCount,
|
|
||||||
folderCount: result.folderCount
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
// Resume watcher and trigger single refresh
|
|
||||||
await window.api.file.resumeFileWatcher()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Batch upload failed, falling back to legacy method:', error as Error)
|
|
||||||
// Fall back to old method if new method fails
|
|
||||||
return uploadNotesLegacy(files, targetPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Legacy upload method using Renderer process
|
|
||||||
* Kept as fallback for browser File API files without paths
|
|
||||||
*/
|
|
||||||
async function uploadNotesLegacy(files: File[], targetPath: string): Promise<UploadResult> {
|
|
||||||
const basePath = normalizePath(targetPath)
|
const basePath = normalizePath(targetPath)
|
||||||
const markdownFiles = filterMarkdown(files)
|
const markdownFiles = filterMarkdown(files)
|
||||||
const skippedFiles = files.length - markdownFiles.length
|
const skippedFiles = files.length - markdownFiles.length
|
||||||
@@ -163,37 +101,18 @@ async function uploadNotesLegacy(files: File[], targetPath: string): Promise<Upl
|
|||||||
await createFolders(folders)
|
await createFolders(folders)
|
||||||
|
|
||||||
let fileCount = 0
|
let fileCount = 0
|
||||||
const BATCH_SIZE = 5 // Process 5 files concurrently to balance performance and responsiveness
|
|
||||||
|
|
||||||
// Process files in batches to avoid blocking the UI thread
|
for (const file of markdownFiles) {
|
||||||
for (let i = 0; i < markdownFiles.length; i += BATCH_SIZE) {
|
const { dir, name } = resolveFileTarget(file, basePath)
|
||||||
const batch = markdownFiles.slice(i, i + BATCH_SIZE)
|
const { safeName } = await window.api.file.checkFileName(dir, name, true)
|
||||||
|
const finalPath = `${dir}/${safeName}${MARKDOWN_EXT}`
|
||||||
|
|
||||||
// Process current batch in parallel
|
try {
|
||||||
const results = await Promise.allSettled(
|
const content = await file.text()
|
||||||
batch.map(async (file) => {
|
await window.api.file.write(finalPath, content)
|
||||||
const { dir, name } = resolveFileTarget(file, basePath)
|
fileCount += 1
|
||||||
const { safeName } = await window.api.file.checkFileName(dir, name, true)
|
} catch (error) {
|
||||||
const finalPath = `${dir}/${safeName}${MARKDOWN_EXT}`
|
logger.error('Failed to write uploaded file:', error as Error)
|
||||||
|
|
||||||
const content = await file.text()
|
|
||||||
await window.api.file.write(finalPath, content)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Count successful uploads
|
|
||||||
results.forEach((result) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
fileCount += 1
|
|
||||||
} else {
|
|
||||||
logger.error('Failed to write uploaded file:', result.reason)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Yield to the event loop between batches to keep UI responsive
|
|
||||||
if (i + BATCH_SIZE < markdownFiles.length) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export type NotesSortType =
|
|||||||
export interface NotesTreeNode {
|
export interface NotesTreeNode {
|
||||||
id: string
|
id: string
|
||||||
name: string // 不包含扩展名
|
name: string // 不包含扩展名
|
||||||
type: 'folder' | 'file' | 'hint'
|
type: 'folder' | 'file'
|
||||||
treePath: string // 相对路径
|
treePath: string // 相对路径
|
||||||
externalPath: string // 绝对路径
|
externalPath: string // 绝对路径
|
||||||
children?: NotesTreeNode[]
|
children?: NotesTreeNode[]
|
||||||
|
|||||||
@@ -1931,18 +1931,6 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@cherrystudio/embedjs-interfaces@npm:0.1.30":
|
|
||||||
version: 0.1.30
|
|
||||||
resolution: "@cherrystudio/embedjs-interfaces@npm:0.1.30"
|
|
||||||
dependencies:
|
|
||||||
"@langchain/core": "npm:^0.3.26"
|
|
||||||
debug: "npm:^4.4.0"
|
|
||||||
md5: "npm:^2.3.0"
|
|
||||||
uuid: "npm:^11.0.3"
|
|
||||||
checksum: 10c0/1d0eca816d89df25adfa15eb0b6ce67e8b3446966886c4e5e84f4c657daf3b5cad728c953479e8f317136a3c86ca512ebf13ceb070462da733eaab02937bc460
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@cherrystudio/embedjs-interfaces@npm:0.1.31":
|
"@cherrystudio/embedjs-interfaces@npm:0.1.31":
|
||||||
version: 0.1.31
|
version: 0.1.31
|
||||||
resolution: "@cherrystudio/embedjs-interfaces@npm:0.1.31"
|
resolution: "@cherrystudio/embedjs-interfaces@npm:0.1.31"
|
||||||
@@ -1955,15 +1943,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@cherrystudio/embedjs-libsql@npm:^0.1.31":
|
"@cherrystudio/embedjs-interfaces@npm:0.1.33":
|
||||||
version: 0.1.31
|
version: 0.1.33
|
||||||
resolution: "@cherrystudio/embedjs-libsql@npm:0.1.31"
|
resolution: "@cherrystudio/embedjs-interfaces@npm:0.1.33"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@cherrystudio/embedjs-interfaces": "npm:0.1.30"
|
"@langchain/core": "npm:^0.3.26"
|
||||||
"@cherrystudio/embedjs-utils": "npm:0.1.30"
|
|
||||||
"@libsql/client": "npm:^0.14.0"
|
|
||||||
debug: "npm:^4.4.0"
|
debug: "npm:^4.4.0"
|
||||||
checksum: 10c0/248453e07b7ff1661f18213f69d74a0ab2e5d722d3ae5409240fd38cf3c263da5c8a224635f6ec4cf823cdaa91846ba0f4890d64872133950810afcfd8512498
|
md5: "npm:^2.3.0"
|
||||||
|
uuid: "npm:^11.0.3"
|
||||||
|
checksum: 10c0/b5e8a9ca589056aa608f82e661b3185e417bcc4c0b27262d7c78c3cb99b8c3d2e64d69a645a95d94a7a7f5d6546b2ca3a8a6e11b0922a07f78ebcecc6e56630a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -2100,15 +2088,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@cherrystudio/embedjs-utils@npm:0.1.30":
|
|
||||||
version: 0.1.30
|
|
||||||
resolution: "@cherrystudio/embedjs-utils@npm:0.1.30"
|
|
||||||
dependencies:
|
|
||||||
"@cherrystudio/embedjs-interfaces": "npm:0.1.30"
|
|
||||||
checksum: 10c0/1bd6151a69b6e4db6c93528622ff4f7834f80834681f28758d19f9780e8da36f29c21737d49809021ba5b6b1127dd7d2891e26864e2d696f83f577966d1cbf2c
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@cherrystudio/embedjs-utils@npm:0.1.31":
|
"@cherrystudio/embedjs-utils@npm:0.1.31":
|
||||||
version: 0.1.31
|
version: 0.1.31
|
||||||
resolution: "@cherrystudio/embedjs-utils@npm:0.1.31"
|
resolution: "@cherrystudio/embedjs-utils@npm:0.1.31"
|
||||||
@@ -2118,6 +2097,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@cherrystudio/embedjs-utils@npm:0.1.33":
|
||||||
|
version: 0.1.33
|
||||||
|
resolution: "@cherrystudio/embedjs-utils@npm:0.1.33"
|
||||||
|
dependencies:
|
||||||
|
"@cherrystudio/embedjs-interfaces": "npm:0.1.33"
|
||||||
|
checksum: 10c0/5e5cbf4a3cb7af4f3c5b48ed0a0c20090abea39d80fe57c2aa6e2d6d304248c5b3cc4229d2733968b6f28a724aedeae96848d9b3a8138d27eda292c481200b8a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@cherrystudio/embedjs@npm:^0.1.31":
|
"@cherrystudio/embedjs@npm:^0.1.31":
|
||||||
version: 0.1.31
|
version: 0.1.31
|
||||||
resolution: "@cherrystudio/embedjs@npm:0.1.31"
|
resolution: "@cherrystudio/embedjs@npm:0.1.31"
|
||||||
@@ -2671,6 +2659,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@defi-failure/embedjs-libsql@npm:0.1.33":
|
||||||
|
version: 0.1.33
|
||||||
|
resolution: "@defi-failure/embedjs-libsql@npm:0.1.33"
|
||||||
|
dependencies:
|
||||||
|
"@cherrystudio/embedjs-interfaces": "npm:0.1.33"
|
||||||
|
"@cherrystudio/embedjs-utils": "npm:0.1.33"
|
||||||
|
"@libsql/client": "npm:^0.15.15"
|
||||||
|
debug: "npm:^4.4.0"
|
||||||
|
checksum: 10c0/28b8b5f31f78ca2c6576ba357a1d807938e41e85637cfb898724b732fc71cc759dfe2a1777fd1f855ec63029ebbb8ccefa66902bc891a62f3ee91a2603061a17
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@develar/schema-utils@npm:~2.6.5":
|
"@develar/schema-utils@npm:~2.6.5":
|
||||||
version: 2.6.5
|
version: 2.6.5
|
||||||
resolution: "@develar/schema-utils@npm:2.6.5"
|
resolution: "@develar/schema-utils@npm:2.6.5"
|
||||||
@@ -4558,38 +4558,38 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/client@npm:0.14.0, @libsql/client@npm:^0.14.0":
|
"@libsql/client@npm:0.15.15, @libsql/client@npm:^0.15.15":
|
||||||
version: 0.14.0
|
version: 0.15.15
|
||||||
resolution: "@libsql/client@npm:0.14.0"
|
resolution: "@libsql/client@npm:0.15.15"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@libsql/core": "npm:^0.14.0"
|
"@libsql/core": "npm:^0.15.14"
|
||||||
"@libsql/hrana-client": "npm:^0.7.0"
|
"@libsql/hrana-client": "npm:^0.7.0"
|
||||||
js-base64: "npm:^3.7.5"
|
js-base64: "npm:^3.7.5"
|
||||||
libsql: "npm:^0.4.4"
|
libsql: "npm:^0.5.22"
|
||||||
promise-limit: "npm:^2.7.0"
|
promise-limit: "npm:^2.7.0"
|
||||||
checksum: 10c0/9c6bab468453df765f647422c772af3578f1e108b663a80b99063f47ed3542db26ae0fcdba2e153d72e6d5089c5caeba947a167a6c065b0191a0832621539335
|
checksum: 10c0/1ae67280ebe27903ff142b07e2a256c22ef5ada65185286a72823e8eae8d9d2602e0d72e423d3bd64ae57494791bfffff946aa0fc7c2378b55a227ff63f8df69
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/core@npm:^0.14.0":
|
"@libsql/core@npm:^0.15.14":
|
||||||
version: 0.14.0
|
version: 0.15.15
|
||||||
resolution: "@libsql/core@npm:0.14.0"
|
resolution: "@libsql/core@npm:0.15.15"
|
||||||
dependencies:
|
dependencies:
|
||||||
js-base64: "npm:^3.7.5"
|
js-base64: "npm:^3.7.5"
|
||||||
checksum: 10c0/327bb991cf191d5a9a9fc0cc1a17123f7ca88f222187a3bde845fbad8ceaeaa1f139882080e4b2969da57b83e576c52702572e2838d1743c6bff75f95e6f774a
|
checksum: 10c0/0a619689c9504f4239d9745882a128b81e2f6c0547352bbb0d36932261c053bbcbea4435a17f91abe61556bb791f2f1203b36c36b2d4b4f369953d7949bdc40e
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/darwin-arm64@npm:0.4.7":
|
"@libsql/darwin-arm64@npm:0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "@libsql/darwin-arm64@npm:0.4.7"
|
resolution: "@libsql/darwin-arm64@npm:0.5.22"
|
||||||
conditions: os=darwin & cpu=arm64
|
conditions: os=darwin & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/darwin-x64@npm:0.4.7":
|
"@libsql/darwin-x64@npm:0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "@libsql/darwin-x64@npm:0.4.7"
|
resolution: "@libsql/darwin-x64@npm:0.5.22"
|
||||||
conditions: os=darwin & cpu=x64
|
conditions: os=darwin & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -4623,38 +4623,52 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/linux-arm64-gnu@npm:0.4.7":
|
"@libsql/linux-arm-gnueabihf@npm:0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "@libsql/linux-arm64-gnu@npm:0.4.7"
|
resolution: "@libsql/linux-arm-gnueabihf@npm:0.5.22"
|
||||||
|
conditions: os=linux & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@libsql/linux-arm-musleabihf@npm:0.5.22":
|
||||||
|
version: 0.5.22
|
||||||
|
resolution: "@libsql/linux-arm-musleabihf@npm:0.5.22"
|
||||||
|
conditions: os=linux & cpu=arm
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@libsql/linux-arm64-gnu@npm:0.5.22":
|
||||||
|
version: 0.5.22
|
||||||
|
resolution: "@libsql/linux-arm64-gnu@npm:0.5.22"
|
||||||
conditions: os=linux & cpu=arm64
|
conditions: os=linux & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/linux-arm64-musl@npm:0.4.7":
|
"@libsql/linux-arm64-musl@npm:0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "@libsql/linux-arm64-musl@npm:0.4.7"
|
resolution: "@libsql/linux-arm64-musl@npm:0.5.22"
|
||||||
conditions: os=linux & cpu=arm64
|
conditions: os=linux & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/linux-x64-gnu@npm:0.4.7":
|
"@libsql/linux-x64-gnu@npm:0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "@libsql/linux-x64-gnu@npm:0.4.7"
|
resolution: "@libsql/linux-x64-gnu@npm:0.5.22"
|
||||||
conditions: os=linux & cpu=x64
|
conditions: os=linux & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/linux-x64-musl@npm:0.4.7":
|
"@libsql/linux-x64-musl@npm:0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "@libsql/linux-x64-musl@npm:0.4.7"
|
resolution: "@libsql/linux-x64-musl@npm:0.5.22"
|
||||||
conditions: os=linux & cpu=x64
|
conditions: os=linux & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@libsql/win32-x64-msvc@npm:0.4.7, @libsql/win32-x64-msvc@npm:^0.4.7":
|
"@libsql/win32-x64-msvc@npm:0.5.22, @libsql/win32-x64-msvc@npm:^0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "@libsql/win32-x64-msvc@npm:0.4.7"
|
resolution: "@libsql/win32-x64-msvc@npm:0.5.22"
|
||||||
checksum: 10c0/2fcb8715b6f0571dec145eaaf3fd53c7c5aa5bf408fe1be9d84b10adc8a909bb6ee60b45e0d7052b0c1722c30ac212356a3f1adcdf7f57d5a59b48f36ca5bdf5
|
checksum: 10c0/1bb2730563c603c03a229faa352897685648659d85ba0872dda60cc02abc469fbd55539ffd8b86c81d00230d76292e5a4d2a763fe44c05694612ce6db6e929aa
|
||||||
conditions: os=win32 & cpu=x64
|
conditions: os=win32 & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@@ -7128,14 +7142,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@strongtz/win32-arm64-msvc@npm:^0.4.7":
|
|
||||||
version: 0.4.7
|
|
||||||
resolution: "@strongtz/win32-arm64-msvc@npm:0.4.7"
|
|
||||||
checksum: 10c0/21946f2ed43ff0b2381f945e22cf7f5ef6de412347c36523ed1d72f33591b2b85bf4cc9dd493f0eab95ed5e152d3944a2329791c3d8ed88c7ad450ccd9078015
|
|
||||||
conditions: os=win32 & cpu=arm64
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@svta/common-media-library@npm:^0.12.4":
|
"@svta/common-media-library@npm:^0.12.4":
|
||||||
version: 0.12.4
|
version: 0.12.4
|
||||||
resolution: "@svta/common-media-library@npm:0.12.4"
|
resolution: "@svta/common-media-library@npm:0.12.4"
|
||||||
@@ -8744,12 +8750,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-dom@npm:^19.2.3":
|
"@types/react-dom@npm:^19.0.4":
|
||||||
version: 19.2.3
|
version: 19.1.2
|
||||||
resolution: "@types/react-dom@npm:19.2.3"
|
resolution: "@types/react-dom@npm:19.1.2"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@types/react": ^19.2.0
|
"@types/react": ^19.0.0
|
||||||
checksum: 10c0/b486ebe0f4e2fb35e2e108df1d8fc0927ca5d6002d5771e8a739de11239fe62d0e207c50886185253c99eb9dedfeeb956ea7429e5ba17f6693c7acb4c02f8cd1
|
checksum: 10c0/100c341cacba9ec8ae1d47ee051072a3450e9573bf8eeb7262490e341cb246ea0f95a07a1f2077e61cf92648f812a0324c602fcd811bd87b7ce41db2811510cd
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -8789,12 +8795,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react@npm:^19.2.6":
|
"@types/react@npm:^19.0.12":
|
||||||
version: 19.2.6
|
version: 19.1.2
|
||||||
resolution: "@types/react@npm:19.2.6"
|
resolution: "@types/react@npm:19.1.2"
|
||||||
dependencies:
|
dependencies:
|
||||||
csstype: "npm:^3.2.2"
|
csstype: "npm:^3.0.2"
|
||||||
checksum: 10c0/23b1100f88662ce9f9e4fcca3a2b4ef9fff1ecde24ede2b2dcbd07731e48d6946fd7fd156cd133f5b25321694b0569cd9b8dd30b22c4e076d1cf4c8cdd9a75cb
|
checksum: 10c0/76ffe71395c713d4adc3c759465012d3c956db00af35ab7c6d0d91bd07b274b7ce69caa0478c0760311587bd1e38c78ffc9688ebc629f2b266682a19d8750947
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -9922,7 +9928,6 @@ __metadata:
|
|||||||
"@biomejs/biome": "npm:2.2.4"
|
"@biomejs/biome": "npm:2.2.4"
|
||||||
"@cherrystudio/ai-core": "workspace:^1.0.9"
|
"@cherrystudio/ai-core": "workspace:^1.0.9"
|
||||||
"@cherrystudio/embedjs": "npm:^0.1.31"
|
"@cherrystudio/embedjs": "npm:^0.1.31"
|
||||||
"@cherrystudio/embedjs-libsql": "npm:^0.1.31"
|
|
||||||
"@cherrystudio/embedjs-loader-csv": "npm:^0.1.31"
|
"@cherrystudio/embedjs-loader-csv": "npm:^0.1.31"
|
||||||
"@cherrystudio/embedjs-loader-image": "npm:^0.1.31"
|
"@cherrystudio/embedjs-loader-image": "npm:^0.1.31"
|
||||||
"@cherrystudio/embedjs-loader-markdown": "npm:^0.1.31"
|
"@cherrystudio/embedjs-loader-markdown": "npm:^0.1.31"
|
||||||
@@ -9935,6 +9940,7 @@ __metadata:
|
|||||||
"@cherrystudio/embedjs-openai": "npm:^0.1.31"
|
"@cherrystudio/embedjs-openai": "npm:^0.1.31"
|
||||||
"@cherrystudio/extension-table-plus": "workspace:^"
|
"@cherrystudio/extension-table-plus": "workspace:^"
|
||||||
"@cherrystudio/openai": "npm:^6.9.0"
|
"@cherrystudio/openai": "npm:^6.9.0"
|
||||||
|
"@defi-failure/embedjs-libsql": "npm:0.1.33"
|
||||||
"@dnd-kit/core": "npm:^6.3.1"
|
"@dnd-kit/core": "npm:^6.3.1"
|
||||||
"@dnd-kit/modifiers": "npm:^9.0.0"
|
"@dnd-kit/modifiers": "npm:^9.0.0"
|
||||||
"@dnd-kit/sortable": "npm:^10.0.0"
|
"@dnd-kit/sortable": "npm:^10.0.0"
|
||||||
@@ -9953,8 +9959,8 @@ __metadata:
|
|||||||
"@langchain/community": "npm:^1.0.0"
|
"@langchain/community": "npm:^1.0.0"
|
||||||
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch"
|
"@langchain/core": "patch:@langchain/core@npm%3A1.0.2#~/.yarn/patches/@langchain-core-npm-1.0.2-183ef83fe4.patch"
|
||||||
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch"
|
"@langchain/openai": "patch:@langchain/openai@npm%3A1.0.0#~/.yarn/patches/@langchain-openai-npm-1.0.0-474d0ad9d4.patch"
|
||||||
"@libsql/client": "npm:0.14.0"
|
"@libsql/client": "npm:0.15.15"
|
||||||
"@libsql/win32-x64-msvc": "npm:^0.4.7"
|
"@libsql/win32-x64-msvc": "npm:^0.5.22"
|
||||||
"@mistralai/mistralai": "npm:^1.7.5"
|
"@mistralai/mistralai": "npm:^1.7.5"
|
||||||
"@modelcontextprotocol/sdk": "npm:^1.17.5"
|
"@modelcontextprotocol/sdk": "npm:^1.17.5"
|
||||||
"@mozilla/readability": "npm:^0.6.0"
|
"@mozilla/readability": "npm:^0.6.0"
|
||||||
@@ -9973,7 +9979,6 @@ __metadata:
|
|||||||
"@radix-ui/react-context-menu": "npm:^2.2.16"
|
"@radix-ui/react-context-menu": "npm:^2.2.16"
|
||||||
"@reduxjs/toolkit": "npm:^2.2.5"
|
"@reduxjs/toolkit": "npm:^2.2.5"
|
||||||
"@shikijs/markdown-it": "npm:^3.12.0"
|
"@shikijs/markdown-it": "npm:^3.12.0"
|
||||||
"@strongtz/win32-arm64-msvc": "npm:^0.4.7"
|
|
||||||
"@swc/plugin-styled-components": "npm:^8.0.4"
|
"@swc/plugin-styled-components": "npm:^8.0.4"
|
||||||
"@tailwindcss/vite": "npm:^4.1.13"
|
"@tailwindcss/vite": "npm:^4.1.13"
|
||||||
"@tanstack/react-query": "npm:^5.85.5"
|
"@tanstack/react-query": "npm:^5.85.5"
|
||||||
@@ -10015,8 +10020,8 @@ __metadata:
|
|||||||
"@types/mime-types": "npm:^3"
|
"@types/mime-types": "npm:^3"
|
||||||
"@types/node": "npm:^22.17.1"
|
"@types/node": "npm:^22.17.1"
|
||||||
"@types/pako": "npm:^1.0.2"
|
"@types/pako": "npm:^1.0.2"
|
||||||
"@types/react": "npm:^19.2.6"
|
"@types/react": "npm:^19.0.12"
|
||||||
"@types/react-dom": "npm:^19.2.3"
|
"@types/react-dom": "npm:^19.0.4"
|
||||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||||
"@types/react-transition-group": "npm:^4.4.12"
|
"@types/react-transition-group": "npm:^4.4.12"
|
||||||
"@types/react-window": "npm:^1"
|
"@types/react-window": "npm:^1"
|
||||||
@@ -10107,6 +10112,7 @@ __metadata:
|
|||||||
jest-styled-components: "npm:^7.2.0"
|
jest-styled-components: "npm:^7.2.0"
|
||||||
js-yaml: "npm:^4.1.0"
|
js-yaml: "npm:^4.1.0"
|
||||||
jsdom: "npm:26.1.0"
|
jsdom: "npm:26.1.0"
|
||||||
|
libsql: "npm:^0.5.22"
|
||||||
linguist-languages: "npm:^8.1.0"
|
linguist-languages: "npm:^8.1.0"
|
||||||
lint-staged: "npm:^15.5.0"
|
lint-staged: "npm:^15.5.0"
|
||||||
lodash: "npm:^4.17.21"
|
lodash: "npm:^4.17.21"
|
||||||
@@ -12283,13 +12289,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"csstype@npm:^3.2.2":
|
|
||||||
version: 3.2.3
|
|
||||||
resolution: "csstype@npm:3.2.3"
|
|
||||||
checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"csv-parse@npm:^5.6.0":
|
"csv-parse@npm:^5.6.0":
|
||||||
version: 5.6.0
|
version: 5.6.0
|
||||||
resolution: "csv-parse@npm:5.6.0"
|
resolution: "csv-parse@npm:5.6.0"
|
||||||
@@ -17274,17 +17273,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"libsql@npm:0.4.7":
|
"libsql@npm:^0.5.22":
|
||||||
version: 0.4.7
|
version: 0.5.22
|
||||||
resolution: "libsql@npm:0.4.7"
|
resolution: "libsql@npm:0.5.22"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@libsql/darwin-arm64": "npm:0.4.7"
|
"@libsql/darwin-arm64": "npm:0.5.22"
|
||||||
"@libsql/darwin-x64": "npm:0.4.7"
|
"@libsql/darwin-x64": "npm:0.5.22"
|
||||||
"@libsql/linux-arm64-gnu": "npm:0.4.7"
|
"@libsql/linux-arm-gnueabihf": "npm:0.5.22"
|
||||||
"@libsql/linux-arm64-musl": "npm:0.4.7"
|
"@libsql/linux-arm-musleabihf": "npm:0.5.22"
|
||||||
"@libsql/linux-x64-gnu": "npm:0.4.7"
|
"@libsql/linux-arm64-gnu": "npm:0.5.22"
|
||||||
"@libsql/linux-x64-musl": "npm:0.4.7"
|
"@libsql/linux-arm64-musl": "npm:0.5.22"
|
||||||
"@libsql/win32-x64-msvc": "npm:0.4.7"
|
"@libsql/linux-x64-gnu": "npm:0.5.22"
|
||||||
|
"@libsql/linux-x64-musl": "npm:0.5.22"
|
||||||
|
"@libsql/win32-x64-msvc": "npm:0.5.22"
|
||||||
"@neon-rs/load": "npm:^0.0.4"
|
"@neon-rs/load": "npm:^0.0.4"
|
||||||
detect-libc: "npm:2.0.2"
|
detect-libc: "npm:2.0.2"
|
||||||
dependenciesMeta:
|
dependenciesMeta:
|
||||||
@@ -17292,6 +17293,10 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
"@libsql/darwin-x64":
|
"@libsql/darwin-x64":
|
||||||
optional: true
|
optional: true
|
||||||
|
"@libsql/linux-arm-gnueabihf":
|
||||||
|
optional: true
|
||||||
|
"@libsql/linux-arm-musleabihf":
|
||||||
|
optional: true
|
||||||
"@libsql/linux-arm64-gnu":
|
"@libsql/linux-arm64-gnu":
|
||||||
optional: true
|
optional: true
|
||||||
"@libsql/linux-arm64-musl":
|
"@libsql/linux-arm64-musl":
|
||||||
@@ -17302,41 +17307,8 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
"@libsql/win32-x64-msvc":
|
"@libsql/win32-x64-msvc":
|
||||||
optional: true
|
optional: true
|
||||||
checksum: 10c0/351952440e6bad3477e5f1bb1b9d6570d16e403b894f4a13c5c7e183a1307b2fb04a2fa902728cb8594a259e1726c51c61b822d545bbc88319b126ad15468a87
|
checksum: 10c0/6c34f08fc7408ebee16708ba12e5def9d1b2a4fa166070c956a120133ba9be68ec532e2d0b76bdc7005ef9ef69bf70d2ba7208ed824c4288c2a3d881edd5eaf6
|
||||||
conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=wasm32)
|
conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=wasm32 | cpu=arm)
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"libsql@patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch":
|
|
||||||
version: 0.4.7
|
|
||||||
resolution: "libsql@patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch::version=0.4.7&hash=972e11"
|
|
||||||
dependencies:
|
|
||||||
"@libsql/darwin-arm64": "npm:0.4.7"
|
|
||||||
"@libsql/darwin-x64": "npm:0.4.7"
|
|
||||||
"@libsql/linux-arm64-gnu": "npm:0.4.7"
|
|
||||||
"@libsql/linux-arm64-musl": "npm:0.4.7"
|
|
||||||
"@libsql/linux-x64-gnu": "npm:0.4.7"
|
|
||||||
"@libsql/linux-x64-musl": "npm:0.4.7"
|
|
||||||
"@libsql/win32-x64-msvc": "npm:0.4.7"
|
|
||||||
"@neon-rs/load": "npm:^0.0.4"
|
|
||||||
detect-libc: "npm:2.0.2"
|
|
||||||
dependenciesMeta:
|
|
||||||
"@libsql/darwin-arm64":
|
|
||||||
optional: true
|
|
||||||
"@libsql/darwin-x64":
|
|
||||||
optional: true
|
|
||||||
"@libsql/linux-arm64-gnu":
|
|
||||||
optional: true
|
|
||||||
"@libsql/linux-arm64-musl":
|
|
||||||
optional: true
|
|
||||||
"@libsql/linux-x64-gnu":
|
|
||||||
optional: true
|
|
||||||
"@libsql/linux-x64-musl":
|
|
||||||
optional: true
|
|
||||||
"@libsql/win32-x64-msvc":
|
|
||||||
optional: true
|
|
||||||
checksum: 10c0/6098770dc6c31ae0dbfe0821719d184d9bb353ac92553923096f6e3420d3786f240f0b3858f519af0aeada93beb4aa83cb9a9a1a6aa18d625511b484dcb53d07
|
|
||||||
conditions: (os=darwin | os=linux | os=win32) & (cpu=x64 | cpu=arm64 | cpu=wasm32)
|
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user