feat: 优化聊天附件拖拽上传提示
- 不支持格式改用轻量提示并展示支持范围 - 扩展拖拽区域并增加全区上传覆盖层 - 补充文档和图片格式校验测试
This commit is contained in:
@@ -107,6 +107,21 @@ describe('useChatDocumentUploads', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects unsupported files without adding an error attachment', async () => {
|
||||||
|
const uploads = useChatDocumentUploads();
|
||||||
|
const result = await uploads.addFiles(
|
||||||
|
[new File(['archive'], 'unsupported.zip', { type: 'application/zip' })],
|
||||||
|
{ agentId: 'agent-1', mode: 'FORMAL', sessionId: 'session-1' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
rejectedCount: 0,
|
||||||
|
unsupportedCount: 1,
|
||||||
|
});
|
||||||
|
expect(uploads.items.value).toEqual([]);
|
||||||
|
expect(mediaApi.uploadAgentChatDocument).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('exposes a server read failure without continuing to poll', async () => {
|
it('exposes a server read failure without continuing to poll', async () => {
|
||||||
mediaApi.uploadAgentChatDocument.mockResolvedValue({
|
mediaApi.uploadAgentChatDocument.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -59,4 +59,19 @@ describe('useChatImageUploads', () => {
|
|||||||
expect(observedStatuses.at(-1)).toBe('ready');
|
expect(observedStatuses.at(-1)).toBe('ready');
|
||||||
expect(uploads.uploadIds.value).toEqual(['upload-1']);
|
expect(uploads.uploadIds.value).toEqual(['upload-1']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects unsupported images without adding an error attachment', async () => {
|
||||||
|
const uploads = useChatImageUploads();
|
||||||
|
const result = await uploads.addFiles(
|
||||||
|
[new File(['vector'], 'unsupported.svg', { type: 'image/svg+xml' })],
|
||||||
|
{ agentId: 'agent-1', mode: 'FORMAL', sessionId: 'session-1' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
rejectedCount: 0,
|
||||||
|
unsupportedCount: 1,
|
||||||
|
});
|
||||||
|
expect(uploads.items.value).toEqual([]);
|
||||||
|
expect(mediaApi.uploadAgentChatImage).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { AgentComposerMode, AgentMediaUpload } from './mediaApi';
|
|||||||
|
|
||||||
const MAX_IMAGES = 5;
|
const MAX_IMAGES = 5;
|
||||||
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||||
|
export const CHAT_IMAGE_SUPPORTED_FORMATS = 'PNG、JPG、JPEG、WebP、GIF、BMP';
|
||||||
const ACCEPTED_EXTENSIONS = new Set([
|
const ACCEPTED_EXTENSIONS = new Set([
|
||||||
'bmp',
|
'bmp',
|
||||||
'gif',
|
'gif',
|
||||||
@@ -26,6 +27,11 @@ interface LocalAttachment extends ChatImageAttachment {
|
|||||||
file?: File;
|
file?: File;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChatImageAddFilesResult {
|
||||||
|
rejectedCount: number;
|
||||||
|
unsupportedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
function createLocalId() {
|
function createLocalId() {
|
||||||
return `agent-image-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
return `agent-image-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
|
||||||
}
|
}
|
||||||
@@ -69,12 +75,20 @@ export function useChatImageUploads() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function addFiles(files: File[], context: UploadContext) {
|
async function addFiles(files: File[], context: UploadContext) {
|
||||||
|
const supportedFiles: File[] = [];
|
||||||
|
let unsupportedCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
if (ACCEPTED_EXTENSIONS.has(extensionOf(file))) {
|
||||||
|
supportedFiles.push(file);
|
||||||
|
} else {
|
||||||
|
unsupportedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
const available = Math.max(0, MAX_IMAGES - items.value.length);
|
const available = Math.max(0, MAX_IMAGES - items.value.length);
|
||||||
const accepted = files.slice(0, available);
|
const accepted = supportedFiles.slice(0, available);
|
||||||
const rejectedCount = Math.max(0, files.length - accepted.length);
|
const rejectedCount = Math.max(0, supportedFiles.length - accepted.length);
|
||||||
const uploads: Promise<void>[] = [];
|
const uploads: Promise<void>[] = [];
|
||||||
for (const file of accepted) {
|
for (const file of accepted) {
|
||||||
const extension = extensionOf(file);
|
|
||||||
const local: LocalAttachment = {
|
const local: LocalAttachment = {
|
||||||
file,
|
file,
|
||||||
localId: createLocalId(),
|
localId: createLocalId(),
|
||||||
@@ -85,11 +99,6 @@ export function useChatImageUploads() {
|
|||||||
status: 'uploading',
|
status: 'uploading',
|
||||||
};
|
};
|
||||||
items.value.push(local);
|
items.value.push(local);
|
||||||
if (!ACCEPTED_EXTENSIONS.has(extension)) {
|
|
||||||
local.status = 'error';
|
|
||||||
local.error = '仅支持 PNG、JPG、JPEG、WebP、GIF、BMP';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (file.size > MAX_IMAGE_BYTES) {
|
if (file.size > MAX_IMAGE_BYTES) {
|
||||||
local.status = 'error';
|
local.status = 'error';
|
||||||
local.error = '单张图片不能超过 10 MiB';
|
local.error = '单张图片不能超过 10 MiB';
|
||||||
@@ -98,7 +107,10 @@ export function useChatImageUploads() {
|
|||||||
uploads.push(upload(local, context));
|
uploads.push(upload(local, context));
|
||||||
}
|
}
|
||||||
await Promise.all(uploads);
|
await Promise.all(uploads);
|
||||||
return rejectedCount;
|
return {
|
||||||
|
rejectedCount,
|
||||||
|
unsupportedCount,
|
||||||
|
} satisfies ChatImageAddFilesResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upload(item: LocalAttachment, context: UploadContext) {
|
async function upload(item: LocalAttachment, context: UploadContext) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
CHAT_DOCUMENT_SUPPORTED_FORMATS,
|
||||||
ChatDocumentAttachments,
|
ChatDocumentAttachments,
|
||||||
ChatImageAttachments,
|
ChatImageAttachments,
|
||||||
ChatTimeline,
|
ChatTimeline,
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
Paperclip,
|
Paperclip,
|
||||||
Plus,
|
Plus,
|
||||||
Promotion,
|
Promotion,
|
||||||
|
UploadFilled,
|
||||||
} from '@element-plus/icons-vue';
|
} from '@element-plus/icons-vue';
|
||||||
import {
|
import {
|
||||||
ElButton,
|
ElButton,
|
||||||
@@ -52,6 +54,7 @@ import {
|
|||||||
loadAgentChatImage,
|
loadAgentChatImage,
|
||||||
} from '#/components/ai-chat/mediaApi';
|
} from '#/components/ai-chat/mediaApi';
|
||||||
import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft';
|
import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft';
|
||||||
|
import { CHAT_IMAGE_SUPPORTED_FORMATS } from '#/components/ai-chat/useChatImageUploads';
|
||||||
import ChatCapabilityMenu from '#/components/chat-workspace/ChatCapabilityMenu.vue';
|
import ChatCapabilityMenu from '#/components/chat-workspace/ChatCapabilityMenu.vue';
|
||||||
import ChatInputTriggerPanel from '#/components/chat-workspace/ChatInputTriggerPanel.vue';
|
import ChatInputTriggerPanel from '#/components/chat-workspace/ChatInputTriggerPanel.vue';
|
||||||
import { useChatInputTrigger } from '#/components/chat-workspace/input-triggers/useChatInputTrigger';
|
import { useChatInputTrigger } from '#/components/chat-workspace/input-triggers/useChatInputTrigger';
|
||||||
@@ -114,6 +117,11 @@ const selectedAgentImageSupport = computed(() =>
|
|||||||
selectedAgent.value?.publishedSnapshotJson?.modelSummary?.supportImage,
|
selectedAgent.value?.publishedSnapshotJson?.modelSummary?.supportImage,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const supportedAttachmentFormats = computed(() =>
|
||||||
|
selectedAgentImageSupport.value === false
|
||||||
|
? CHAT_DOCUMENT_SUPPORTED_FORMATS
|
||||||
|
: `${CHAT_DOCUMENT_SUPPORTED_FORMATS};图片支持 ${CHAT_IMAGE_SUPPORTED_FORMATS}`,
|
||||||
|
);
|
||||||
const interactionDisplay = computed(() =>
|
const interactionDisplay = computed(() =>
|
||||||
resolveInteractionDisplay(selectedAgent.value),
|
resolveInteractionDisplay(selectedAgent.value),
|
||||||
);
|
);
|
||||||
@@ -747,17 +755,29 @@ async function addImageFiles(files: File[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (selectedAgentImageSupport.value === false) {
|
if (selectedAgentImageSupport.value === false) {
|
||||||
ElMessage.warning('当前智能体不支持图片');
|
ElMessage.warning({
|
||||||
|
grouping: true,
|
||||||
|
message: `当前智能体不支持图片,支持:${CHAT_DOCUMENT_SUPPORTED_FORMATS}`,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await composer.ensureSession();
|
await composer.ensureSession();
|
||||||
const rejected = await composer.images.addFiles(files, {
|
const { rejectedCount, unsupportedCount } = await composer.images.addFiles(
|
||||||
|
files,
|
||||||
|
{
|
||||||
agentId: selectedAgentId.value,
|
agentId: selectedAgentId.value,
|
||||||
mode: 'FORMAL',
|
mode: 'FORMAL',
|
||||||
sessionId: composer.sessionId.value,
|
sessionId: composer.sessionId.value,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
composer.scheduleSave();
|
composer.scheduleSave();
|
||||||
if (rejected > 0) {
|
if (unsupportedCount > 0) {
|
||||||
|
ElMessage.warning({
|
||||||
|
grouping: true,
|
||||||
|
message: `不支持该图片格式,支持:${CHAT_IMAGE_SUPPORTED_FORMATS}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (rejectedCount > 0) {
|
||||||
ElMessage.warning('每次最多添加 5 张图片');
|
ElMessage.warning('每次最多添加 5 张图片');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -768,22 +788,38 @@ async function addDocumentFiles(files: File[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await composer.ensureSession();
|
await composer.ensureSession();
|
||||||
const rejected = await composer.documents.addFiles(files, {
|
const { rejectedCount, unsupportedCount } = await composer.documents.addFiles(
|
||||||
|
files,
|
||||||
|
{
|
||||||
agentId: selectedAgentId.value,
|
agentId: selectedAgentId.value,
|
||||||
mode: 'FORMAL',
|
mode: 'FORMAL',
|
||||||
sessionId: composer.sessionId.value,
|
sessionId: composer.sessionId.value,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
composer.scheduleSave();
|
composer.scheduleSave();
|
||||||
if (rejected > 0) {
|
if (unsupportedCount > 0) {
|
||||||
|
ElMessage.warning({
|
||||||
|
grouping: true,
|
||||||
|
message: `不支持该文件格式,支持:${supportedAttachmentFormats.value}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (rejectedCount > 0) {
|
||||||
ElMessage.warning('每轮最多添加 3 份文档');
|
ElMessage.warning('每轮最多添加 3 份文档');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImageAttachmentFile(file: File) {
|
||||||
|
return (
|
||||||
|
file.type.startsWith('image/') ||
|
||||||
|
/\.(?:bmp|gif|jpe?g|png|webp)$/i.test(file.name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function handleAttachmentFiles(event: Event) {
|
function handleAttachmentFiles(event: Event) {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const files = [...(target.files || [])];
|
const files = [...(target.files || [])];
|
||||||
const imageFiles = files.filter((file) => file.type.startsWith('image/'));
|
const imageFiles = files.filter((file) => isImageAttachmentFile(file));
|
||||||
const documentFiles = files.filter((file) => !file.type.startsWith('image/'));
|
const documentFiles = files.filter((file) => !isImageAttachmentFile(file));
|
||||||
if (imageFiles.length > 0) void addImageFiles(imageFiles);
|
if (imageFiles.length > 0) void addImageFiles(imageFiles);
|
||||||
if (documentFiles.length > 0) void addDocumentFiles(documentFiles);
|
if (documentFiles.length > 0) void addDocumentFiles(documentFiles);
|
||||||
target.value = '';
|
target.value = '';
|
||||||
@@ -810,6 +846,13 @@ function handleAttachmentDragEnter(event: DragEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleAttachmentDragOver(event: DragEvent) {
|
||||||
|
handleAttachmentDragEnter(event);
|
||||||
|
if (composerDragActive.value && event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleAttachmentDragLeave(event: DragEvent) {
|
function handleAttachmentDragLeave(event: DragEvent) {
|
||||||
const container = event.currentTarget as HTMLElement;
|
const container = event.currentTarget as HTMLElement;
|
||||||
if (
|
if (
|
||||||
@@ -824,17 +867,8 @@ function handleAttachmentDrop(event: DragEvent) {
|
|||||||
composerDragActive.value = false;
|
composerDragActive.value = false;
|
||||||
if (capabilityDisabled.value) return;
|
if (capabilityDisabled.value) return;
|
||||||
const files = [...(event.dataTransfer?.files || [])];
|
const files = [...(event.dataTransfer?.files || [])];
|
||||||
const imageFiles = files.filter(
|
const imageFiles = files.filter((file) => isImageAttachmentFile(file));
|
||||||
(file) =>
|
const documentFiles = files.filter((file) => !isImageAttachmentFile(file));
|
||||||
file.type.startsWith('image/') &&
|
|
||||||
selectedAgentImageSupport.value !== false &&
|
|
||||||
composer.images.items.value.length < 5,
|
|
||||||
);
|
|
||||||
const documentFiles = files.filter(
|
|
||||||
(file) =>
|
|
||||||
!file.type.startsWith('image/') &&
|
|
||||||
composer.documents.items.value.length < 3,
|
|
||||||
);
|
|
||||||
if (imageFiles.length > 0) void addImageFiles(imageFiles);
|
if (imageFiles.length > 0) void addImageFiles(imageFiles);
|
||||||
if (documentFiles.length > 0) void addDocumentFiles(documentFiles);
|
if (documentFiles.length > 0) void addDocumentFiles(documentFiles);
|
||||||
}
|
}
|
||||||
@@ -1151,7 +1185,13 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="agent-chat__main">
|
<main
|
||||||
|
class="agent-chat__main"
|
||||||
|
@dragenter.prevent="handleAttachmentDragEnter"
|
||||||
|
@dragover.prevent="handleAttachmentDragOver"
|
||||||
|
@dragleave.prevent="handleAttachmentDragLeave"
|
||||||
|
@drop.prevent="handleAttachmentDrop"
|
||||||
|
>
|
||||||
<header class="agent-chat__main-head">
|
<header class="agent-chat__main-head">
|
||||||
<div>
|
<div>
|
||||||
<div class="agent-chat__main-title">
|
<div class="agent-chat__main-title">
|
||||||
@@ -1214,14 +1254,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="agent-chat__composer">
|
||||||
class="agent-chat__composer"
|
|
||||||
:class="{ 'is-dragging': composerDragActive }"
|
|
||||||
@dragenter.prevent="handleAttachmentDragEnter"
|
|
||||||
@dragover.prevent
|
|
||||||
@dragleave.prevent="handleAttachmentDragLeave"
|
|
||||||
@drop.prevent="handleAttachmentDrop"
|
|
||||||
>
|
|
||||||
<ChatCapabilityMenu
|
<ChatCapabilityMenu
|
||||||
:disabled="capabilityDisabled"
|
:disabled="capabilityDisabled"
|
||||||
:extra-knowledge-ids="extraKnowledgeIds"
|
:extra-knowledge-ids="extraKnowledgeIds"
|
||||||
@@ -1353,6 +1386,24 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Transition name="agent-chat-drag-fade">
|
||||||
|
<div
|
||||||
|
v-if="composerDragActive"
|
||||||
|
class="agent-chat__drop-overlay"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<div class="agent-chat__drop-overlay-content">
|
||||||
|
<ElIcon class="agent-chat__drop-overlay-icon">
|
||||||
|
<UploadFilled />
|
||||||
|
</ElIcon>
|
||||||
|
<div class="agent-chat__drop-overlay-title">松手上传文件</div>
|
||||||
|
<div class="agent-chat__drop-overlay-formats">
|
||||||
|
支持 {{ supportedAttachmentFormats }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -1526,9 +1577,53 @@ onBeforeUnmount(() => {
|
|||||||
box-shadow: var(--el-box-shadow-light);
|
box-shadow: var(--el-box-shadow-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-chat__composer.is-dragging {
|
.agent-chat__drop-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 16px;
|
||||||
|
z-index: 20;
|
||||||
|
display: grid;
|
||||||
|
pointer-events: none;
|
||||||
background: var(--el-color-primary-light-9);
|
background: var(--el-color-primary-light-9);
|
||||||
border-color: var(--el-color-primary-light-5);
|
border: 2px dashed var(--el-color-primary-light-5);
|
||||||
|
border-radius: 16px;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-chat__drop-overlay-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
max-width: min(560px, calc(100% - 48px));
|
||||||
|
padding: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-chat__drop-overlay-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-chat__drop-overlay-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-chat__drop-overlay-formats {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-chat-drag-fade-enter-active,
|
||||||
|
.agent-chat-drag-fade-leave-active {
|
||||||
|
transition: opacity 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-chat-drag-fade-enter-from,
|
||||||
|
.agent-chat-drag-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-chat__trigger-panel {
|
.agent-chat__trigger-panel {
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import type {
|
|||||||
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { ChatTimeline } from '@easyflow/common-ui';
|
import {
|
||||||
|
CHAT_DOCUMENT_SUPPORTED_FORMATS,
|
||||||
|
ChatTimeline,
|
||||||
|
} from '@easyflow/common-ui';
|
||||||
import { BrushCleaning } from '@easyflow/icons';
|
import { BrushCleaning } from '@easyflow/icons';
|
||||||
import { copyTextToClipboard } from '@easyflow/utils';
|
import { copyTextToClipboard } from '@easyflow/utils';
|
||||||
|
|
||||||
@@ -26,6 +29,7 @@ import {
|
|||||||
loadAgentChatImage,
|
loadAgentChatImage,
|
||||||
} from '#/components/ai-chat/mediaApi';
|
} from '#/components/ai-chat/mediaApi';
|
||||||
import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft';
|
import { useAgentComposerDraft } from '#/components/ai-chat/useAgentComposerDraft';
|
||||||
|
import { CHAT_IMAGE_SUPPORTED_FORMATS } from '#/components/ai-chat/useChatImageUploads';
|
||||||
|
|
||||||
import { approveAgentRun, rejectAgentRun } from '../api';
|
import { approveAgentRun, rejectAgentRun } from '../api';
|
||||||
import { useAgentTryoutStream } from '../composables/useAgentTryoutStream';
|
import { useAgentTryoutStream } from '../composables/useAgentTryoutStream';
|
||||||
@@ -214,12 +218,21 @@ async function handleAddImageFiles(files: File[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await composer.ensureSession();
|
await composer.ensureSession();
|
||||||
const rejected = await composer.images.addFiles(files, {
|
const { rejectedCount, unsupportedCount } = await composer.images.addFiles(
|
||||||
|
files,
|
||||||
|
{
|
||||||
agentId: String(props.agent.id),
|
agentId: String(props.agent.id),
|
||||||
mode: 'DRAFT',
|
mode: 'DRAFT',
|
||||||
sessionId: composer.sessionId.value,
|
sessionId: composer.sessionId.value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (unsupportedCount > 0) {
|
||||||
|
ElMessage.warning({
|
||||||
|
grouping: true,
|
||||||
|
message: `不支持该图片格式,支持:${CHAT_IMAGE_SUPPORTED_FORMATS}`,
|
||||||
});
|
});
|
||||||
if (rejected > 0) {
|
}
|
||||||
|
if (rejectedCount > 0) {
|
||||||
ElMessage.warning('每次最多添加 5 张图片');
|
ElMessage.warning('每次最多添加 5 张图片');
|
||||||
}
|
}
|
||||||
composer.scheduleSave();
|
composer.scheduleSave();
|
||||||
@@ -231,12 +244,21 @@ async function handleAddDocumentFiles(files: File[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await composer.ensureSession();
|
await composer.ensureSession();
|
||||||
const rejected = await composer.documents.addFiles(files, {
|
const { rejectedCount, unsupportedCount } = await composer.documents.addFiles(
|
||||||
|
files,
|
||||||
|
{
|
||||||
agentId: String(props.agent.id),
|
agentId: String(props.agent.id),
|
||||||
mode: 'DRAFT',
|
mode: 'DRAFT',
|
||||||
sessionId: composer.sessionId.value,
|
sessionId: composer.sessionId.value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (unsupportedCount > 0) {
|
||||||
|
ElMessage.warning({
|
||||||
|
grouping: true,
|
||||||
|
message: `不支持该文件格式,支持:${CHAT_DOCUMENT_SUPPORTED_FORMATS}`,
|
||||||
});
|
});
|
||||||
if (rejected > 0) {
|
}
|
||||||
|
if (rejectedCount > 0) {
|
||||||
ElMessage.warning('每轮最多添加 3 份文档');
|
ElMessage.warning('每轮最多添加 3 份文档');
|
||||||
}
|
}
|
||||||
composer.scheduleSave();
|
composer.scheduleSave();
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export type {
|
|||||||
ChatTimelineToolStatus,
|
ChatTimelineToolStatus,
|
||||||
} from './types';
|
} from './types';
|
||||||
export {
|
export {
|
||||||
|
CHAT_DOCUMENT_SUPPORTED_FORMATS,
|
||||||
|
type ChatDocumentAddFilesResult,
|
||||||
type ChatDocumentUploadApi,
|
type ChatDocumentUploadApi,
|
||||||
type ChatDocumentUploadContext,
|
type ChatDocumentUploadContext,
|
||||||
type ChatDocumentUploadView,
|
type ChatDocumentUploadView,
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const MAX_EXCEL_BYTES = 10 * 1024 * 1024;
|
|||||||
const MAX_TEXT_BYTES = 5 * 1024 * 1024;
|
const MAX_TEXT_BYTES = 5 * 1024 * 1024;
|
||||||
const POLL_INTERVAL_MS = 750;
|
const POLL_INTERVAL_MS = 750;
|
||||||
const MAX_POLL_ATTEMPTS = 80;
|
const MAX_POLL_ATTEMPTS = 80;
|
||||||
|
export const CHAT_DOCUMENT_SUPPORTED_FORMATS =
|
||||||
|
'PDF、Word、PPT、Excel、TXT、Markdown';
|
||||||
const SUPPORTED_EXTENSIONS = new Set([
|
const SUPPORTED_EXTENSIONS = new Set([
|
||||||
'doc',
|
'doc',
|
||||||
'docx',
|
'docx',
|
||||||
@@ -54,6 +56,11 @@ export interface ChatDocumentUploadApi {
|
|||||||
) => Promise<RequestResult<ChatDocumentUploadView>>;
|
) => Promise<RequestResult<ChatDocumentUploadView>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChatDocumentAddFilesResult {
|
||||||
|
rejectedCount: number;
|
||||||
|
unsupportedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface LocalDocument extends ChatDocumentAttachment {
|
interface LocalDocument extends ChatDocumentAttachment {
|
||||||
file?: File;
|
file?: File;
|
||||||
}
|
}
|
||||||
@@ -198,9 +205,18 @@ export function createChatDocumentUploads(api: ChatDocumentUploadApi) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addFiles(files: File[], context: ChatDocumentUploadContext) {
|
async function addFiles(files: File[], context: ChatDocumentUploadContext) {
|
||||||
|
const supportedFiles: File[] = [];
|
||||||
|
let unsupportedCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
if (SUPPORTED_EXTENSIONS.has(extensionOf(file))) {
|
||||||
|
supportedFiles.push(file);
|
||||||
|
} else {
|
||||||
|
unsupportedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
const available = Math.max(0, MAX_DOCUMENTS - items.value.length);
|
const available = Math.max(0, MAX_DOCUMENTS - items.value.length);
|
||||||
const accepted = files.slice(0, available);
|
const accepted = supportedFiles.slice(0, available);
|
||||||
const rejectedCount = Math.max(0, files.length - accepted.length);
|
const rejectedCount = Math.max(0, supportedFiles.length - accepted.length);
|
||||||
const currentTotal = items.value.reduce(
|
const currentTotal = items.value.reduce(
|
||||||
(total, item) => total + Number(item.size || 0),
|
(total, item) => total + Number(item.size || 0),
|
||||||
0,
|
0,
|
||||||
@@ -220,11 +236,6 @@ export function createChatDocumentUploads(api: ChatDocumentUploadApi) {
|
|||||||
items.value.push(item);
|
items.value.push(item);
|
||||||
const trackedItem = items.value[items.value.length - 1];
|
const trackedItem = items.value[items.value.length - 1];
|
||||||
if (!trackedItem) continue;
|
if (!trackedItem) continue;
|
||||||
if (!SUPPORTED_EXTENSIONS.has(extension)) {
|
|
||||||
trackedItem.status = 'error';
|
|
||||||
trackedItem.error = '仅支持 PDF、Word、PPT、Excel、TXT、Markdown';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (file.size > maxBytes(extension)) {
|
if (file.size > maxBytes(extension)) {
|
||||||
trackedItem.status = 'error';
|
trackedItem.status = 'error';
|
||||||
trackedItem.error = sizeLimitMessage(extension);
|
trackedItem.error = sizeLimitMessage(extension);
|
||||||
@@ -239,7 +250,10 @@ export function createChatDocumentUploads(api: ChatDocumentUploadApi) {
|
|||||||
uploads.push(upload(trackedItem, context));
|
uploads.push(upload(trackedItem, context));
|
||||||
}
|
}
|
||||||
await Promise.all(uploads);
|
await Promise.all(uploads);
|
||||||
return rejectedCount;
|
return {
|
||||||
|
rejectedCount,
|
||||||
|
unsupportedCount,
|
||||||
|
} satisfies ChatDocumentAddFilesResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(item: ChatDocumentAttachment) {
|
async function remove(item: ChatDocumentAttachment) {
|
||||||
|
|||||||
Reference in New Issue
Block a user