perf: limit agent preset preview images
This commit is contained in:
parent
2eba3d8b5e
commit
33698bbc9c
@ -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]"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
{/* 渐变遮罩 */}
|
||||
|
||||
@ -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] }}
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
48
components/agents/agent-preset-preview.ts
Normal file
48
components/agents/agent-preset-preview.ts
Normal 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`;
|
||||
Loading…
x
Reference in New Issue
Block a user