fix safari direct upload feedback
This commit is contained in:
parent
91ce0ff5f8
commit
66cdf6d3d1
@ -78,6 +78,8 @@ type UploadQueueItem = {
|
|||||||
status: UploadQueueStatus;
|
status: UploadQueueStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const UPLOAD_PROGRESS_HOLD_MS = 700;
|
||||||
|
|
||||||
function createUploadQueueId(file: File) {
|
function createUploadQueueId(file: File) {
|
||||||
return [
|
return [
|
||||||
file.name || "image",
|
file.name || "image",
|
||||||
@ -103,6 +105,12 @@ function getUploadQueueStatus(phase: UploadProgressPhase): UploadQueueStatus {
|
|||||||
return "uploading";
|
return "uploading";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUploadQueueStatusText(status: UploadQueueStatus) {
|
||||||
|
if (status === "preparing") return "正在处理图片";
|
||||||
|
if (status === "finalizing") return "正在保存图片";
|
||||||
|
return "正在上传图片";
|
||||||
|
}
|
||||||
|
|
||||||
function isVideoProviderType(model?: Partial<DynamicModel> | null) {
|
function isVideoProviderType(model?: Partial<DynamicModel> | null) {
|
||||||
return isVideoModelDescriptor({
|
return isVideoModelDescriptor({
|
||||||
providerType: model?.providerType,
|
providerType: model?.providerType,
|
||||||
@ -519,6 +527,36 @@ function PureMultimodalInput({
|
|||||||
setUploadQueue((current) => current.filter((item) => !idSet.has(item.id)));
|
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 () => {
|
const ensureImagePointsBeforeSend = useCallback(async () => {
|
||||||
if (!isImageModel || isVideoModel) return true;
|
if (!isImageModel || isVideoModel) return true;
|
||||||
if (membershipSummary?.isHighVip) return true;
|
if (membershipSummary?.isHighVip) return true;
|
||||||
@ -868,11 +906,11 @@ function PureMultimodalInput({
|
|||||||
delete next[slotIndex];
|
delete next[slotIndex];
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
removeUploadQueueItems([queueItem.id]);
|
await holdAndRemoveUploadQueueItems([queueItem.id]);
|
||||||
event.currentTarget.value = "";
|
event.currentTarget.value = "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[removeUploadQueueItems, updateAttachmentAtSlot, uploadFile]
|
[holdAndRemoveUploadQueueItems, updateAttachmentAtSlot, uploadFile]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 🆕 视频模式下的图片数量限制
|
// 🆕 视频模式下的图片数量限制
|
||||||
@ -999,13 +1037,13 @@ function PureMultimodalInput({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading files!", error);
|
console.error("Error uploading files!", error);
|
||||||
} finally {
|
} finally {
|
||||||
removeUploadQueueItems(queueItems.map((item) => item.id));
|
await holdAndRemoveUploadQueueItems(queueItems.map((item) => item.id));
|
||||||
if (fileInputRef.current) {
|
if (fileInputRef.current) {
|
||||||
fileInputRef.current.value = "";
|
fileInputRef.current.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[enforceVideoImageLimit, removeUploadQueueItems, setAttachments, uploadFile]
|
[enforceVideoImageLimit, holdAndRemoveUploadQueueItems, setAttachments, uploadFile]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePaste = useCallback(
|
const handlePaste = useCallback(
|
||||||
@ -1061,10 +1099,10 @@ function PureMultimodalInput({
|
|||||||
console.error("Error uploading pasted images:", error);
|
console.error("Error uploading pasted images:", error);
|
||||||
toast.error("粘贴的图片上传失败");
|
toast.error("粘贴的图片上传失败");
|
||||||
} finally {
|
} 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
|
// Add paste event listener to textarea
|
||||||
@ -1158,10 +1196,10 @@ function PureMultimodalInput({
|
|||||||
console.error("拖拽上传失败:", error);
|
console.error("拖拽上传失败:", error);
|
||||||
toast.error("拖拽的图片上传失败");
|
toast.error("拖拽的图片上传失败");
|
||||||
} finally {
|
} 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 && (
|
{videoFrameSlots.length > 0 && (
|
||||||
<div className="mb-2 flex flex-row gap-2">
|
<div className="mb-2 flex flex-row gap-2">
|
||||||
{videoFrameSlots.map((label, index) => {
|
{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) {
|
function normalizeXhrHeaders(headers?: HeadersInit) {
|
||||||
if (!headers) return [] as Array<[string, string]>;
|
if (!headers) return [] as Array<[string, string]>;
|
||||||
if (headers instanceof Headers) {
|
if (headers instanceof Headers) {
|
||||||
@ -658,7 +670,7 @@ async function fetchDirectUpload(
|
|||||||
emitUploadProgress(onProgress, file.blob.size, file.blob.size, "complete", {
|
emitUploadProgress(onProgress, file.blob.size, file.blob.size, "complete", {
|
||||||
percent: 100,
|
percent: 100,
|
||||||
});
|
});
|
||||||
return Response.json(payload);
|
return createJsonResponse(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
multipartUpload = upload;
|
multipartUpload = upload;
|
||||||
@ -690,7 +702,7 @@ async function fetchDirectUpload(
|
|||||||
emitUploadProgress(onProgress, file.blob.size, file.blob.size, "complete", {
|
emitUploadProgress(onProgress, file.blob.size, file.blob.size, "complete", {
|
||||||
percent: 100,
|
percent: 100,
|
||||||
});
|
});
|
||||||
return Response.json(payload);
|
return createJsonResponse(payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (multipartUpload && !isAbortError(error, signal)) {
|
if (multipartUpload && !isAbortError(error, signal)) {
|
||||||
postDirectUploadAction(
|
postDirectUploadAction(
|
||||||
@ -706,7 +718,7 @@ async function fetchDirectUpload(
|
|||||||
|
|
||||||
if (!isAbortError(error, signal)) {
|
if (!isAbortError(error, signal)) {
|
||||||
if (file.blob.size > SERVER_UPLOAD_MAX_BYTES) {
|
if (file.blob.size > SERVER_UPLOAD_MAX_BYTES) {
|
||||||
return Response.json(
|
return createJsonResponse(
|
||||||
{
|
{
|
||||||
error:
|
error:
|
||||||
"图片超过 50MB,必须使用浏览器直传;当前直传失败,请刷新后重试,或联系管理员检查存储桶 CORS/ETag 配置。",
|
"图片超过 50MB,必须使用浏览器直传;当前直传失败,请刷新后重试,或联系管理员检查存储桶 CORS/ETag 配置。",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user