fix safari direct upload feedback
This commit is contained in:
parent
91ce0ff5f8
commit
66cdf6d3d1
@ -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) => {
|
||||
|
||||
@ -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 配置。",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user