diff --git a/mateclaw-server/pom.xml b/mateclaw-server/pom.xml index 502833e0..b57b5293 100644 --- a/mateclaw-server/pom.xml +++ b/mateclaw-server/pom.xml @@ -6,7 +6,7 @@ vip.mate mateclaw-server - 1.0.0-SNAPSHOT + 1.0.101-SNAPSHOT jar MateClaw Server diff --git a/mateclaw-ui/package.json b/mateclaw-ui/package.json index 97a6f235..72cf6435 100644 --- a/mateclaw-ui/package.json +++ b/mateclaw-ui/package.json @@ -1,6 +1,6 @@ { "name": "mateclaw-ui", - "version": "1.0.0", + "version": "1.0.101-SNAPSHOT", "private": true, "type": "module", "description": "MateClaw - Personal AI Assistant Web Console", diff --git a/mateclaw-ui/src/components/chat/ChatInput.vue b/mateclaw-ui/src/components/chat/ChatInput.vue index 0dd8cf16..6fe364df 100644 --- a/mateclaw-ui/src/components/chat/ChatInput.vue +++ b/mateclaw-ui/src/components/chat/ChatInput.vue @@ -12,22 +12,25 @@
- - {{ attachment.name }} - {{ formatFileSize(attachment.size) }} - + {{ attachment.contentType === 'inode/directory' ? '📁 ' : '' }}{{ attachment.name }} + {{ formatFileSize(attachment.size) }} + {{ attachment.path }} + @@ -443,6 +446,19 @@ defineExpose({ background: rgba(217, 119, 87, 0.24); } +.attachment-chip--dir { + border-style: dashed; +} + +.attachment-chip__path { + font-size: 11px; + color: var(--mc-text-tertiary, #94a3b8); + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* 输入区域 */ .input-area { display: flex; diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 32e3f607..ef7ea3c1 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -106,7 +106,7 @@ export default { deleteConversationFailed: 'Failed to delete conversation', switchModelFailed: 'Failed to switch model', uploadFailed: 'File upload failed', - dropToUpload: 'Drop files here to upload', + dropToUpload: 'Drop files or folders here', copyFailed: 'Copy failed', dateToday: 'Today', dateYesterday: 'Yesterday', diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 95866b02..c5a1790f 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -106,7 +106,7 @@ export default { deleteConversationFailed: '删除会话失败', switchModelFailed: '切换模型失败', uploadFailed: '文件上传失败', - dropToUpload: '拖放文件到此处上传', + dropToUpload: '拖放文件或文件夹到此处', copyFailed: '复制失败', dateToday: '今天', dateYesterday: '昨天', diff --git a/mateclaw-ui/src/views/ChatConsole.vue b/mateclaw-ui/src/views/ChatConsole.vue index 88e75c14..ddd578ea 100644 --- a/mateclaw-ui/src/views/ChatConsole.vue +++ b/mateclaw-ui/src/views/ChatConsole.vue @@ -240,13 +240,94 @@ function onDragLeave() { } } -function onDrop(e: DragEvent) { +async function onDrop(e: DragEvent) { dragCounter = 0 isDragging.value = false - const files = Array.from(e.dataTransfer?.files || []) - if (files.length) { - handleFileSelect(files) + + const dtFiles = Array.from(e.dataTransfer?.files || []) + const items = Array.from(e.dataTransfer?.items || []) + + const electronDirs: File[] = [] + const webDirEntries: FileSystemDirectoryEntry[] = [] + const regularFiles: File[] = [] + + for (let i = 0; i < items.length; i++) { + const entry = items[i].webkitGetAsEntry?.() + const file = dtFiles[i] + if (entry?.isDirectory) { + if ((file as any)?.path) { + // Electron: has absolute path + electronDirs.push(file) + } else { + // Web: need to recursively collect files + webDirEntries.push(entry as FileSystemDirectoryEntry) + } + } else if (file) { + regularFiles.push(file) + } } + + // Electron directories → record path reference + if (electronDirs.length) { + handleDirectoryAttach(electronDirs) + } + // Web directories → recursively collect files and upload + if (webDirEntries.length) { + const collected = await collectFilesFromEntries(webDirEntries) + if (collected.length) { + handleFileSelect(collected) + } + } + // Regular files → normal upload + if (regularFiles.length) { + handleFileSelect(regularFiles) + } +} + +function handleDirectoryAttach(dirFiles: File[]) { + if (!currentConversationId.value) { + newConversation() + } + for (const dir of dirFiles) { + const dirPath = (dir as any).path as string + pendingAttachments.value.push({ + name: dirPath.split('/').pop() || dir.name, + size: 0, + url: '', + storedName: '', + path: dirPath, + contentType: 'inode/directory', + }) + } +} + +async function collectFilesFromEntries(dirEntries: FileSystemDirectoryEntry[]): Promise { + const files: File[] = [] + + async function readDir(dir: FileSystemDirectoryEntry) { + const reader = dir.createReader() + let batch: FileSystemEntry[] + do { + batch = await new Promise((resolve, reject) => { + reader.readEntries(resolve, reject) + }) + for (const entry of batch) { + if (entry.isFile) { + const file = await new Promise((resolve, reject) => { + (entry as FileSystemFileEntry).file(resolve, reject) + }) + files.push(file) + } else if (entry.isDirectory) { + await readDir(entry as FileSystemDirectoryEntry) + } + } + } while (batch.length > 0) // readEntries returns empty when done + } + + for (const dir of dirEntries) { + await readDir(dir) + } + return files } const messageListRef = ref | null>(null) @@ -815,8 +896,10 @@ async function handleFileSelect(files: File[]) { } } -function removeAttachment(storedName: string) { - pendingAttachments.value = pendingAttachments.value.filter(a => a.storedName !== storedName) +function removeAttachment(key: string) { + pendingAttachments.value = pendingAttachments.value.filter( + a => a.storedName !== key && a.path !== key + ) } function buildOutgoingParts(text: string, attachments: ChatAttachment[]): MessageContentPart[] {