chore: bump version to 1.0.101-SNAPSHOT with versioned docs

This commit is contained in:
matevip 2026-04-05 06:22:28 +08:00
parent d7dcaab6f4
commit eea622099d
6 changed files with 116 additions and 17 deletions

View File

@ -6,7 +6,7 @@
<groupId>vip.mate</groupId>
<artifactId>mateclaw-server</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>1.0.101-SNAPSHOT</version>
<packaging>jar</packaging>
<name>MateClaw Server</name>

View File

@ -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",

View File

@ -12,22 +12,25 @@
<div v-if="attachments.length" class="attachment-list">
<div
v-for="attachment in attachments"
:key="attachment.storedName"
:key="attachment.storedName || attachment.path"
class="attachment-chip"
:class="{ 'attachment-chip--dir': attachment.contentType === 'inode/directory' }"
>
<a
:href="attachment.url"
<component
:is="attachment.url ? 'a' : 'span'"
:href="attachment.url || undefined"
target="_blank"
rel="noreferrer"
class="attachment-chip__label"
>
<span>{{ attachment.name }}</span>
<span>{{ formatFileSize(attachment.size) }}</span>
</a>
<span>{{ attachment.contentType === 'inode/directory' ? '📁 ' : '' }}{{ attachment.name }}</span>
<span v-if="attachment.size">{{ formatFileSize(attachment.size) }}</span>
<span v-else-if="attachment.contentType === 'inode/directory'" class="attachment-chip__path">{{ attachment.path }}</span>
</component>
<button
type="button"
class="attachment-chip__remove"
@click="removeAttachment(attachment.storedName)"
@click="removeAttachment(attachment.storedName || attachment.path)"
>
×
</button>
@ -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;

View File

@ -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',

View File

@ -106,7 +106,7 @@ export default {
deleteConversationFailed: '删除会话失败',
switchModelFailed: '切换模型失败',
uploadFailed: '文件上传失败',
dropToUpload: '拖放文件到此处上传',
dropToUpload: '拖放文件或文件夹到此处',
copyFailed: '复制失败',
dateToday: '今天',
dateYesterday: '昨天',

View File

@ -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<File[]> {
const files: File[] = []
async function readDir(dir: FileSystemDirectoryEntry) {
const reader = dir.createReader()
let batch: FileSystemEntry[]
do {
batch = await new Promise<FileSystemEntry[]>((resolve, reject) => {
reader.readEntries(resolve, reject)
})
for (const entry of batch) {
if (entry.isFile) {
const file = await new Promise<File>((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<InstanceType<typeof MessageList> | 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[] {