perf: limit agent preset preview images

This commit is contained in:
Your Name 2026-05-28 19:05:25 +08:00
parent 2eba3d8b5e
commit 33698bbc9c
7 changed files with 115 additions and 17 deletions

View File

@ -4,6 +4,10 @@ import { Copy, Pencil, Sparkles, Trash2, Video } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import type { AgentPreset } from "@/types/agent";
import {
AGENT_PRESET_PREVIEW_IMAGE_SIZES,
getAgentPresetPreviewImageUrl,
} from "./agent-preset-preview";
interface AgentPresetCardProps {
preset: AgentPreset;
@ -22,11 +26,12 @@ export function AgentPresetCard({
onEdit,
onDelete,
}: AgentPresetCardProps) {
const previewUrl =
const rawPreviewUrl =
preset.avatarUrl ||
preset.referenceAssets?.[0]?.thumbnailUrl ||
preset.referenceAssets?.[0]?.originalUrl ||
"";
const previewUrl = getAgentPresetPreviewImageUrl(rawPreviewUrl);
const ownerName =
String(preset.owner?.name || "").trim() ||
String(preset.owner?.email || "").trim() ||
@ -45,7 +50,7 @@ export function AgentPresetCard({
return (
<div
className={cn(
"group overflow-hidden rounded-2xl border bg-background transition-all",
"group mx-auto w-full max-w-[230px] overflow-hidden rounded-2xl border bg-background transition-all",
selected
? "border-primary bg-primary/5 shadow-[0_0_0_1px_rgba(59,130,246,0.12)]"
: "border-border hover:border-primary/40 hover:shadow-sm"
@ -61,6 +66,10 @@ export function AgentPresetCard({
<img
src={previewUrl}
alt={preset.name}
loading="lazy"
decoding="async"
fetchPriority="low"
sizes={AGENT_PRESET_PREVIEW_IMAGE_SIZES}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
/>
) : (

View File

@ -8,6 +8,10 @@ import {
normalizeAgentPresetModeConfig,
} from "@/lib/agents/preset-mode-config";
import type { AgentPreset } from "@/types/agent";
import {
AGENT_PRESET_PREVIEW_IMAGE_SIZES,
getAgentPresetPreviewImageUrl,
} from "./agent-preset-preview";
interface AgentPresetCardDarkProps {
preset: AgentPreset;
@ -30,11 +34,12 @@ export function AgentPresetCardDark({
index = 0,
compact = false,
}: AgentPresetCardDarkProps) {
const previewUrl =
const rawPreviewUrl =
preset.avatarUrl ||
preset.referenceAssets?.[0]?.thumbnailUrl ||
preset.referenceAssets?.[0]?.originalUrl ||
"";
const previewUrl = getAgentPresetPreviewImageUrl(rawPreviewUrl);
const ownerName =
String(preset.owner?.name || "").trim() ||
@ -186,6 +191,10 @@ export function AgentPresetCardDark({
<img
src={previewUrl}
alt={preset.name}
loading="lazy"
decoding="async"
fetchPriority="low"
sizes="88px"
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
@ -260,8 +269,8 @@ export function AgentPresetCardDark({
whileHover={{ y: -8, scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={cn(
"group relative cursor-pointer",
"w-full aspect-[4/3]",
"group relative mx-auto cursor-pointer",
"aspect-[4/3] w-full max-w-[230px]",
"rounded-2xl overflow-hidden",
"bg-zinc-900",
"transition-all duration-500",
@ -278,6 +287,10 @@ export function AgentPresetCardDark({
<img
src={previewUrl}
alt={preset.name}
loading="lazy"
decoding="async"
fetchPriority="low"
sizes={AGENT_PRESET_PREVIEW_IMAGE_SIZES}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
{/* 渐变遮罩 */}

View File

@ -5,6 +5,10 @@ import { Copy, Pencil, Sparkles, Trash2, Video, Wand2, FileText } from "lucide-r
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
import type { AgentPreset } from "@/types/agent";
import {
AGENT_PRESET_PREVIEW_IMAGE_SIZES,
getAgentPresetPreviewImageUrl,
} from "./agent-preset-preview";
interface AgentPresetCardNewProps {
preset: AgentPreset;
@ -25,11 +29,12 @@ export function AgentPresetCardNew({
onDelete,
index = 0,
}: AgentPresetCardNewProps) {
const previewUrl =
const rawPreviewUrl =
preset.avatarUrl ||
preset.referenceAssets?.[0]?.thumbnailUrl ||
preset.referenceAssets?.[0]?.originalUrl ||
"";
const previewUrl = getAgentPresetPreviewImageUrl(rawPreviewUrl);
const ownerName =
String(preset.owner?.name || "").trim() ||
@ -67,7 +72,7 @@ export function AgentPresetCardNew({
}}
whileHover={{ y: -4 }}
className={cn(
"group relative overflow-hidden rounded-2xl bg-white transition-all duration-300",
"group relative mx-auto w-full max-w-[230px] overflow-hidden rounded-2xl bg-white transition-all duration-300",
"border border-slate-200/60 dark:border-slate-800/60",
"shadow-[0_0_0_1px_rgba(0,0,0,0.02),0_2px_8px_rgba(0,0,0,0.04)]",
"hover:shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_12px_32px_rgba(0,0,0,0.08)]",
@ -100,6 +105,10 @@ export function AgentPresetCardNew({
<motion.img
src={previewUrl}
alt={preset.name}
loading="lazy"
decoding="async"
fetchPriority="low"
sizes={AGENT_PRESET_PREVIEW_IMAGE_SIZES}
className="h-full w-full object-cover"
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] }}

View File

@ -29,6 +29,10 @@ import type {
AgentWorkflowStepKey,
AgentWorkflowUiConfig,
} from "@/types/agent";
import {
AGENT_PRESET_PREVIEW_IMAGE_SIZES,
getAgentPresetPreviewImageUrl,
} from "./agent-preset-preview";
export interface AgentWorkflowInputs {
primarySku: string;
@ -395,8 +399,9 @@ export function AgentWorkflowPanel({
</div>
<div className="grid grid-cols-1 gap-3 xl:grid-cols-2">
{step.options.map((option) => {
{step.options.map((option, optionIndex) => {
const selected = selections[step.key] === option.key;
const previewUrl = getAgentPresetPreviewImageUrl(option.previewImageUrl || "");
return (
<button
key={option.key}
@ -408,17 +413,21 @@ export function AgentWorkflowPanel({
})
}
className={cn(
"overflow-hidden rounded-2xl border text-left transition-all",
"mx-auto w-full max-w-[230px] overflow-hidden rounded-2xl border text-left transition-all",
selected
? "border-primary bg-primary/5 shadow-[0_0_0_1px_rgba(59,130,246,0.12)]"
: "border-border bg-background hover:border-primary/40 hover:shadow-sm"
)}
>
<div className="relative aspect-[4/3] w-full overflow-hidden bg-muted/40">
{option.previewImageUrl ? (
{previewUrl ? (
<img
src={option.previewImageUrl}
src={previewUrl}
alt={option.buttonName}
loading={optionIndex < 4 ? "eager" : "lazy"}
decoding="async"
fetchPriority={optionIndex < 4 ? "high" : "low"}
sizes={AGENT_PRESET_PREVIEW_IMAGE_SIZES}
className="h-full w-full object-cover"
/>
) : (

View File

@ -30,6 +30,10 @@ import type {
AgentWorkflowStepKey,
AgentWorkflowUiConfig,
} from "@/types/agent";
import {
AGENT_PRESET_PREVIEW_IMAGE_SIZES,
getAgentPresetPreviewImageUrl,
} from "./agent-preset-preview";
export interface AgentWorkflowInputs {
primarySku: string;
@ -91,6 +95,8 @@ function StepOptionCard({
onClick: () => void;
index: number;
}) {
const previewUrl = getAgentPresetPreviewImageUrl(option.previewImageUrl || "");
return (
<motion.button
type="button"
@ -102,7 +108,7 @@ function StepOptionCard({
whileTap={{ scale: 0.98 }}
className={cn(
"group relative overflow-hidden rounded-2xl text-left",
"aspect-[16/10] w-full",
"mx-auto aspect-[16/10] w-full max-w-[230px]",
"transition-all duration-500",
selected
? "ring-2 ring-white/30 shadow-[0_0_30px_rgba(255,255,255,0.1)]"
@ -111,11 +117,15 @@ function StepOptionCard({
>
{/* 背景 */}
<div className="absolute inset-0 bg-zinc-900">
{option.previewImageUrl ? (
{previewUrl ? (
<>
<img
src={option.previewImageUrl}
src={previewUrl}
alt={option.buttonName}
loading={index < 4 ? "eager" : "lazy"}
decoding="async"
fetchPriority={index < 4 ? "high" : "low"}
sizes={AGENT_PRESET_PREVIEW_IMAGE_SIZES}
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent" />

View File

@ -111,7 +111,7 @@ const VIRTUAL_OPTION_GRID_GAP = 16;
const VIRTUAL_OPTION_GRID_MAX_HEIGHT = 640;
const VIRTUAL_OPTION_GRID_MIN_CARD_HEIGHT = 122;
const VIRTUAL_OPTION_GRID_THRESHOLD = 12;
const WORKFLOW_OPTION_PREVIEW_WIDTH = 320;
const WORKFLOW_OPTION_PREVIEW_WIDTH = 230;
const WORKFLOW_OPTION_PREVIEW_QUALITY = 56;
const WORKFLOW_OPTION_PREVIEW_EAGER_COUNT = 2;
const WORKFLOW_OPTION_NEXT_PREVIEW_PREFETCH_COUNT = 3;
@ -443,7 +443,7 @@ const StepOptionCard = memo(function StepOptionCard({
onClick={handleClick}
className={cn(
"group relative overflow-hidden rounded-2xl text-left",
"aspect-[16/10] w-full",
"mx-auto aspect-[16/10] w-full max-w-[230px]",
"transition-all duration-500",
disabled && "cursor-not-allowed opacity-45 saturate-0",
selected
@ -467,7 +467,7 @@ const StepOptionCard = memo(function StepOptionCard({
fetchPriority={shouldLoadFast ? "high" : "low"}
referrerPolicy="no-referrer"
draggable={false}
sizes="(min-width: 1536px) 18vw, (min-width: 1280px) 22vw, (min-width: 1024px) 25vw, (min-width: 640px) 33vw, 50vw"
sizes="(max-width: 640px) 50vw, 230px"
onError={(event) =>
handleWorkflowOptionPreviewError(event, option.previewImageUrl)
}

View File

@ -0,0 +1,48 @@
const PRESET_PREVIEW_MAX_WIDTH = 230;
const QINIU_IMAGE_PROCESS_PARAM = `imageView2/2/w/${PRESET_PREVIEW_MAX_WIDTH}/q/80/format/webp`;
function isLikelyImageUrl(url: URL) {
return /\.(avif|gif|jpe?g|png|webp)$/i.test(url.pathname);
}
function hasImageProcessParams(url: URL) {
const search = url.search.toLowerCase();
return search.includes("x-oss-process=") || search.includes("imageview2/");
}
function appendRawQuery(url: URL, rawQuery: string) {
const separator = url.search ? "&" : "?";
return `${url.origin}${url.pathname}${url.search}${separator}${rawQuery}${url.hash}`;
}
export function getAgentPresetPreviewImageUrl(sourceUrl: string) {
const trimmed = sourceUrl.trim();
if (!trimmed) return "";
try {
const url = new URL(trimmed);
const host = url.hostname.toLowerCase();
if (!isLikelyImageUrl(url) || hasImageProcessParams(url)) {
return trimmed;
}
if (host.includes("aliyuncs.com") || host.includes("oss-")) {
const optimized = new URL(url.toString());
optimized.searchParams.set("x-oss-process", `image/resize,w_${PRESET_PREVIEW_MAX_WIDTH}/quality,q_80/format,webp`);
return optimized.toString();
}
if (host.includes("qiniu") || host.includes("clouddn") || host.includes("qiniucdn")) {
return appendRawQuery(url, QINIU_IMAGE_PROCESS_PARAM);
}
return trimmed;
} catch {
return trimmed;
}
}
export const AGENT_PRESET_PREVIEW_IMAGE_SIZES =
`(max-width: 640px) 50vw, ${PRESET_PREVIEW_MAX_WIDTH}px`;