fix safari direct upload feedback

This commit is contained in:
134 2026-07-08 19:39:10 +08:00
parent 91ce0ff5f8
commit 66cdf6d3d1
2 changed files with 89 additions and 11 deletions

View File

@ -78,6 +78,8 @@ type UploadQueueItem = {
status: UploadQueueStatus;
};
const UPLOAD_PROGRESS_HOLD_MS = 700;
function createUploadQueueId(file: File) {
return [
file.name || "image",
@ -103,6 +105,12 @@ function getUploadQueueStatus(phase: UploadProgressPhase): UploadQueueStatus {
return "uploading";
}
function getUploadQueueStatusText(status: UploadQueueStatus) {
if (status === "preparing") return "正在处理图片";
if (status === "finalizing") return "正在保存图片";
return "正在上传图片";
}
function isVideoProviderType(model?: Partial<DynamicModel> | null) {
return isVideoModelDescriptor({
providerType: model?.providerType,
@ -519,6 +527,36 @@ function PureMultimodalInput({
setUploadQueue((current) => current.filter((item) => !idSet.has(item.id)));
}, []);
const holdAndRemoveUploadQueueItems = useCallback(
async (ids: string[]) => {
if (ids.length === 0) return;
await new Promise<void>((resolve) => {
globalThis.setTimeout(resolve, UPLOAD_PROGRESS_HOLD_MS);
});
removeUploadQueueItems(ids);
},
[removeUploadQueueItems]
);
const uploadProgressSummary = useMemo(() => {
if (uploadQueue.length === 0) return null;
const totalProgress = Math.round(
uploadQueue.reduce((sum, item) => sum + item.progress, 0) /
uploadQueue.length
);
const activeItem =
uploadQueue.find((item) => item.progress < 100) ||
uploadQueue[uploadQueue.length - 1];
return {
count: uploadQueue.length,
fileName: activeItem?.name || "图片",
percent: Math.max(0, Math.min(100, totalProgress)),
statusText: getUploadQueueStatusText(activeItem?.status || "uploading"),
};
}, [uploadQueue]);
const ensureImagePointsBeforeSend = useCallback(async () => {
if (!isImageModel || isVideoModel) return true;
if (membershipSummary?.isHighVip) return true;
@ -868,11 +906,11 @@ function PureMultimodalInput({
delete next[slotIndex];
return next;
});
removeUploadQueueItems([queueItem.id]);
await holdAndRemoveUploadQueueItems([queueItem.id]);
event.currentTarget.value = "";
}
},
[removeUploadQueueItems, updateAttachmentAtSlot, uploadFile]
[holdAndRemoveUploadQueueItems, updateAttachmentAtSlot, uploadFile]
);
// 🆕 视频模式下的图片数量限制
@ -999,13 +1037,13 @@ function PureMultimodalInput({
} catch (error) {
console.error("Error uploading files!", error);
} finally {
removeUploadQueueItems(queueItems.map((item) => item.id));
await holdAndRemoveUploadQueueItems(queueItems.map((item) => item.id));
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
},
[enforceVideoImageLimit, removeUploadQueueItems, setAttachments, uploadFile]
[enforceVideoImageLimit, holdAndRemoveUploadQueueItems, setAttachments, uploadFile]
);
const handlePaste = useCallback(
@ -1061,10 +1099,10 @@ function PureMultimodalInput({
console.error("Error uploading pasted images:", error);
toast.error("粘贴的图片上传失败");
} finally {
removeUploadQueueItems(queueItems.map((item) => item.id));
await holdAndRemoveUploadQueueItems(queueItems.map((item) => item.id));
}
},
[enforceVideoImageLimit, removeUploadQueueItems, setAttachments, uploadFile]
[enforceVideoImageLimit, holdAndRemoveUploadQueueItems, setAttachments, uploadFile]
);
// Add paste event listener to textarea
@ -1158,10 +1196,10 @@ function PureMultimodalInput({
console.error("拖拽上传失败:", error);
toast.error("拖拽的图片上传失败");
} finally {
removeUploadQueueItems(queueItems.map((item) => item.id));
await holdAndRemoveUploadQueueItems(queueItems.map((item) => item.id));
}
},
[enforceVideoImageLimit, removeUploadQueueItems, setAttachments, uploadFile]
[enforceVideoImageLimit, holdAndRemoveUploadQueueItems, setAttachments, uploadFile]
);
// 🆕 拖拽排序处理函数
@ -1263,6 +1301,34 @@ function PureMultimodalInput({
}
}}
>
{uploadProgressSummary && (
<div
aria-live="polite"
className="mb-2 rounded-lg border border-primary/25 bg-primary/5 px-3 py-2"
data-testid="upload-progress-banner"
>
<div className="mb-1.5 flex items-center justify-between gap-3 text-xs">
<span className="min-w-0 truncate font-medium text-foreground">
{uploadProgressSummary.count > 1
? `正在上传 ${uploadProgressSummary.count} 张图片`
: uploadProgressSummary.statusText}
</span>
<span className="shrink-0 tabular-nums text-primary">
{uploadProgressSummary.percent}%
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-primary/15">
<div
className="h-full rounded-full bg-primary transition-[width] duration-200"
style={{ width: `${uploadProgressSummary.percent}%` }}
/>
</div>
<div className="mt-1 truncate text-[11px] text-muted-foreground">
{uploadProgressSummary.fileName}
</div>
</div>
)}
{videoFrameSlots.length > 0 && (
<div className="mb-2 flex flex-row gap-2">
{videoFrameSlots.map((label, index) => {

View File

@ -353,6 +353,18 @@ function createResponseFromXhr(xhr: XMLHttpRequest) {
});
}
function createJsonResponse(payload: unknown, init?: ResponseInit) {
const headers = new Headers(init?.headers);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
return new Response(JSON.stringify(payload), {
...init,
headers,
});
}
function normalizeXhrHeaders(headers?: HeadersInit) {
if (!headers) return [] as Array<[string, string]>;
if (headers instanceof Headers) {
@ -658,7 +670,7 @@ async function fetchDirectUpload(
emitUploadProgress(onProgress, file.blob.size, file.blob.size, "complete", {
percent: 100,
});
return Response.json(payload);
return createJsonResponse(payload);
}
multipartUpload = upload;
@ -690,7 +702,7 @@ async function fetchDirectUpload(
emitUploadProgress(onProgress, file.blob.size, file.blob.size, "complete", {
percent: 100,
});
return Response.json(payload);
return createJsonResponse(payload);
} catch (error) {
if (multipartUpload && !isAbortError(error, signal)) {
postDirectUploadAction(
@ -706,7 +718,7 @@ async function fetchDirectUpload(
if (!isAbortError(error, signal)) {
if (file.blob.size > SERVER_UPLOAD_MAX_BYTES) {
return Response.json(
return createJsonResponse(
{
error:
"图片超过 50MB,必须使用浏览器直传;当前直传失败,请刷新后重试,或联系管理员检查存储桶 CORS/ETag 配置。",