fix: preserve canvas image context and compact overlays

This commit is contained in:
134 2026-07-10 14:49:45 +08:00
parent c044ee2c40
commit 988e1fefed
26 changed files with 550 additions and 347 deletions

View File

@ -289,6 +289,7 @@ import {
} from "@/lib/video-agent/task-store";
import type { StoryboardModelCandidate } from "@/lib/video-agent/types";
import type { ChatMessage } from "@/lib/types";
import { collectMessageImageAttachments } from "@/lib/chat/message-image-attachments";
import type { AppUsage } from "@/lib/usage";
import { convertToUIMessages, generateUUID } from "@/lib/utils";
import { isAiModelAgentAuto } from "@/lib/db/ai-model-flags";
@ -1306,7 +1307,7 @@ export async function POST(request: Request) {
chatId: id,
role: "user",
parts: message.parts as any,
attachments: [],
attachments: collectMessageImageAttachments({ parts: message.parts }),
createdAt: new Date(),
videoStorageStatus: null,
temporaryVideoUrl: null,
@ -2889,7 +2890,7 @@ export async function POST(request: Request) {
id: message.id,
role: "user",
parts: message.parts as any,
attachments: [],
attachments: collectMessageImageAttachments({ parts: message.parts }),
createdAt: new Date(),
videoStorageStatus: null,
temporaryVideoUrl: null,

View File

@ -11434,60 +11434,60 @@ export default function CanvasEditorLegacyPage({
</DropdownMenuTrigger>
<DropdownMenuContent
align={shouldDockFloatingToolbarToSide ? "start" : "center"}
className="w-[420px] rounded-2xl border bg-background p-0 shadow-2xl"
className="w-[360px] max-w-[calc(100vw-24px)] rounded-[var(--ui-radius-panel)] border bg-background p-0 shadow-xl"
side={shouldDockFloatingToolbarToSide ? "right" : "top"}
>
<div className="border-b px-4 py-3">
<div className="font-semibold text-foreground text-sm">
<div className="border-b px-3 py-2.5">
<div className="text-[13px] font-semibold text-foreground">
视频工作流节点库
</div>
<div className="mt-1 text-muted-foreground text-xs">
<div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">
这里按参考 APP
的节点目录整理,先把结构补齐,再逐步接入具体能力。
</div>
</div>
<ScrollArea className="max-h-[60vh]">
<div className="space-y-4 p-3">
<div className="space-y-3 p-2">
{VIDEO_WORKFLOW_LIBRARY_SECTIONS.map((section) => {
const sectionNodes = getVideoWorkflowSectionNodes(
section.id
);
return (
<div className="space-y-2" key={section.id}>
<div className="px-2">
<div className="font-semibold text-muted-foreground text-xs uppercase tracking-[0.16em]">
<div className="space-y-1.5" key={section.id}>
<div className="px-1.5">
<div className="text-[10px] font-semibold uppercase text-muted-foreground">
{section.label}
</div>
<div className="mt-1 text-muted-foreground text-xs">
<div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">
{section.description}
</div>
</div>
<div className="grid gap-1">
{sectionNodes.map((nodeDefinition) => (
<DropdownMenuItem
className="flex items-start gap-3 rounded-xl px-3 py-3"
className="flex items-start gap-2 rounded-[var(--ui-radius-control)] px-2.5 py-2"
key={nodeDefinition.type}
onClick={() =>
handleAddWorkflowNode(nodeDefinition.type)
}
>
<div
className="mt-0.5 h-2.5 w-2.5 rounded-full"
className="mt-1 h-2 w-2 rounded-full"
style={{
backgroundColor:
nodeDefinition.accentColor,
}}
/>
<div className="flex-1">
<div className="flex items-center justify-between gap-3">
<span className="font-medium text-foreground text-sm">
<div className="flex items-center justify-between gap-2">
<span className="text-[13px] font-medium text-foreground">
{nodeDefinition.label}
</span>
<span className="font-mono text-[10px] text-muted-foreground">
{nodeDefinition.type}
</span>
</div>
<div className="mt-1 text-muted-foreground text-xs leading-5">
<div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">
{nodeDefinition.description}
</div>
</div>

View File

@ -52,30 +52,30 @@ export function AgentExecutionSnapshotDialog({
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
</div>
) : !snapshot ? (
<div className="rounded-xl border border-dashed p-6 text-sm text-muted-foreground">
<div className="rounded-[var(--ui-radius-control)] border border-dashed p-5 text-[13px] text-muted-foreground">
暂无快照详情
</div>
) : (
<div className="space-y-5">
<div className="space-y-4">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
<div className="rounded-xl border p-3">
<div className="text-xs text-muted-foreground">风格化</div>
<div className="rounded-[var(--ui-radius-control)] border p-3">
<div className="text-[11px] text-muted-foreground">风格化</div>
<div className="mt-1 font-medium">{snapshot.presetName}</div>
<div className="mt-1 text-sm text-muted-foreground">
<div className="mt-1 text-[13px] text-muted-foreground">
类型:{snapshot.presetType === "video" ? "视频" : "生图"}
</div>
<div className="text-sm text-muted-foreground">
分类:{snapshot.categoryName || "未分类"}
</div>
</div>
<div className="rounded-xl border p-3">
<div className="text-xs text-muted-foreground">模型</div>
<div className="rounded-[var(--ui-radius-control)] border p-3">
<div className="text-[11px] text-muted-foreground">模型</div>
<div className="mt-1 font-medium">
{snapshot.resolvedModel?.displayName ||
snapshot.defaultModel?.displayName ||
"未记录"}
</div>
<div className="mt-1 text-sm text-muted-foreground">
<div className="mt-1 text-[13px] text-muted-foreground">
槽位:{snapshot.slotCount} 个
</div>
<div className="text-sm text-muted-foreground">
@ -84,8 +84,8 @@ export function AgentExecutionSnapshotDialog({
</div>
</div>
<div className="rounded-xl border p-3">
<div className="mb-2 text-sm font-medium">本次使用的 3 张素材图</div>
<div className="rounded-[var(--ui-radius-control)] border p-3">
<div className="mb-2 text-[13px] font-medium">本次使用的 3 张素材图</div>
{uploadedImageUrls.length > 0 ? (
<div className="flex flex-wrap gap-3">
{uploadedImageUrls.map((url, index) => (
@ -107,18 +107,18 @@ export function AgentExecutionSnapshotDialog({
</div>
{snapshot.userInputText ? (
<div className="rounded-xl border p-3">
<div className="mb-2 text-sm font-medium">用户补充提示</div>
<div className="whitespace-pre-wrap text-sm text-muted-foreground">
<div className="rounded-[var(--ui-radius-control)] border p-3">
<div className="mb-2 text-[13px] font-medium">用户补充提示</div>
<div className="whitespace-pre-wrap text-[13px] text-muted-foreground">
{snapshot.userInputText}
</div>
</div>
) : null}
<div className="space-y-3">
<div className="text-sm font-medium">槽位详情</div>
<div className="text-[13px] font-medium">槽位详情</div>
{snapshot.slots.map((slot, index) => (
<div key={`snapshot-slot-${slot.slotIndex}-${index}`} className="rounded-xl border p-3">
<div key={`snapshot-slot-${slot.slotIndex}-${index}`} className="rounded-[var(--ui-radius-control)] border p-3">
<div className="mb-2 flex items-center justify-between">
<div className="font-medium">
{slot.title || slot.uploadLabel || `槽位 ${slot.slotIndex + 1}`}

View File

@ -189,7 +189,7 @@ export function AngleEditDialog({
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent
overlayClassName="bg-black"
className="max-w-4xl p-0 gap-0 overflow-hidden border border-border shadow-2xl !bg-white dark:!bg-zinc-950 sm:rounded-[20px]"
className="max-w-4xl gap-0 overflow-hidden border border-border !bg-white p-0 shadow-2xl sm:rounded-[var(--ui-radius-panel)] dark:!bg-zinc-950"
onPointerDownCapture={stopDialogEventCapture}
onPointerMoveCapture={stopDialogEventCapture}
onPointerUpCapture={stopDialogEventCapture}

View File

@ -63,7 +63,7 @@ function MaterialImageCard({
return (
<button
className={cn(
"group overflow-hidden rounded-xl border bg-background text-left transition-all",
"group overflow-hidden rounded-[var(--ui-radius-control)] border bg-background text-left transition-all",
selected
? "border-primary shadow-[0_0_0_1px_hsl(var(--primary)/0.25)]"
: "border-border hover:border-primary/45"
@ -91,11 +91,11 @@ function MaterialImageCard({
{selected ? <Check className="h-3.5 w-3.5" /> : null}
</div>
</div>
<div className="space-y-1 px-3 py-2.5">
<div className="truncate text-sm font-medium">
<div className="space-y-0.5 px-2.5 py-2">
<div className="truncate text-[13px] font-medium">
{image.label || image.fileName || "白底图"}
</div>
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<div className="flex items-center justify-between gap-2 text-[11px] text-muted-foreground">
<span>{image.group === "fixed" ? "固定角度" : "补充图"}</span>
<span className="truncate">{image.fileName || image.key}</span>
</div>
@ -114,7 +114,7 @@ function SkuOptionCard({
return (
<button
className={cn(
"flex items-center gap-3 rounded-xl border bg-background p-3 text-left transition-colors",
"flex items-center gap-2.5 rounded-[var(--ui-radius-control)] border bg-background p-2.5 text-left transition-colors",
option.hasWhiteBackground
? "hover:border-primary/55 hover:bg-muted/30"
: "cursor-not-allowed opacity-55"
@ -123,7 +123,7 @@ function SkuOptionCard({
onClick={() => onSelect(option.skuName)}
type="button"
>
<div className="h-14 w-14 overflow-hidden rounded-lg border bg-muted/40">
<div className="h-12 w-12 overflow-hidden rounded-[6px] border bg-muted/40">
{option.previewImageUrl ? (
<AgentPreviewImage
alt={option.previewFileName || option.skuName}
@ -139,8 +139,8 @@ function SkuOptionCard({
)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium">{option.skuName}</div>
<div className="mt-1 text-xs text-muted-foreground">
<div className="truncate text-[13px] font-medium">{option.skuName}</div>
<div className="mt-0.5 text-[11px] text-muted-foreground">
{option.hasWhiteBackground
? `${option.whiteBackgroundCount} 张白底图`
: "暂无白底图"}
@ -266,9 +266,9 @@ export function CanvasAddImageDialog({
return (
<Dialog onOpenChange={handleDialogOpenChange} open={open}>
<DialogContent className="max-h-[86vh] max-w-4xl overflow-hidden p-0">
<DialogHeader className="border-b px-6 pt-5 pb-4">
<DialogHeader className="border-b px-5 pb-3 pt-4">
<DialogTitle className="flex items-center gap-2">
<ImagePlus className="h-5 w-5" />
<ImagePlus className="h-4 w-4" />
添加图片
</DialogTitle>
<DialogDescription>
@ -276,11 +276,11 @@ export function CanvasAddImageDialog({
</DialogDescription>
</DialogHeader>
<div className="max-h-[62vh] overflow-y-auto px-6 py-5">
<div className="rounded-2xl border bg-muted/20 p-4">
<div className="flex flex-col gap-3 sm:flex-row">
<div className="max-h-[62vh] overflow-y-auto px-5 py-4">
<div className="rounded-[var(--ui-radius-panel)] border bg-muted/20 p-3">
<div className="flex flex-col gap-2.5 sm:flex-row">
<Button
className="h-11 justify-start gap-2 rounded-xl"
className="h-9 justify-start gap-2"
onClick={() => {
onCreateBlankImage();
closeDialog();
@ -293,14 +293,14 @@ export function CanvasAddImageDialog({
</Button>
{materialSearchEnabled ? (
<div className="flex min-w-0 flex-1 flex-col gap-3 sm:flex-row">
<div className="flex min-w-0 flex-1 flex-col gap-2.5 sm:flex-row">
<Select
onValueChange={(value) =>
setQueryMode(value === "sku" ? "sku" : "spu")
}
value={queryMode}
>
<SelectTrigger className="h-11 w-full rounded-xl sm:w-[118px]">
<SelectTrigger className="h-9 w-full sm:w-[108px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -310,7 +310,7 @@ export function CanvasAddImageDialog({
</Select>
<div className="relative min-w-0 flex-1">
<Input
className="h-11 rounded-xl pr-10"
className="h-9 pr-9"
onChange={(event) => {
setQuery(event.target.value);
setErrorText(null);
@ -338,7 +338,7 @@ export function CanvasAddImageDialog({
) : null}
</div>
<Button
className="h-11 gap-2 rounded-xl"
className="h-9 gap-2"
disabled={isSearching}
onClick={() => void handleSearch()}
type="button"
@ -354,14 +354,14 @@ export function CanvasAddImageDialog({
) : null}
</div>
{!materialSearchEnabled ? (
<div className="mt-3 rounded-xl border border-dashed bg-background/70 px-3 py-2 text-muted-foreground text-xs">
<div className="mt-2.5 rounded-[var(--ui-radius-control)] border border-dashed bg-background/70 px-3 py-2 text-[12px] text-muted-foreground">
风格化功能或素材搜索步骤未开启,当前只显示空白图片入口。
</div>
) : null}
</div>
{errorText ? (
<div className="mt-4 rounded-xl border border-destructive/20 bg-destructive/5 px-3 py-2 text-destructive text-sm">
<div className="mt-3 rounded-[var(--ui-radius-control)] border border-destructive/20 bg-destructive/5 px-3 py-2 text-[13px] text-destructive">
{errorText}
</div>
) : null}
@ -370,8 +370,8 @@ export function CanvasAddImageDialog({
<div className="mt-5 space-y-3">
<div className="flex items-center justify-between gap-3">
<div>
<div className="font-medium text-sm">选择 SKU</div>
<div className="text-muted-foreground text-xs">
<div className="text-[13px] font-medium">选择 SKU</div>
<div className="text-[11px] text-muted-foreground">
命中 {skuOptions.length} 个 SKU,选择有白底图的 SKU 继续拉取图片。
</div>
</div>
@ -392,11 +392,11 @@ export function CanvasAddImageDialog({
<div className="mt-5 space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="font-medium text-sm">
<div className="text-[13px] font-medium">
白底图素材
{result?.matchedSku ? ` · ${result.matchedSku}` : ""}
</div>
<div className="text-muted-foreground text-xs">
<div className="text-[11px] text-muted-foreground">
共 {images.length} 张,已选 {selectedImages.length} 张。
</div>
</div>
@ -432,13 +432,13 @@ export function CanvasAddImageDialog({
</div>
</div>
) : materialSearchEnabled && !isSearching && !skuOptions.length && result ? (
<div className="mt-5 flex min-h-[160px] items-center justify-center rounded-2xl border border-dashed text-muted-foreground text-sm">
<div className="mt-5 flex min-h-[160px] items-center justify-center rounded-[var(--ui-radius-panel)] border border-dashed text-[13px] text-muted-foreground">
当前没有可添加的白底图。
</div>
) : null}
</div>
<DialogFooter className="border-t px-6 py-4">
<DialogFooter className="border-t px-5 py-3">
<Button
onClick={closeDialog}
type="button"

View File

@ -261,6 +261,7 @@ import {
} from "@/components/canvas/chat-panel/canvas-chat-session";
import {
buildCanvasHistoryMessagesSignature,
collectMessageImageAttachments,
extractAssistantImageUrls,
extractCanvasModelName,
extractImageUrls,
@ -1721,20 +1722,20 @@ function CanvasAssistantTypingPlaceholder({
<div className="flex w-full justify-start">
<div className="flex max-w-[min(78%,680px)] flex-col items-start gap-2">
{hint ? (
<div className="rounded-[18px] bg-[#f4f5f7] px-4 py-3 text-[15px] leading-6 text-[#2f343d] shadow-sm dark:bg-white/8 dark:text-zinc-200">
<div className="rounded-[var(--ui-radius-panel)] bg-[#f4f5f7] px-3 py-2.5 text-[13px] leading-5 text-[#2f343d] shadow-sm dark:bg-white/8 dark:text-zinc-200">
{hint}
</div>
) : null}
<div
aria-label={label}
className="inline-flex h-8 items-center gap-2 rounded-[16px] bg-[#f4f5f7] px-3.5 text-[15px] font-medium text-[#9aa3b2] dark:bg-white/8 dark:text-zinc-400"
className="inline-flex h-7 items-center gap-1.5 rounded-[var(--ui-radius-control)] bg-[#f4f5f7] px-2.5 text-[12px] font-medium text-[#9aa3b2] dark:bg-white/8 dark:text-zinc-400"
>
<Loader2 className="h-4 w-4 animate-spin" />
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<span>{label}</span>
{onCancel ? (
<button
type="button"
className="-mr-1 ml-1 rounded-full px-2 py-0.5 text-[12px] font-semibold text-[#5d6675] transition-colors hover:bg-black/5 hover:text-[#242a33] dark:text-zinc-300 dark:hover:bg-white/10 dark:hover:text-white"
className="-mr-1 ml-1 rounded-[5px] px-1.5 py-0.5 text-[11px] font-semibold text-[#5d6675] transition-colors hover:bg-black/5 hover:text-[#242a33] dark:text-zinc-300 dark:hover:bg-white/10 dark:hover:text-white"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
@ -1772,13 +1773,13 @@ function CanvasComposerUploadProgressDialog({
return createPortal(
<div className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/35 px-4 backdrop-blur-[2px]">
<div className="w-full max-w-[360px] rounded-[22px] border border-white/12 bg-[#111215]/95 p-5 text-white shadow-2xl">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/10 text-[#e0ff3a]">
<Loader2 className="h-5 w-5 animate-spin" />
<div className="w-full max-w-[340px] rounded-[var(--ui-radius-panel)] border border-white/12 bg-[#111215]/95 p-4 text-white shadow-xl">
<div className="flex items-start gap-2.5">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-[var(--ui-radius-control)] bg-white/10 text-[#e0ff3a]">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
<div className="min-w-0 flex-1">
<div className="text-[15px] font-semibold leading-5">
<div className="text-[14px] font-semibold leading-5">
{progress.title}
</div>
{progress.fileName ? (
@ -5253,7 +5254,7 @@ export function CanvasChatPanel({
const getComposerModeMenuItemClass = useCallback(
(mode: CanvasComposerMode) =>
cn(
"h-11 cursor-pointer rounded-[12px] px-3 text-[16px] font-normal outline-none transition-colors focus:text-[#1f2937] dark:focus:text-white [&_svg]:!size-5",
"h-9 cursor-pointer rounded-[var(--ui-radius-control)] px-2.5 text-[13px] font-normal outline-none transition-colors focus:text-[#1f2937] dark:focus:text-white [&_svg]:!size-4",
isVideoToolbarDark
? "text-zinc-100 focus:bg-white/12 focus:text-white"
: "text-[#2e3238] focus:bg-[#eef0f3] dark:text-zinc-100 dark:focus:bg-white/12",
@ -11466,6 +11467,7 @@ export function CanvasChatPanel({
id: userMessageId,
role: "user",
parts,
attachments: effectiveAttachments,
createdAt: optimisticCreatedAt,
metadata: optimisticUserMetadata,
} as ChatMessage;
@ -12942,13 +12944,13 @@ export function CanvasChatPanel({
const renderModelSelectContent = () => (
<SelectContent
className={cn(
"w-[360px] max-w-[calc(100vw-32px)] max-h-[420px] overflow-hidden rounded-[22px] border border-[#cfd4dc] bg-white p-0 text-[#252b33] shadow-[0_18px_44px_rgba(15,23,42,0.16)]",
"max-h-[360px] w-[292px] max-w-[calc(100vw-24px)] overflow-hidden rounded-[var(--ui-radius-panel)] border border-[#cfd4dc] bg-white p-0 text-[#252b33] shadow-[0_14px_36px_rgba(15,23,42,0.14)]",
isVideoWorkspace
? "border border-border/60 bg-background/95 shadow-[0_14px_30px_-20px_rgba(15,23,42,0.35)] backdrop-blur-xl"
: "dark:border-white/10 dark:bg-zinc-900/95 dark:text-zinc-100"
)}
>
<div className="max-h-[392px] space-y-1 overflow-y-auto p-2">
<div className="max-h-[344px] space-y-0.5 overflow-y-auto p-1.5">
{selectableModels.map((model) => {
const discountLabel = getImageModelCreditDiscountLabel(
model.imageGenConfig
@ -12960,7 +12962,7 @@ export function CanvasChatPanel({
value={model.id}
disabled={model.selectable === false}
className={cn(
"h-12 rounded-[14px] px-4 text-[17px] font-medium [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full",
"h-9 rounded-[var(--ui-radius-control)] px-2.5 text-[13px] font-normal [&>span:first-child]:hidden [&>span:last-child]:block [&>span:last-child]:w-full",
isVideoWorkspace
? "text-foreground focus:bg-background focus:text-foreground data-[state=checked]:bg-background data-[state=checked]:text-foreground"
: "text-[#2f3640] focus:bg-[#f4f5f6] focus:text-[#1f2937] data-[state=checked]:bg-transparent data-[state=checked]:text-[#1f2937] dark:text-zinc-100 dark:focus:bg-white/10 dark:focus:text-white dark:data-[state=checked]:text-white",
@ -12968,10 +12970,10 @@ export function CanvasChatPanel({
)}
>
<span className="flex w-full items-center justify-between gap-2">
<span className="flex min-w-0 flex-1 items-center gap-3">
<span className="flex min-w-0 flex-1 items-center gap-2">
<span
className={cn(
"flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded-full leading-none",
"flex h-5 w-5 shrink-0 items-center justify-center overflow-hidden rounded-full leading-none",
isVideoWorkspace
? "border border-border/60 bg-background"
: "border border-transparent bg-white dark:border-white/20 dark:bg-white"
@ -12993,7 +12995,7 @@ export function CanvasChatPanel({
) : (
<span
className={cn(
"flex h-full w-full items-center justify-center text-[13px] font-medium leading-none",
"flex h-full w-full items-center justify-center text-[10px] font-medium leading-none",
isVideoWorkspace ? "text-muted-foreground" : "text-[#6b7280]"
)}
>
@ -13001,7 +13003,7 @@ export function CanvasChatPanel({
</span>
)}
</span>
<span className="min-w-0 flex-1 whitespace-nowrap text-[17px] font-medium leading-none tracking-[-0.01em]">
<span className="min-w-0 flex-1 truncate text-[13px] font-normal leading-5">
{model.displayName}
</span>
</span>
@ -13009,7 +13011,7 @@ export function CanvasChatPanel({
{discountLabel ? (
<span
className={cn(
"rounded-[10px] border px-2 py-0.5 text-[11px] font-semibold leading-none",
"rounded-[5px] border px-1.5 py-0.5 text-[10px] font-medium leading-none",
isVideoWorkspace
? "border-border/70 bg-muted/80 text-foreground/80"
: "border-[#d0d4dc] bg-[#eef1f5] text-[#5c6574] dark:border-white/15 dark:bg-white/10 dark:text-zinc-200"
@ -13021,7 +13023,7 @@ export function CanvasChatPanel({
{model.availabilityLabel ? (
<span
className={cn(
"rounded-[10px] border px-2 py-0.5 text-[11px] font-semibold leading-none",
"rounded-[5px] border px-1.5 py-0.5 text-[10px] font-medium leading-none",
model.availabilityStatus === "maintenance"
? isVideoWorkspace
? "border-amber-400/30 bg-amber-500/10 text-amber-300"
@ -13035,7 +13037,7 @@ export function CanvasChatPanel({
</span>
) : null}
{modelSelectValue === model.id ? (
<Check className={cn("h-5 w-5", isVideoWorkspace ? "text-foreground" : "text-[#1f2937]")} />
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-foreground" : "text-[#1f2937]")} />
) : null}
</span>
</span>
@ -13718,6 +13720,10 @@ export function CanvasChatPanel({
) : null}
{renderableVisibleMessages.map((message, index) => {
const userImageAttachments =
message.role === "user"
? collectMessageImageAttachments(message as any)
: [];
const assistantTextParts = message.role === "assistant"
? (message.parts || []).filter((part: any) => part.type === "text" && part.text)
: [];
@ -13840,43 +13846,37 @@ export function CanvasChatPanel({
{(() => {
const renderedUserParts: React.ReactNode[] = [];
message.parts?.forEach((part: any, idx: number) => {
const fileUrl =
typeof part?.url === "string" && part.url.trim()
? part.url
: typeof part?.data === "string" && part.data.trim()
? part.data
: "";
if (part.type === 'file' && fileUrl) {
renderedUserParts.push(
<span
key={`part-${idx}`}
className={cn(
"inline-flex items-center gap-1 rounded-xl px-2 py-1 text-xs",
isVideoWorkspace
? "border border-border/60 bg-background"
: "border border-black/5 bg-white/90 text-[#334155] dark:border-white/10 dark:bg-white/10 dark:text-zinc-100"
)}
>
<img
src={buildPreviewImageUrl(fileUrl, 48)}
alt=""
className="w-5 h-5 object-cover rounded"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== fileUrl) {
event.currentTarget.src = fileUrl;
}
}}
/>
<span className="max-w-[80px] truncate text-current">{part.name || 'Image'}</span>
userImageAttachments.forEach((attachment, idx) => {
renderedUserParts.push(
<span
key={`image-${attachment.url}-${idx}`}
className={cn(
"inline-flex items-center gap-1 rounded-[var(--ui-radius-control)] px-1.5 py-1 text-[11px]",
isVideoWorkspace
? "border border-border/60 bg-background"
: "border border-black/5 bg-white/90 text-[#334155] dark:border-white/10 dark:bg-white/10 dark:text-zinc-100"
)}
>
<img
src={buildPreviewImageUrl(attachment.url, 48)}
alt=""
className="h-4 w-4 rounded-[3px] object-cover"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== attachment.url) {
event.currentTarget.src = attachment.url;
}
}}
/>
<span className="max-w-[72px] truncate text-current">
{attachment.name || "图片"}
</span>
);
return;
}
</span>
);
});
message.parts?.forEach((part: any, idx: number) => {
if (part.type === 'text' && part.text) {
const text = String(part.text)
.replace(/【注意:图片中有标注(箭头、圆圈、文字等)标记的区域是需要重点关注或修改的部分,请根据标注进行处理】\n?/g, '')
@ -13888,35 +13888,6 @@ export function CanvasChatPanel({
}
});
if (
!message.parts?.some((p: any) => p.type === 'file') &&
(message as any).attachments?.length > 0
) {
(message as any).attachments.forEach((att: any, idx: number) => {
if (!att?.url) return;
renderedUserParts.push(
<span
key={`att-${idx}`}
className="inline-flex items-center gap-1 rounded-xl border border-black/5 bg-white/90 px-2 py-1 text-xs text-[#334155] dark:border-white/10 dark:bg-white/10 dark:text-zinc-100"
>
<img
src={buildPreviewImageUrl(att.url, 48)}
alt=""
className="w-5 h-5 object-cover rounded"
loading="lazy"
decoding="async"
onError={(event) => {
if (event.currentTarget.src !== att.url) {
event.currentTarget.src = att.url;
}
}}
/>
<span className="max-w-[80px] truncate text-current">{att.name || 'Image'}</span>
</span>
);
});
}
if (renderedUserParts.length === 0) {
const fallbackText =
extractUserTextPrompt(message as any) ||
@ -16006,7 +15977,7 @@ export function CanvasChatPanel({
side="top"
sideOffset={10}
className={cn(
"w-[184px] space-y-0.5 rounded-[12px] border border-[#d8dce3] bg-white p-1.5 text-[#20242a] shadow-[0_18px_44px_rgba(15,23,42,0.14)]",
"w-[164px] space-y-0.5 rounded-[var(--ui-radius-panel)] border border-[#d8dce3] bg-white p-1 text-[#20242a] shadow-[0_14px_36px_rgba(15,23,42,0.14)]",
isVideoToolbarDark
? "border-white/10 bg-zinc-900/95 text-zinc-100 shadow-[0_18px_44px_rgba(0,0,0,0.38)] backdrop-blur-xl"
: "dark:border-white/10 dark:bg-zinc-900/95 dark:text-zinc-100"

View File

@ -127,8 +127,8 @@ export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileList
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md max-h-[80vh] flex flex-col p-0 gap-0 overflow-hidden bg-background/95 backdrop-blur-sm">
<DialogHeader className="p-4 border-b">
<DialogContent className="flex max-h-[80vh] max-w-md flex-col gap-0 overflow-hidden bg-background/95 p-0 backdrop-blur-sm">
<DialogHeader className="border-b px-4 py-3">
<div className="flex items-center justify-between">
<DialogTitle>已生成文件列表</DialogTitle>
<Button size="sm" onClick={handleCreateNew} className="h-8">
@ -139,14 +139,14 @@ export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileList
<DialogDescription className="hidden">画布列表</DialogDescription>
</DialogHeader>
<ScrollArea className="flex-1 p-4">
<ScrollArea className="flex-1 p-3">
{isLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : projects.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
<FileImage className="h-12 w-12 mx-auto mb-3 opacity-20" />
<div className="py-10 text-center text-[13px] text-muted-foreground">
<FileImage className="mx-auto mb-2 h-8 w-8 opacity-20" />
<p>暂无画布历史</p>
</div>
) : (
@ -156,7 +156,7 @@ export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileList
key={project.id}
onClick={() => handleSwitch(project.id)}
className={cn(
"flex items-center gap-3 p-2 rounded-xl cursor-pointer transition-all border",
"group flex cursor-pointer items-center gap-2.5 rounded-[var(--ui-radius-control)] border p-2 transition-all",
"hover:bg-muted/50 hover:shadow-sm",
project.id === currentId
? "bg-primary/5 border-primary/20 ring-1 ring-primary/10"
@ -164,19 +164,19 @@ export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileList
)}
>
{/* 列表只展示轻量占位,不拉取未打开项目的云端图片 */}
<div className="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-lg border bg-muted/50">
<FileImage className="h-5 w-5 text-muted-foreground/40" />
<div className="flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-[6px] border bg-muted/50">
<FileImage className="h-4 w-4 text-muted-foreground/40" />
</div>
{/* 信息 */}
<div className="flex-1 min-w-0">
<h4 className={cn(
"font-medium text-sm truncate",
"truncate text-[13px] font-medium",
project.id === currentId && "text-primary"
)}>
{project.title || "未命名画布"}
</h4>
<p className="text-xs text-muted-foreground">
<p className="text-[11px] text-muted-foreground">
{formatDistanceToNow(new Date(project.updatedAt), {
addSuffix: true,
locale: zhCN
@ -188,7 +188,7 @@ export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileList
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100"
className="h-7 w-7 text-muted-foreground opacity-0 hover:text-destructive group-hover:opacity-100"
onClick={(e) => handleDelete(e, project.id)}
>
<Trash2 className="h-4 w-4" />
@ -210,7 +210,7 @@ export function CanvasFileList({ open, onOpenChange, currentId }: CanvasFileList
加载更多
</Button>
) : (
<div className="text-center py-4 text-xs text-muted-foreground/50">
<div className="py-3 text-center text-[11px] text-muted-foreground/50">
到底了
</div>
)}

View File

@ -34,24 +34,24 @@ export function CanvasImageDownloadProgressDialog({
className="fixed inset-0 z-[10000] flex items-center justify-center bg-black/42 px-4 backdrop-blur-[2px]"
data-canvas-download-progress="true"
>
<div className="w-full max-w-[380px] rounded-[18px] border border-white/12 bg-[#111317] p-5 text-white shadow-[0_24px_80px_rgba(0,0,0,0.42)]">
<div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-3">
<span className="flex h-10 w-10 items-center justify-center rounded-2xl bg-white/10 text-cyan-200">
<div className="w-full max-w-[360px] rounded-[var(--ui-radius-panel)] border border-white/12 bg-[#111317] p-4 text-white shadow-[0_20px_64px_rgba(0,0,0,0.4)]">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2.5">
<span className="flex h-9 w-9 items-center justify-center rounded-[var(--ui-radius-control)] bg-white/10 text-cyan-200">
{progress.phase === "saving" ? (
<Download className="h-5 w-5" />
<Download className="h-4 w-4" />
) : (
<Loader2 className="h-5 w-5 animate-spin" />
<Loader2 className="h-4 w-4 animate-spin" />
)}
</span>
<div>
<div className="text-[15px] font-semibold">下载原图</div>
<div className="text-[14px] font-semibold">下载原图</div>
<div className="mt-0.5 text-[12px] text-white/58">{progress.message}</div>
</div>
</div>
{onCancel ? (
<button
className="flex h-8 w-8 items-center justify-center rounded-full text-white/60 transition hover:bg-white/10 hover:text-white"
className="flex h-7 w-7 items-center justify-center rounded-[var(--ui-radius-control)] text-white/60 transition hover:bg-white/10 hover:text-white"
onClick={onCancel}
title="取消下载"
type="button"
@ -61,7 +61,7 @@ export function CanvasImageDownloadProgressDialog({
) : null}
</div>
<div className="mt-5">
<div className="mt-4">
<div className="mb-2 flex items-center justify-between text-[12px] text-white/55">
<span>{loadedLabel && totalLabel ? `${loadedLabel} / ${totalLabel}` : "正在处理文件"}</span>
<span>{percent !== null ? `${percent}%` : "..."}</span>

View File

@ -3599,7 +3599,7 @@ export function RealtimeCanvasDialog({
const primaryActionButtonClass =
"border border-[#111111] bg-[#111111] text-white shadow-[0_16px_30px_rgba(0,0,0,0.18)] transition hover:bg-black hover:border-black dark:border-white dark:bg-white dark:text-[#111111] dark:hover:border-white/90 dark:hover:bg-white/90";
const compactToolButtonClass =
"h-9 w-9 rounded-[16px] border border-transparent text-[#666666] transition hover:border-[#e5e5e5] hover:bg-[#fafafa] hover:text-[#111111] dark:text-white/58 dark:hover:border-white/12 dark:hover:bg-white/10 dark:hover:text-white";
"h-9 w-9 rounded-[var(--ui-radius-control)] border border-transparent text-[#666666] transition hover:border-[#e5e5e5] hover:bg-[#fafafa] hover:text-[#111111] dark:text-white/58 dark:hover:border-white/12 dark:hover:bg-white/10 dark:hover:text-white";
const compactToolButtonActiveClass =
"border-[#111111] bg-[#111111] text-white shadow-[0_10px_20px_rgba(0,0,0,0.12)] hover:bg-black hover:text-white dark:border-white dark:bg-white dark:text-[#111111] dark:hover:bg-white/90 dark:hover:text-[#111111]";
const modeSwitchIconSlotClass =
@ -3647,12 +3647,12 @@ export function RealtimeCanvasDialog({
<div className="pointer-events-none absolute top-5 left-5 right-5 z-50 flex flex-wrap items-start gap-2.5">
<div
className={cn(
"pointer-events-auto inline-flex max-w-full items-center gap-1.5 rounded-[28px] p-1.5",
"pointer-events-auto inline-flex max-w-full items-center gap-1.5 rounded-[var(--ui-radius-panel)] p-1.5",
floatingSurfaceClass
)}
>
<button
className="inline-flex h-10 shrink-0 items-center gap-2 rounded-[20px] border border-white/90 bg-white/78 px-3 text-left text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95)] transition hover:bg-white dark:border-white/12 dark:bg-white/8 dark:text-white dark:shadow-none dark:hover:bg-white/12"
className="inline-flex h-9 shrink-0 items-center gap-2 rounded-[var(--ui-radius-control)] border border-white/90 bg-white/78 px-2.5 text-left text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95)] transition hover:bg-white dark:border-white/12 dark:bg-white/8 dark:text-white dark:shadow-none dark:hover:bg-white/12"
onClick={handleCloseRealtime}
title={realtimeReturnCanvasItem.label}
type="button"
@ -3664,15 +3664,15 @@ export function RealtimeCanvasDialog({
item={realtimeReturnCanvasItem}
/>
</span>
<span className="font-semibold text-[13px] tracking-[-0.02em]">
<span className="text-[13px] font-semibold">
{realtimeReturnCanvasItem.label}
</span>
</button>
<div className="inline-flex h-10 min-w-0 items-center gap-0.5 rounded-[20px] border border-[#ebebeb] bg-[#fafafa]/90 p-0.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.9)] dark:border-white/12 dark:bg-white/8 dark:shadow-none">
<div className="inline-flex h-9 min-w-0 items-center gap-0.5 rounded-[var(--ui-radius-control)] border border-[#ebebeb] bg-[#fafafa]/90 p-0.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.9)] dark:border-white/12 dark:bg-white/8 dark:shadow-none">
<button
className={cn(
"group inline-flex h-full min-w-0 items-center gap-1.5 rounded-[17px] px-3 text-[13px] font-medium tracking-[-0.02em] transition",
"group inline-flex h-full min-w-0 items-center gap-1.5 rounded-[6px] px-2.5 text-[13px] font-medium transition",
workspaceMode === "realtime"
? softActionButtonClass
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111] dark:text-white/58 dark:hover:bg-white/10 dark:hover:text-white"
@ -3695,7 +3695,7 @@ export function RealtimeCanvasDialog({
{isEnhanceProEnabled ? (
<button
className={cn(
"group inline-flex h-full min-w-0 items-center gap-1.5 rounded-[17px] px-3 text-[13px] font-medium tracking-[-0.02em] transition",
"group inline-flex h-full min-w-0 items-center gap-1.5 rounded-[6px] px-2.5 text-[13px] font-medium transition",
workspaceMode === "enhance-pro"
? softActionButtonClass
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111] dark:text-white/58 dark:hover:bg-white/10 dark:hover:text-white"
@ -3719,7 +3719,7 @@ export function RealtimeCanvasDialog({
{isAiWashEnabled ? (
<button
className={cn(
"group inline-flex h-full min-w-0 items-center gap-1.5 rounded-[17px] px-3 text-[13px] font-medium tracking-[-0.02em] transition",
"group inline-flex h-full min-w-0 items-center gap-1.5 rounded-[6px] px-2.5 text-[13px] font-medium transition",
workspaceMode === "ai-wash"
? softActionButtonClass
: "border border-transparent bg-transparent text-[#666666] hover:bg-white hover:text-[#111111] dark:text-white/58 dark:hover:bg-white/10 dark:hover:text-white"
@ -3744,21 +3744,21 @@ export function RealtimeCanvasDialog({
{saveProjectDialogOpen ? (
<div
className={cn(
"-translate-x-1/2 absolute top-24 left-1/2 z-40 w-[22rem] max-w-[calc(100vw-2rem)] rounded-[24px] p-4",
"-translate-x-1/2 absolute top-24 left-1/2 z-40 w-[21rem] max-w-[calc(100vw-2rem)] rounded-[var(--ui-radius-panel)] p-3.5",
floatingSurfaceClass
)}
>
<div className="flex items-center justify-between">
<div>
<div className="font-semibold text-[#111111] text-sm dark:text-white">
<div className="text-[13px] font-semibold text-[#111111] dark:text-white">
保存项目
</div>
<div className="text-[#666666] text-xs dark:text-white/55">
<div className="text-[11px] text-[#666666] dark:text-white/55">
请输入项目名称后保存当前画布
</div>
</div>
<Button
className="h-8 w-8 rounded-full"
className="h-7 w-7"
onClick={() => setSaveProjectDialogOpen(false)}
size="icon"
type="button"
@ -3769,7 +3769,7 @@ export function RealtimeCanvasDialog({
</div>
<Input
autoFocus
className={cn("mt-4 h-11 rounded-2xl", controlSurfaceClass)}
className={cn("mt-3 h-9", controlSurfaceClass)}
onChange={(event) => setPendingProjectName(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
@ -3780,9 +3780,9 @@ export function RealtimeCanvasDialog({
placeholder="请输入项目名称"
value={pendingProjectName}
/>
<div className="mt-4 flex gap-2">
<div className="mt-3 flex gap-2">
<Button
className={cn("h-10 flex-1 rounded-2xl", controlSurfaceClass)}
className={cn("h-9 flex-1", controlSurfaceClass)}
onClick={() => setSaveProjectDialogOpen(false)}
type="button"
variant="outline"
@ -3791,7 +3791,7 @@ export function RealtimeCanvasDialog({
</Button>
<Button
className={cn(
"h-10 flex-1 rounded-2xl",
"h-9 flex-1",
primaryActionButtonClass
)}
onClick={() => void confirmSaveCurrentProject()}
@ -3805,21 +3805,21 @@ export function RealtimeCanvasDialog({
{projectPanelOpen ? (
<div
className={cn(
"absolute top-20 left-4 z-40 w-[22rem] max-w-[calc(100vw-2rem)] rounded-[24px] p-4",
"absolute top-20 left-4 z-40 w-[21rem] max-w-[calc(100vw-2rem)] rounded-[var(--ui-radius-panel)] p-3.5",
floatingSurfaceClass
)}
>
<div className="flex items-center justify-between">
<div>
<div className="font-semibold text-[#111111] text-sm dark:text-white">
<div className="text-[13px] font-semibold text-[#111111] dark:text-white">
项目列表
</div>
<div className="text-[#666666] text-xs dark:text-white/55">
<div className="text-[11px] text-[#666666] dark:text-white/55">
本地保存,便于下次继续编辑
</div>
</div>
<Button
className="h-8 w-8 rounded-full"
className="h-7 w-7"
onClick={() => setProjectPanelOpen(false)}
size="icon"
type="button"
@ -3828,19 +3828,19 @@ export function RealtimeCanvasDialog({
<X className="h-4 w-4" />
</Button>
</div>
<div className="mt-4 max-h-[55vh] min-h-0 space-y-2 overflow-y-auto overscroll-contain pr-1">
<div className="mt-3 max-h-[55vh] min-h-0 space-y-1.5 overflow-y-auto overscroll-contain pr-1">
{savedProjects.length === 0 ? (
<div className="rounded-2xl border border-[#e5e5e5] border-dashed px-4 py-8 text-center text-[#666666] text-sm">
<div className="rounded-[var(--ui-radius-control)] border border-[#e5e5e5] border-dashed px-4 py-7 text-center text-[13px] text-[#666666]">
暂无保存的项目
</div>
) : (
savedProjects.map((project) => (
<div
className="rounded-2xl border border-[#e5e5e5] bg-white p-3 shadow-sm"
className="rounded-[var(--ui-radius-control)] border border-[#e5e5e5] bg-white p-2.5 shadow-sm"
key={project.id}
>
<div className="flex items-center gap-3">
<div className="h-14 w-14 shrink-0 overflow-hidden rounded-xl border border-[#e5e5e5] bg-[#fafafa]">
<div className="h-12 w-12 shrink-0 overflow-hidden rounded-[6px] border border-[#e5e5e5] bg-[#fafafa]">
{project.coverImageUrl || project.previewUrl ? (
<img
alt={project.name}
@ -3866,13 +3866,13 @@ export function RealtimeCanvasDialog({
)}
</div>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-[#111111] text-sm leading-5">
<div className="truncate text-[13px] font-medium leading-5 text-[#111111]">
{project.name}
</div>
<div className="mt-2 flex items-center justify-end gap-2">
<Button
className={cn(
"h-8 rounded-xl px-4",
"h-8 px-3",
softActionButtonClass
)}
onClick={() => void openSavedProject(project.id)}
@ -3883,7 +3883,7 @@ export function RealtimeCanvasDialog({
</Button>
<Button
className={cn(
"h-8 rounded-xl px-4",
"h-8 px-3",
controlSurfaceClass
)}
onClick={() => deleteSavedProject(project.id)}
@ -3950,7 +3950,7 @@ export function RealtimeCanvasDialog({
>
<div className="pattern-grid-lg pointer-events-none absolute inset-0 opacity-[0.03]" />
{isDragImportActive ? (
<div className="absolute inset-6 z-20 flex items-center justify-center rounded-[28px] border-2 border-[#111111] border-dashed bg-white/92 font-medium text-[#111111] text-sm shadow-inner dark:border-white/70 dark:bg-[#111317]/92 dark:text-white">
<div className="absolute inset-6 z-20 flex items-center justify-center rounded-[var(--ui-radius-panel)] border-2 border-[#111111] border-dashed bg-white/92 text-[13px] font-medium text-[#111111] shadow-inner dark:border-white/70 dark:bg-[#111317]/92 dark:text-white">
松开鼠标,上传图片到图层
</div>
) : null}
@ -4048,7 +4048,7 @@ export function RealtimeCanvasDialog({
</div>
<div
className={cn(
"absolute top-28 bottom-4 left-4 z-10 flex w-16 max-w-[18vw] flex-col rounded-[24px] p-2",
"absolute top-28 bottom-4 left-4 z-10 flex w-14 max-w-[18vw] flex-col rounded-[var(--ui-radius-panel)] p-1.5",
floatingSurfaceClass
)}
>
@ -4066,14 +4066,14 @@ export function RealtimeCanvasDialog({
</div>
<div className="mt-2 flex flex-1 flex-col items-center space-y-2 overflow-y-auto py-1">
{imageLayers.length === 0 ? (
<div className="rounded-2xl border border-[#e5e5e5] border-dashed px-1 py-4 text-center text-[10px] text-[#666666] leading-4 dark:border-white/12 dark:text-white/55">
<div className="rounded-[var(--ui-radius-control)] border border-[#e5e5e5] border-dashed px-1 py-4 text-center text-[10px] leading-4 text-[#666666] dark:border-white/12 dark:text-white/55">
暂无图层
</div>
) : (
[...imageLayers].reverse().map((layer) => (
<button
className={cn(
"flex h-12 w-12 shrink-0 items-center justify-center rounded-[18px] p-1.5 transition hover:bg-[#f5f5f5] dark:hover:bg-white/10",
"flex h-10 w-10 shrink-0 items-center justify-center rounded-[6px] p-1 transition hover:bg-[#f5f5f5] dark:hover:bg-white/10",
selectedLayerId === layer.id
? "bg-[#f5f5f5] shadow-[inset_0_0_0_1px_rgba(0,0,0,0.08)] dark:bg-white/10 dark:shadow-[inset_0_0_0_1px_rgba(255,255,255,0.14)]"
: ""
@ -4224,7 +4224,7 @@ export function RealtimeCanvasDialog({
<TooltipProvider delayDuration={120}>
<div
className={cn(
"pointer-events-auto flex flex-wrap items-center justify-center gap-1 rounded-[24px] p-1.5",
"pointer-events-auto flex flex-wrap items-center justify-center gap-1 rounded-[var(--ui-radius-panel)] p-1.5",
floatingSurfaceClass
)}
>
@ -4250,10 +4250,10 @@ export function RealtimeCanvasDialog({
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeSelectToolItem.label}
</div>
<div className="text-[#666666] text-xs">
<div className="text-[10px] text-[#666666]">
选择图层并拖动位置
</div>
</TooltipContent>
@ -4288,18 +4288,18 @@ export function RealtimeCanvasDialog({
</TooltipTrigger>
<PopoverContent
className={cn(
"w-[250px] rounded-[18px] p-4 backdrop-blur-md",
"w-[224px] rounded-[var(--ui-radius-panel)] p-3 backdrop-blur-md",
floatingSurfaceClass
)}
>
<div className="space-y-4">
<div className="space-y-2">
<div className="space-y-3">
<div className="space-y-1.5">
<div className="font-medium text-[#666666] text-xs">
颜色
</div>
<button
className={cn(
"flex h-10 w-full items-center justify-between rounded-xl px-3 font-medium text-sm",
"flex h-9 w-full items-center justify-between rounded-[var(--ui-radius-control)] px-2.5 text-[13px] font-medium",
controlSurfaceClass
)}
onClick={() => colorInputRef.current?.click()}
@ -4345,10 +4345,10 @@ export function RealtimeCanvasDialog({
</PopoverContent>
</Popover>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeBrushToolItem.label}
</div>
<div className="text-[#666666] text-xs">
<div className="text-[10px] text-[#666666]">
点击后直接调整颜色和粗细
</div>
</TooltipContent>
@ -4383,7 +4383,7 @@ export function RealtimeCanvasDialog({
</TooltipTrigger>
<PopoverContent
className={cn(
"w-[220px] rounded-[18px] p-4 backdrop-blur-md",
"w-[208px] rounded-[var(--ui-radius-panel)] p-3 backdrop-blur-md",
floatingSurfaceClass
)}
>
@ -4409,10 +4409,10 @@ export function RealtimeCanvasDialog({
</PopoverContent>
</Popover>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeEraserToolItem.label}
</div>
<div className="text-[#666666] text-xs">
<div className="text-[10px] text-[#666666]">
点击后直接调整橡皮粗细
</div>
</TooltipContent>
@ -4443,7 +4443,7 @@ export function RealtimeCanvasDialog({
</TooltipTrigger>
<PopoverContent
className={cn(
"w-[220px] rounded-[18px] p-3 backdrop-blur-md",
"w-[208px] rounded-[var(--ui-radius-panel)] p-2.5 backdrop-blur-md",
floatingSurfaceClass
)}
>
@ -4453,7 +4453,7 @@ export function RealtimeCanvasDialog({
<div className="grid grid-cols-3 gap-2">
<Button
className={cn(
"h-10 rounded-xl border",
"h-9 rounded-[var(--ui-radius-control)] border",
brushMode === "rect"
? softActionButtonClass
: "border-transparent text-[#666666] hover:bg-[#f5f5f5] hover:text-[#111111]"
@ -4466,7 +4466,7 @@ export function RealtimeCanvasDialog({
</Button>
<Button
className={cn(
"h-10 rounded-xl border",
"h-9 rounded-[var(--ui-radius-control)] border",
brushMode === "circle"
? softActionButtonClass
: "border-transparent text-[#666666] hover:bg-[#f5f5f5] hover:text-[#111111]"
@ -4479,7 +4479,7 @@ export function RealtimeCanvasDialog({
</Button>
<Button
className={cn(
"h-10 rounded-xl border",
"h-9 rounded-[var(--ui-radius-control)] border",
brushMode === "line"
? softActionButtonClass
: "border-transparent text-[#666666] hover:bg-[#f5f5f5] hover:text-[#111111]"
@ -4494,10 +4494,10 @@ export function RealtimeCanvasDialog({
</PopoverContent>
</Popover>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeShapeToolItem.label}
</div>
<div className="text-[#666666] text-xs">
<div className="text-[10px] text-[#666666]">
矩形、圆形与直线
</div>
</TooltipContent>
@ -4521,10 +4521,10 @@ export function RealtimeCanvasDialog({
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeSaveProjectItem.label}
</div>
<div className="text-[#666666] text-xs">
<div className="text-[10px] text-[#666666]">
保存当前画布状态
</div>
</TooltipContent>
@ -4550,10 +4550,10 @@ export function RealtimeCanvasDialog({
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeProjectListItem.label}
</div>
<div className="text-[#666666] text-xs">打开已保存项目</div>
<div className="text-[10px] text-[#666666]">打开已保存项目</div>
</TooltipContent>
</Tooltip>
<Tooltip>
@ -4574,10 +4574,10 @@ export function RealtimeCanvasDialog({
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeNewBlankItem.label}
</div>
<div className="text-[#666666] text-xs">
<div className="text-[10px] text-[#666666]">
清空并开始新项目
</div>
</TooltipContent>
@ -4602,10 +4602,10 @@ export function RealtimeCanvasDialog({
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeUndoItem.label}
</div>
<div className="text-[#666666] text-xs">回退上一步编辑</div>
<div className="text-[10px] text-[#666666]">回退上一步编辑</div>
</TooltipContent>
</Tooltip>
<Tooltip>
@ -4627,10 +4627,10 @@ export function RealtimeCanvasDialog({
</Button>
</TooltipTrigger>
<TooltipContent>
<div className="font-medium text-sm">
<div className="text-[12px] font-medium">
{realtimeRedoItem.label}
</div>
<div className="text-[#666666] text-xs">恢复已撤销操作</div>
<div className="text-[10px] text-[#666666]">恢复已撤销操作</div>
</TooltipContent>
</Tooltip>
</div>
@ -4638,12 +4638,12 @@ export function RealtimeCanvasDialog({
<div
className={cn(
"pointer-events-auto w-full rounded-[24px] p-4",
"pointer-events-auto w-full rounded-[var(--ui-radius-panel)] p-3",
floatingSurfaceClass
)}
>
<Textarea
className="min-h-[56px] resize-none border-0 bg-transparent px-1 py-0.5 text-[15px] text-[#111111] leading-7 caret-[#111111] shadow-none placeholder:text-[#9b9b9b] focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white dark:caret-white dark:placeholder:text-white/45"
className="min-h-[48px] resize-none border-0 bg-transparent px-1 py-0.5 text-[13px] leading-6 text-[#111111] caret-[#111111] shadow-none placeholder:text-[#9b9b9b] focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white dark:caret-white dark:placeholder:text-white/45"
onBlur={() => scheduleFlush(true, false)}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(event) => {
@ -4699,7 +4699,7 @@ export function RealtimeCanvasDialog({
</PopoverTrigger>
<PopoverContent
align="center"
className="w-auto rounded-2xl border border-[#e5e5e5] bg-white p-1 shadow-[0_16px_36px_rgba(0,0,0,0.12)] dark:border-white/12 dark:bg-[#171717]"
className="w-auto rounded-[var(--ui-radius-panel)] border border-[#e5e5e5] bg-white p-1 shadow-[0_12px_28px_rgba(0,0,0,0.12)] dark:border-white/12 dark:bg-[#171717]"
side="top"
sideOffset={8}
>
@ -4709,7 +4709,7 @@ export function RealtimeCanvasDialog({
return (
<button
className={cn(
"h-8 rounded-full px-3 font-medium text-xs transition",
"h-8 rounded-[var(--ui-radius-control)] px-2.5 text-[12px] font-medium transition",
active
? "bg-[#111111] text-white dark:bg-white dark:text-[#111111]"
: "text-[#666666] hover:bg-[#f4f4f4] dark:text-white/58 dark:hover:bg-white/10"
@ -4812,7 +4812,7 @@ export function RealtimeCanvasDialog({
return (
<Dialog onOpenChange={onOpenChange} open={open}>
<DialogContent className="h-[97vh] w-[98vw] max-w-[1800px] overflow-hidden rounded-[20px] border-0 bg-white p-0 text-[#111111] shadow-2xl dark:bg-[#050608] dark:text-white sm:rounded-[20px] md:rounded-[24px]">
<DialogContent className="h-[97vh] w-[98vw] max-w-[1800px] overflow-hidden rounded-[var(--ui-radius-panel)] border-0 bg-white p-0 text-[#111111] shadow-2xl dark:bg-[#050608] dark:text-white">
{content}
</DialogContent>
</Dialog>

View File

@ -3,8 +3,11 @@ import {
extractOriginalImageUrlFromAlt,
inferOriginalImageUrl,
} from "@/lib/image-url";
import { getMessageImagePartUrl } from "@/lib/chat/message-image-attachments";
import type { ChatMessage } from "@/lib/types";
export { collectMessageImageAttachments } from "@/lib/chat/message-image-attachments";
export function extractImageUrls(text: string): string[] {
if (!text) return [];
return extractImageUrlsFromText(text);
@ -108,12 +111,7 @@ export function collectMessageImageUrlKeys(message: ChatMessage | undefined): Se
const parts = (message as any).parts || [];
for (const part of parts) {
if (!part) continue;
if (
(part.type === "file" || part.type === "image" || part.type === "input_image") &&
(typeof part.url === "string" || typeof part.data === "string")
) {
pushUrl(typeof part.url === "string" ? part.url : part.data);
}
pushUrl(getMessageImagePartUrl(part));
if (part.type === "text" && typeof part.text === "string") {
extractImageUrls(part.text).forEach((url) => pushUrl(url));
@ -146,11 +144,8 @@ export function collectMessageImageUrls(message: ChatMessage | undefined): strin
const parts = (message as any).parts || [];
for (const part of parts) {
if (!part) continue;
if (
(part.type === "file" || part.type === "image" || part.type === "input_image") &&
(typeof part.url === "string" || typeof part.data === "string")
) {
const direct = typeof part.url === "string" ? part.url : part.data;
const direct = getMessageImagePartUrl(part);
if (direct) {
pushUrl(inferOriginalImageUrl(String(direct || "").trim()));
}

View File

@ -9967,7 +9967,7 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
<div
className="fixed z-[10000] overflow-hidden rounded-[14px] border border-slate-200 bg-white text-slate-950 shadow-[0_18px_44px_rgba(15,23,42,0.18)] dark:border-white/12 dark:bg-slate-950 dark:text-white"
className="fixed z-[10000] overflow-hidden rounded-[var(--ui-radius-panel)] border border-slate-200 bg-white text-slate-950 shadow-[0_14px_36px_rgba(15,23,42,0.16)] dark:border-white/12 dark:bg-slate-950 dark:text-white"
data-canvas-no-drag="true"
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
@ -9981,21 +9981,21 @@ export function ImageNode({
scrollbarWidth: "none",
}}
>
<div className="flex items-center justify-between gap-3 border-b border-slate-100 px-4 py-3 dark:border-white/10">
<div className="flex min-w-0 items-center gap-3">
<span className="flex h-6 w-6 shrink-0 items-center justify-center text-slate-950 dark:text-white">
<div className="flex items-center justify-between gap-2 border-b border-slate-100 px-3 py-2.5 dark:border-white/10">
<div className="flex min-w-0 items-center gap-2">
<span className="flex h-5 w-5 shrink-0 items-center justify-center text-slate-950 dark:text-white">
<CanvasToolbarIcon
className="h-5 w-5"
className="h-4 w-4"
fallback={Sparkles}
iconUrl={aiWashQuickConfig.modeIconUrl}
/>
</span>
<div className="min-w-0 truncate text-[17px] font-semibold tracking-[-0.02em]">
<div className="min-w-0 truncate text-[14px] font-semibold">
{aiWashQuickModeLabel}
</div>
</div>
<button
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-950 dark:text-white/54 dark:hover:bg-white/10 dark:hover:text-white"
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[var(--ui-radius-control)] text-slate-400 transition hover:bg-slate-100 hover:text-slate-950 dark:text-white/54 dark:hover:bg-white/10 dark:hover:text-white"
onClick={() => setAiWashQuickPanelOpen(false)}
title="关闭"
type="button"
@ -10005,20 +10005,20 @@ export function ImageNode({
</div>
{aiWashConfigLoading ? (
<div className="flex items-center gap-2 border-b border-slate-100 px-4 py-3 text-[13px] text-slate-500 dark:border-white/10 dark:text-white/58">
<div className="flex items-center gap-2 border-b border-slate-100 px-3 py-2.5 text-[12px] text-slate-500 dark:border-white/10 dark:text-white/58">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
正在加载 AI洗图 配置...
</div>
) : null}
<div className="divide-y divide-slate-100 dark:divide-white/10">
<div className="px-4 py-3">
<div className="mb-2 flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-[14px] font-semibold text-slate-950 dark:text-white">
<div className="px-3 py-2.5">
<div className="mb-1.5 flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-[13px] font-semibold text-slate-950 dark:text-white">
<SlidersHorizontal className="h-4 w-4" />
目标
</div>
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/8 dark:text-white/58">
<span className="rounded-[5px] border border-slate-200 bg-white px-1.5 py-0.5 text-[10px] text-slate-500 dark:border-white/10 dark:bg-white/8 dark:text-white/58">
{getAiWashStatusLabel(aiWashQuickStatus)}
</span>
</div>
@ -10029,7 +10029,7 @@ export function ImageNode({
}}
value={selectedAiWashQuickPreset?.id || aiWashQuickPresetId}
>
<SelectTrigger className="h-10 rounded-[10px] border-slate-200 bg-white px-3 text-[14px] shadow-none dark:border-white/10 dark:bg-white/8">
<SelectTrigger className="h-9 border-slate-200 bg-white px-2.5 text-[13px] shadow-none dark:border-white/10 dark:bg-white/8">
<SelectValue placeholder="选择渲染目标" />
</SelectTrigger>
<SelectContent className="border-slate-200 bg-white text-slate-950 dark:border-white/10 dark:bg-slate-950 dark:text-white">
@ -10047,8 +10047,8 @@ export function ImageNode({
) : null}
</div>
<div className="px-4 py-3">
<div className="mb-2 text-[14px] font-semibold text-slate-950 dark:text-white">
<div className="px-3 py-2.5">
<div className="mb-1.5 text-[13px] font-semibold text-slate-950 dark:text-white">
参数
</div>
{shouldShowAiWashQuickModelSelector ? (
@ -10060,7 +10060,7 @@ export function ImageNode({
onValueChange={setAiWashQuickModelId}
value={selectedAiWashQuickModel?.id || aiWashQuickModelId}
>
<SelectTrigger className="h-10 rounded-[10px] border-slate-200 bg-white px-3 text-[14px] shadow-none dark:border-white/10 dark:bg-white/8">
<SelectTrigger className="h-9 border-slate-200 bg-white px-2.5 text-[13px] shadow-none dark:border-white/10 dark:bg-white/8">
<SelectValue placeholder="选择生图模型" />
</SelectTrigger>
<SelectContent className="border-slate-200 bg-white text-slate-950 dark:border-white/10 dark:bg-slate-950 dark:text-white">
@ -10097,7 +10097,7 @@ export function ImageNode({
</div>
</div>
) : selectedAiWashQuickModel ? (
<div className="mb-3 rounded-[10px] border border-slate-200 bg-slate-50 px-3 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/52">
<div className="mb-3 rounded-[var(--ui-radius-control)] border border-slate-200 bg-slate-50 px-2.5 py-2 text-[12px] text-slate-500 dark:border-white/10 dark:bg-white/6 dark:text-white/52">
{selectedAiWashQuickModel.displayName}
{selectedAiWashQuickModel.unitCost > 0
? ` · ${selectedAiWashQuickModel.unitCost} 积分/张`
@ -10105,7 +10105,7 @@ export function ImageNode({
</div>
) : null}
<div className="-mx-4 divide-y divide-slate-100 dark:divide-white/10">
<div className="-mx-3 divide-y divide-slate-100 dark:divide-white/10">
{aiWashQuickControls.map((control) => {
const selectedIndex =
aiWashQuickLevels[control.id] ?? control.defaultLevel;
@ -10115,14 +10115,14 @@ export function ImageNode({
null;
return (
<div
className="px-4 py-3"
className="px-3 py-2.5"
key={control.id}
>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="text-[14px] font-semibold">
<div className="mb-1.5 flex items-center justify-between gap-2">
<div className="text-[13px] font-semibold">
{control.label}
</div>
<span className="min-w-8 rounded-full border border-slate-200 bg-white px-2 py-0.5 text-center text-[12px] text-slate-600 dark:border-white/10 dark:bg-white/8 dark:text-white/64">
<span className="min-w-7 rounded-[5px] border border-slate-200 bg-white px-1.5 py-0.5 text-center text-[10px] text-slate-600 dark:border-white/10 dark:bg-white/8 dark:text-white/64">
{activeLevel?.label || selectedIndex + 1}
</span>
</div>
@ -10148,9 +10148,9 @@ export function ImageNode({
</div>
</div>
<div className="px-3 py-3">
<div className="px-3 py-2.5">
<Button
className="h-10 w-full rounded-[10px] bg-slate-950 text-[14px] font-semibold text-white shadow-none hover:bg-slate-900 hover:text-white disabled:bg-slate-100 disabled:text-slate-300 disabled:shadow-none dark:bg-white dark:text-slate-950 dark:hover:bg-slate-100 dark:disabled:bg-white/12 dark:disabled:text-white/30"
className="h-9 w-full bg-slate-950 text-[13px] font-semibold text-white shadow-none hover:bg-slate-900 hover:text-white disabled:bg-slate-100 disabled:text-slate-300 disabled:shadow-none dark:bg-white dark:text-slate-950 dark:hover:bg-slate-100 dark:disabled:bg-white/12 dark:disabled:text-white/30"
disabled={
isSubmittingAiWashQuick ||
!selectedAiWashQuickPreset ||
@ -11682,7 +11682,7 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
<div
className="fixed z-[9999] w-[360px] rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-zinc-800 dark:bg-zinc-950"
className="fixed z-[9999] w-[340px] max-w-[calc(100vw-24px)] rounded-[var(--ui-radius-panel)] border border-slate-200 bg-white shadow-xl dark:border-zinc-800 dark:bg-zinc-950"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
@ -11694,7 +11694,7 @@ export function ImageNode({
<div className="flex items-center justify-between border-border/70 border-b px-4 py-3">
<div className="flex items-center gap-2">
<Type className="h-4 w-4 text-primary" />
<span className="font-medium text-sm">编辑文字</span>
<span className="text-[13px] font-medium">编辑文字</span>
</div>
<Button
className="h-7 w-7"
@ -11707,12 +11707,12 @@ export function ImageNode({
</div>
<div className="max-h-[360px] space-y-3 overflow-y-auto px-4 py-3">
<p className="text-muted-foreground text-xs">
<p className="text-[11px] leading-4 text-muted-foreground">
识别图片文字后可逐条修改或删除,应用后会在保持画面风格的前提下完成文字更新。
</p>
{isDetectingText && (
<div className="flex items-center gap-2 text-muted-foreground text-sm">
<div className="flex items-center gap-2 text-[13px] text-muted-foreground">
<Loader2 className="h-4 w-4 animate-spin" />
正在识别图片文字...
</div>
@ -11734,7 +11734,7 @@ export function ImageNode({
{textEditItems.map((item, index) => (
<div
className="space-y-2 rounded-lg border border-border/70 p-3"
className="space-y-2 rounded-[var(--ui-radius-control)] border border-border/70 p-2.5"
key={item.id}
>
<div className="flex items-center justify-between gap-2">

View File

@ -71,12 +71,12 @@ export function PromptRegenerateDialog({
>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RefreshCw className="h-5 w-5" />
<RefreshCw className="h-4 w-4" />
提示词重绘
</DialogTitle>
</DialogHeader>
<div className="space-y-3 py-2">
<p className="text-muted-foreground text-sm">
<div className="space-y-3 py-1">
<p className="text-[13px] leading-5 text-muted-foreground">
在保留当前图片主体的基础上,用新提示词生成一张新图。原图不会被覆盖。
</p>
<div className="space-y-2">
@ -147,15 +147,15 @@ export function ThreeDGenerationDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{isWorldMode ? (
<Globe2 className="h-5 w-5" />
<Globe2 className="h-4 w-4" />
) : (
<Box className="h-5 w-5" />
<Box className="h-4 w-4" />
)}
{isWorldMode ? "选择 3D 世界模型" : "选择 3D 模型"}
</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<p className="text-muted-foreground text-sm">
<div className="space-y-3 py-1">
<p className="text-[13px] leading-5 text-muted-foreground">
{isWorldMode
? "先选择用于图生世界的生成质量,再开始创建 3D 世界任务。"
: "先选择用于图生 3D 的模型版本,再开始创建 3D 模型任务。"}
@ -186,11 +186,11 @@ export function ThreeDGenerationDialog({
</SelectContent>
</Select>
</div>
<div className="rounded-xl border border-slate-200 bg-slate-50 p-3 text-sm dark:border-slate-800 dark:bg-slate-900/80">
<div className="rounded-[var(--ui-radius-control)] border border-slate-200 bg-slate-50 p-2.5 text-[13px] dark:border-slate-800 dark:bg-slate-900/80">
<div className="font-medium text-slate-900 dark:text-slate-100">
{selectedEntry?.displayName || "未选择模型"}
</div>
<div className="mt-1 text-slate-600 text-xs dark:text-slate-400">
<div className="mt-0.5 text-[11px] leading-4 text-slate-600 dark:text-slate-400">
{selectedEntry?.description || "当前模型暂无说明。"}
</div>
{costLabel ? (

View File

@ -1770,7 +1770,7 @@ export function ImageAnnotator({
<Dialog onOpenChange={handleOpenChange} open={open}>
<DialogContent
className={cn(
"h-[95vh] w-[98vw] max-w-[1800px] overflow-hidden rounded-[20px] border-0 p-0 shadow-2xl sm:rounded-[20px] md:rounded-[24px]",
"h-[95vh] w-[98vw] max-w-[1800px] overflow-hidden rounded-[var(--ui-radius-panel)] border-0 p-0 shadow-2xl",
forceDarkAppearance ? "bg-[#09090b] text-zinc-100" : "bg-background"
)}
>
@ -2013,7 +2013,7 @@ export function ImageAnnotator({
{!showSceneFusion && isResizeMode && (
<div className="-translate-x-1/2 pointer-events-none absolute bottom-8 left-1/2 z-20 w-[min(92vw,1120px)]">
<div className="scrollbar-hide pointer-events-auto flex items-center justify-between gap-3 overflow-x-auto rounded-[28px] border border-border/50 bg-background/95 px-3 py-2 shadow-2xl backdrop-blur-xl">
<div className="scrollbar-hide pointer-events-auto flex items-center justify-between gap-2 overflow-x-auto rounded-[var(--ui-radius-panel)] border border-border/50 bg-background/95 px-2.5 py-1.5 shadow-xl backdrop-blur-xl">
<div className="flex min-w-max items-center gap-2">
<Popover
onOpenChange={setResizeRatioPickerOpen}
@ -2021,7 +2021,7 @@ export function ImageAnnotator({
>
<PopoverTrigger asChild>
<Button
className="h-10 shrink-0 rounded-[20px] bg-muted/35 px-4 font-medium text-foreground text-xs hover:bg-muted/45"
className="h-9 shrink-0 rounded-[var(--ui-radius-control)] bg-muted/35 px-3 text-[12px] font-medium text-foreground hover:bg-muted/45"
variant="ghost"
>
比例
@ -2033,7 +2033,7 @@ export function ImageAnnotator({
</PopoverTrigger>
<PopoverContent
align="start"
className="z-[10020] w-[320px] rounded-[22px] border-border/60 bg-background/96 p-3 backdrop-blur-xl"
className="z-[10020] w-[288px] rounded-[var(--ui-radius-panel)] border-border/60 bg-background/96 p-2.5 backdrop-blur-xl"
side="top"
>
<div className="mb-2 font-medium text-muted-foreground text-xs">
@ -2043,7 +2043,7 @@ export function ImageAnnotator({
{aspectRatios.map((ratio) => (
<Button
className={cn(
"h-9 rounded-[16px] px-3 font-medium text-xs",
"h-8 rounded-[var(--ui-radius-control)] px-2.5 text-[12px] font-medium",
selectedAspectRatio === ratio.value && "shadow-sm"
)}
key={ratio.value}
@ -2069,9 +2069,9 @@ export function ImageAnnotator({
</PopoverContent>
</Popover>
<div className="h-7 w-px shrink-0 bg-border/60" />
<div className="flex items-center gap-2 rounded-[20px] bg-muted/30 px-2 py-1">
<div className="flex items-center gap-2 rounded-[var(--ui-radius-control)] bg-muted/30 px-2 py-1">
<Input
className="h-8 w-24 border-0 bg-transparent px-2 text-center text-sm shadow-none focus-visible:ring-0"
className="h-8 w-24 border-0 bg-transparent px-2 text-center text-[13px] shadow-none focus-visible:ring-0"
onChange={(e) =>
handleWidthChange(Number.parseInt(e.target.value) || 1)
}
@ -2080,7 +2080,7 @@ export function ImageAnnotator({
/>
<span className="text-muted-foreground text-sm">×</span>
<Input
className="h-8 w-24 border-0 bg-transparent px-2 text-center text-sm shadow-none focus-visible:ring-0"
className="h-8 w-24 border-0 bg-transparent px-2 text-center text-[13px] shadow-none focus-visible:ring-0"
onChange={(e) =>
handleHeightChange(Number.parseInt(e.target.value) || 1)
}
@ -2089,7 +2089,7 @@ export function ImageAnnotator({
/>
</div>
<div className="h-7 w-px shrink-0 bg-border/60" />
<div className="flex items-center gap-1 rounded-[22px] bg-muted/35 px-1 py-1">
<div className="flex items-center gap-1 rounded-[var(--ui-radius-control)] bg-muted/35 p-1">
{resizeQuickPresets.map((preset) => {
const isActive =
resizeCanvasWidth === preset.width &&
@ -2097,7 +2097,7 @@ export function ImageAnnotator({
return (
<Button
className={cn(
"h-8 whitespace-nowrap rounded-[18px] px-3 font-medium text-xs",
"h-8 whitespace-nowrap rounded-[6px] px-2.5 text-[12px] font-medium",
isActive && "shadow-sm"
)}
key={`${preset.width}x${preset.height}`}
@ -2111,7 +2111,7 @@ export function ImageAnnotator({
})}
</div>
<div className="h-7 w-px shrink-0 bg-border/60" />
<div className="flex min-w-[280px] items-center gap-3 rounded-[22px] bg-muted/35 px-3 py-2">
<div className="flex min-w-[260px] items-center gap-2.5 rounded-[var(--ui-radius-control)] bg-muted/35 px-2.5 py-1.5">
<span className="shrink-0 font-medium text-muted-foreground text-xs">
缩放
</span>
@ -2162,14 +2162,14 @@ export function ImageAnnotator({
</div>
<div className="flex shrink-0 items-center gap-2">
<Button
className="rounded-[18px] px-4 text-muted-foreground hover:text-foreground"
className="px-3 text-muted-foreground hover:text-foreground"
onClick={onCancel}
variant="ghost"
>
取消
</Button>
<Button
className="rounded-[18px] bg-primary px-5 text-primary-foreground hover:bg-primary/90"
className="bg-primary px-4 text-primary-foreground hover:bg-primary/90"
disabled={isResizing}
onClick={handleResizeConfirm}
>
@ -2192,7 +2192,7 @@ export function ImageAnnotator({
{!showSceneFusion && !isResizeMode && (
<div className="-translate-x-1/2 pointer-events-none absolute bottom-8 left-1/2 z-20 w-[min(94vw,1120px)]">
<div className="scrollbar-hide pointer-events-auto flex items-center justify-between gap-3 overflow-x-auto rounded-[28px] border border-border/60 bg-background px-3 py-2 shadow-2xl">
<div className="scrollbar-hide pointer-events-auto flex items-center justify-between gap-2 overflow-x-auto rounded-[var(--ui-radius-panel)] border border-border/60 bg-background px-2.5 py-1.5 shadow-xl">
<TooltipProvider delayDuration={0}>
<div className="flex min-w-max items-center gap-1">
<Tooltip>
@ -2355,14 +2355,14 @@ export function ImageAnnotator({
<div className="flex shrink-0 items-center gap-2">
<Button
className="rounded-[18px] px-4 text-muted-foreground hover:text-foreground"
className="px-3 text-muted-foreground hover:text-foreground"
onClick={onCancel}
variant="ghost"
>
取消
</Button>
<Button
className="rounded-[18px] bg-primary px-6 text-primary-foreground hover:bg-primary/90"
className="bg-primary px-4 text-primary-foreground hover:bg-primary/90"
onClick={handleConfirm}
>
<Check className="mr-2 h-4 w-4" />

View File

@ -492,18 +492,18 @@ export function ImageParamsBar({
align="center"
side="top"
sideOffset={8}
className="w-[304px] max-w-[calc(100vw-32px)] rounded-[18px] border border-[#c7ccd5] bg-white p-3.5 text-[#252b33] shadow-[0_22px_56px_rgba(15,23,42,0.16)] dark:border-white/12 dark:bg-[#141416] dark:text-zinc-100 dark:shadow-[0_24px_64px_rgba(0,0,0,0.42)]"
className="w-[288px] max-w-[calc(100vw-24px)] rounded-[var(--ui-radius-panel)] border border-[#c7ccd5] bg-white p-3 text-[#252b33] shadow-[0_14px_36px_rgba(15,23,42,0.14)] dark:border-white/12 dark:bg-[#141416] dark:text-zinc-100 dark:shadow-[0_18px_44px_rgba(0,0,0,0.38)]"
>
<div className="space-y-4">
<div className="space-y-3">
<div>
<div className="mb-2 text-[14px] font-medium leading-none">质量</div>
<div className="grid h-9 grid-cols-4 rounded-full bg-[#f4f4f5] p-0.5 dark:bg-white/8">
<div className="mb-1.5 text-[12px] font-medium leading-none">质量</div>
<div className="grid h-8 grid-cols-4 rounded-[var(--ui-radius-control)] bg-[#f4f4f5] p-0.5 dark:bg-white/8">
{(["自动", "高", "中", "低"] as const).map((quality) => (
<button
key={quality}
type="button"
className={cn(
"rounded-full text-[13px] font-medium transition-colors",
"rounded-[6px] text-[12px] font-medium transition-colors",
qualityLabel === quality
? "border border-[#c8cdd5] bg-white shadow-[0_1px_4px_rgba(15,23,42,0.1)] dark:border-white/12 dark:bg-white/16"
: "text-[#3d434d] hover:bg-white/55 dark:text-zinc-300 dark:hover:bg-white/10"
@ -517,14 +517,14 @@ export function ImageParamsBar({
</div>
<div>
<div className="mb-2 text-[14px] font-medium leading-none">尺寸</div>
<div className="mb-1.5 text-[12px] font-medium leading-none">尺寸</div>
<div className="grid grid-cols-[1fr_20px_1fr] items-center gap-2">
<div className="flex h-10 items-center gap-2 rounded-[10px] bg-[#f3f3f4] px-3 text-[15px] dark:bg-white/8">
<div className="flex h-8 items-center gap-2 rounded-[var(--ui-radius-control)] bg-[#f3f3f4] px-2.5 text-[13px] dark:bg-white/8">
<span className="text-[#68707c] dark:text-zinc-400">W</span>
<span>{outputSize.width}</span>
</div>
<Link2 className="mx-auto h-3.5 w-3.5 text-[#8b929c]" />
<div className="flex h-10 items-center gap-2 rounded-[10px] bg-[#f3f3f4] px-3 text-[15px] dark:bg-white/8">
<div className="flex h-8 items-center gap-2 rounded-[var(--ui-radius-control)] bg-[#f3f3f4] px-2.5 text-[13px] dark:bg-white/8">
<span className="text-[#68707c] dark:text-zinc-400">H</span>
<span>{outputSize.height}</span>
</div>
@ -533,8 +533,8 @@ export function ImageParamsBar({
{sizeOptions.length > 0 && (
<div>
<div className="mb-2 text-[14px] font-medium leading-none">Size</div>
<div className="grid grid-cols-4 gap-2">
<div className="mb-1.5 text-[12px] font-medium leading-none">Size</div>
<div className="grid grid-cols-4 gap-1.5">
{sizeOptions.map((option) => {
const selected =
params.aspectRatio === option.aspectRatio &&
@ -544,7 +544,7 @@ export function ImageParamsBar({
key={`${option.aspectRatio}-${option.imageSize}-${option.label}`}
type="button"
className={cn(
"flex h-[66px] flex-col items-center justify-center gap-1.5 rounded-[10px] border text-[13px] transition-colors",
"flex h-14 flex-col items-center justify-center gap-1 rounded-[var(--ui-radius-control)] border text-[12px] transition-colors",
selected
? "border-[#bfc4cc] bg-[#e4e4e5] text-[#171b21] dark:border-white/18 dark:bg-white/16 dark:text-zinc-50"
: "border-[#d0d3d9] bg-white text-[#4c535d] hover:bg-[#f5f5f6] dark:border-white/12 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-white/8"
@ -568,14 +568,14 @@ export function ImageParamsBar({
)}
<div>
<div className="mb-2 text-[14px] font-medium leading-none">Image</div>
<div className="grid grid-cols-4 gap-2">
<div className="mb-1.5 text-[12px] font-medium leading-none">Image</div>
<div className="grid grid-cols-4 gap-1.5">
{imageCountOptions.map((count) => (
<button
key={count}
type="button"
className={cn(
"h-9 rounded-[9px] border text-[14px] transition-colors",
"h-8 rounded-[var(--ui-radius-control)] border text-[12px] transition-colors",
selectedImageCount === count
? "border-[#bfc4cc] bg-[#e4e4e5] text-[#171b21] dark:border-white/18 dark:bg-white/16 dark:text-zinc-50"
: "border-[#d0d3d9] bg-white text-[#4c535d] hover:bg-[#f5f5f6] dark:border-white/12 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-white/8"
@ -640,7 +640,7 @@ export function ImageParamsBar({
side="top"
sideOffset={10}
className={cn(
"w-[312px] max-w-[calc(100vw-24px)] rounded-[14px] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
"w-[288px] max-w-[calc(100vw-24px)] rounded-[var(--ui-radius-panel)] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
isDarkInline
? "border border-white/12 bg-[#0d1013]/98 text-white backdrop-blur-xl"
: "border border-[#d8dde4] bg-[#f3f4f6]/90"
@ -651,7 +651,7 @@ export function ImageParamsBar({
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>选择比例</p>
<div
className={cn(
"mt-1.5 grid grid-cols-4 gap-1.5 rounded-[12px] p-1.5 md:grid-cols-5",
"mt-1.5 grid grid-cols-4 gap-1.5 rounded-[var(--ui-radius-control)] p-1.5 md:grid-cols-5",
isDarkInline ? "bg-white/[0.04]" : "bg-[#eceff2]/90"
)}
>
@ -660,7 +660,7 @@ export function ImageParamsBar({
key={option.value}
type="button"
className={cn(
"flex h-[46px] flex-col items-center justify-center rounded-[10px] border transition",
"flex h-10 flex-col items-center justify-center rounded-[6px] border transition",
params.aspectRatio === option.value
? isDarkInline
? "border-emerald-400/50 bg-emerald-500/12 text-emerald-200 shadow-[0_1px_5px_rgba(15,23,42,0.18)]"
@ -687,7 +687,7 @@ export function ImageParamsBar({
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>选择分辨率</p>
<div
className={cn(
"mt-1.5 grid grid-cols-2 gap-1.5 rounded-[12px] p-1",
"mt-1.5 grid grid-cols-2 gap-1.5 rounded-[var(--ui-radius-control)] p-1",
isDarkInline ? "bg-white/[0.04]" : "bg-[#eceff2]/90"
)}
>
@ -696,7 +696,7 @@ export function ImageParamsBar({
key={option.value}
type="button"
className={cn(
"h-8 rounded-[9px] text-[12px] font-semibold transition",
"h-8 rounded-[6px] text-[12px] font-semibold transition",
params.imageSize === option.value
? isDarkInline
? "bg-emerald-500/12 text-emerald-200 shadow-[0_1px_5px_rgba(15,23,42,0.18)]"
@ -779,7 +779,7 @@ export function ImageParamsBar({
side="top"
sideOffset={10}
className={cn(
"w-auto min-w-[120px] rounded-[14px] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
"w-auto min-w-[120px] rounded-[var(--ui-radius-panel)] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
isDarkInline
? "border border-white/12 bg-[#0d1013]/98 text-white backdrop-blur-xl"
: "border border-[#d8dde4] bg-[#f3f4f6]/95"
@ -788,7 +788,7 @@ export function ImageParamsBar({
<div>
<p className={cn("text-[11px] font-medium", isDarkInline ? "text-white/45" : "text-[#8a94a3]")}>生成数量</p>
<div className={cn(
"mt-1.5 grid gap-1.5 rounded-[12px] p-1.5",
"mt-1.5 grid gap-1.5 rounded-[var(--ui-radius-control)] p-1.5",
isDarkInline ? "bg-white/[0.04]" : "bg-[#eceff2]/90",
imageCountOptions.length <= 3 ? "grid-cols-3" : "grid-cols-3"
)}>
@ -797,7 +797,7 @@ export function ImageParamsBar({
key={count}
type="button"
className={cn(
"flex h-8 min-w-[36px] items-center justify-center rounded-[9px] border text-[12px] font-semibold transition",
"flex h-8 min-w-[36px] items-center justify-center rounded-[6px] border text-[12px] font-semibold transition",
(params.imageCount || 1) === count
? isDarkInline
? "border-emerald-400/50 bg-emerald-500/12 text-emerald-200 shadow-[0_1px_5px_rgba(15,23,42,0.18)]"
@ -851,7 +851,7 @@ export function ImageParamsBar({
side="top"
sideOffset={10}
className={cn(
"w-[280px] max-w-[calc(100vw-24px)] rounded-[14px] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
"w-[280px] max-w-[calc(100vw-24px)] rounded-[var(--ui-radius-panel)] p-2 shadow-[0_10px_24px_rgba(15,23,42,0.14)]",
isDarkInline
? "border border-white/12 bg-[#0d1013]/98 text-white backdrop-blur-xl"
: "border border-[#d8dde4] bg-[#f3f4f6]/95"

View File

@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"fixed inset-0 z-50 bg-black/55 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 isolate grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg ring-1 ring-border/50 backdrop-blur-none duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 isolate grid w-full max-w-md translate-x-[-50%] translate-y-[-50%] gap-3 border bg-background p-5 shadow-lg ring-1 ring-border/50 backdrop-blur-none duration-150 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-[var(--ui-radius-panel)]",
className
)}
{...props}
@ -51,7 +51,7 @@ const AlertDialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
@ -79,7 +79,7 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
className={cn("text-base font-semibold leading-tight", className)}
{...props}
/>
))
@ -91,7 +91,7 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
className={cn("text-[13px] leading-5 text-muted-foreground", className)}
{...props}
/>
))

View File

@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"flex cursor-default select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-[13px] outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 isolate min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-border/50 backdrop-blur-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
"z-50 isolate min-w-[8rem] overflow-hidden rounded-[var(--ui-radius-control)] border bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-border/50 backdrop-blur-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 isolate max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-border/50 backdrop-blur-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
"z-50 isolate max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-[var(--ui-radius-control)] border bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-border/50 backdrop-blur-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
className
)}
{...props}
@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
"relative flex cursor-default select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-[13px] outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
@ -99,7 +99,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-[5px] py-1.5 pl-8 pr-2 text-[13px] outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
@ -123,7 +123,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-[5px] py-1.5 pl-8 pr-2 text-[13px] outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
@ -147,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
"px-2 py-1.5 text-[13px] font-semibold",
inset && "pl-8",
className
)}
@ -174,7 +174,7 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
className={cn("ml-auto text-[11px] opacity-60", className)}
{...props}
/>
)

View File

@ -499,7 +499,7 @@ export function VideoParamsBar({
side="top"
sideOffset={10}
className={cn(
"w-[312px] max-w-[calc(100vw-24px)] rounded-[14px] border p-2",
"w-[288px] max-w-[calc(100vw-24px)] rounded-[var(--ui-radius-panel)] border p-2",
panelClass
)}
>
@ -507,7 +507,7 @@ export function VideoParamsBar({
{/* 模式 */}
<div>
<p className={cn("text-[11px] font-medium", labelClass)}>选择模式</p>
<div className={cn("mt-1.5 grid grid-cols-2 gap-1.5 rounded-[12px] p-1.5", groupPanelClass)}>
<div className={cn("mt-1.5 grid grid-cols-2 gap-1.5 rounded-[var(--ui-radius-control)] p-1.5", groupPanelClass)}>
{modeOptions.map((option) => (
<button
key={option.value}
@ -527,7 +527,7 @@ export function VideoParamsBar({
{/* 比例 */}
<div>
<p className={cn("text-[11px] font-medium", labelClass)}>选择比例</p>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[12px] p-1.5", groupPanelClass)}>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[var(--ui-radius-control)] p-1.5", groupPanelClass)}>
{ratioOptions.map((option) => (
<button
key={option.value}
@ -548,7 +548,7 @@ export function VideoParamsBar({
{showResolutionControl && (
<div>
<p className={cn("text-[11px] font-medium", labelClass)}>选择分辨率</p>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[12px] p-1", groupPanelClass)}>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[var(--ui-radius-control)] p-1", groupPanelClass)}>
{resolutionOptions.map((option) => (
<button
key={option.value}
@ -572,7 +572,7 @@ export function VideoParamsBar({
<div>
<p className={cn("text-[11px] font-medium", labelClass)}>选择时长</p>
<div className={cn(
"mt-1.5 grid gap-1.5 rounded-[12px] p-1.5",
"mt-1.5 grid gap-1.5 rounded-[var(--ui-radius-control)] p-1.5",
groupPanelClass,
durationOptions.length > 6 ? "grid-cols-5" : "grid-cols-2"
)}>
@ -616,7 +616,7 @@ export function VideoParamsBar({
side="top"
sideOffset={10}
className={cn(
"w-[280px] max-w-[calc(100vw-24px)] rounded-[14px] border p-2",
"w-[280px] max-w-[calc(100vw-24px)] rounded-[var(--ui-radius-panel)] border p-2",
panelClass
)}
>
@ -624,7 +624,7 @@ export function VideoParamsBar({
{showGenerateAudioControl ? (
<div>
<p className={cn("text-[11px] font-medium", labelClass)}>音频</p>
<div className={cn("mt-1.5 grid grid-cols-2 gap-1.5 rounded-[12px] p-1.5", groupPanelClass)}>
<div className={cn("mt-1.5 grid grid-cols-2 gap-1.5 rounded-[var(--ui-radius-control)] p-1.5", groupPanelClass)}>
<button
type="button"
className={cn(
@ -656,7 +656,7 @@ export function VideoParamsBar({
) : !showFpsControl && !showCameraMoveControl ? (
<div
className={cn(
"rounded-[12px] p-2 text-[11px]",
"rounded-[var(--ui-radius-control)] p-2 text-[11px]",
isDarkInline
? "bg-white/8 text-zinc-400"
: "bg-[#eceff2]/90 text-[#7b8796] dark:bg-white/8 dark:text-zinc-400"
@ -669,7 +669,7 @@ export function VideoParamsBar({
{showFpsControl && (
<div>
<p className={cn("text-[11px] font-medium", labelClass)}>帧率</p>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[12px] p-1.5", groupPanelClass)}>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[var(--ui-radius-control)] p-1.5", groupPanelClass)}>
{FPS_OPTIONS.map((option) => (
<button
key={option.value}
@ -692,7 +692,7 @@ export function VideoParamsBar({
{showCameraMoveControl && (
<div>
<p className={cn("text-[11px] font-medium", labelClass)}>运镜模式</p>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[12px] p-1.5", groupPanelClass)}>
<div className={cn("mt-1.5 grid grid-cols-3 gap-1.5 rounded-[var(--ui-radius-control)] p-1.5", groupPanelClass)}>
{CAMERA_MOVE_OPTIONS.map((option) => (
<button
key={option.value}

View File

@ -10,6 +10,7 @@
import sharp from "sharp";
import { constrainImageDimensions } from "@/lib/ai/image-size-constraints";
import { buildReferenceImageOrderPrompt } from "@/lib/ai/providers/reference-image-order";
import { uploadFile } from "@/lib/storage/service";
interface AlibabaImageConfig {
@ -757,8 +758,12 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
// 根据图片数量确定编辑模式
const editMode = preparedImageUrls.length === 1 ? "单图编辑" : `多图融合 (${preparedImageUrls.length}张)`;
const promptForRequest = buildReferenceImageOrderPrompt(
prompt,
preparedImageUrls
);
console.log("[Alibaba Image] 编辑模式:", editMode);
console.log("[Alibaba Image] Prompt:", prompt.substring(0, 100) + (prompt.length > 100 ? "..." : ""));
console.log("[Alibaba Image] Prompt:", promptForRequest.substring(0, 100) + (promptForRequest.length > 100 ? "..." : ""));
console.log("[Alibaba Image] Input images:", preparedImageUrls.length);
console.log("[Alibaba Image] Final params:", params);
@ -767,7 +772,7 @@ export function createAlibabaImage({ apiKey, baseURL }: AlibabaImageConfig) {
baseURL,
apiKey,
activeModelId,
prompt,
promptForRequest,
preparedImageUrls,
params
);

View File

@ -1,6 +1,7 @@
import "server-only";
import { resolveGrsaiGptImageSize } from "@/lib/ai/grsai-gpt-image-sizes";
import { buildReferenceImageOrderPrompt } from "@/lib/ai/providers/reference-image-order";
import { uploadFile } from "@/lib/storage/service";
import { createLogger } from "@/lib/observability/logger";
@ -1135,17 +1136,18 @@ async function sendGeminiRequest(
processedUrls.push(s3Url);
}
}
const promptForRequest = buildReferenceImageOrderPrompt(prompt, processedUrls);
logger.debug(`${apiLogPrefix} Sending request to:`, url);
logger.debug(`${apiLogPrefix} Using model:`, modelId);
logger.debug(`${apiLogPrefix} Prompt:`, prompt.substring(0, 100));
logger.debug(`${apiLogPrefix} Prompt:`, promptForRequest.substring(0, 100));
logger.debug(`${apiLogPrefix} Reference image count:`, urls.length);
logger.debug(`${apiLogPrefix} Image params:`, imageParams);
// 使用对应模型的原生格式
const requestBody: any = {
model: modelId,
prompt: prompt,
prompt: promptForRequest,
};
if (isGptImageModel) {
@ -1185,7 +1187,7 @@ async function sendGeminiRequest(
baseURL,
apiKey,
modelId,
prompt,
prompt: promptForRequest,
size: resolvedSize,
imageUrls: processedUrls,
timeout,

View File

@ -1,4 +1,4 @@
export function buildSnapgenReferenceOrderPrompt(prompt: string, imageUrls: string[]) {
export function buildReferenceImageOrderPrompt(prompt: string, imageUrls: string[]) {
const normalizedPrompt = String(prompt || "").trim();
const referenceCount = imageUrls.filter((url) => String(url || "").trim()).length;
if (referenceCount < 2) return normalizedPrompt;
@ -6,6 +6,8 @@ export function buildSnapgenReferenceOrderPrompt(prompt: string, imageUrls: stri
return [
`参考图顺序说明:本次请求包含 ${referenceCount} 张参考图,顺序与上传/选中顺序一致。`,
"用户提示中的“第一张图/图1/图一/图片1/图片一”对应第 1 张参考图,“第二张图/图2/图二/图片2/图片二”对应第 2 张参考图,依次类推。",
"必须读取全部参考图,不得忽略后续图片;严格按用户提示判断目标底图、素材图及编辑关系,不要交换图片编号。",
"若用户要求“把图A中的对象放入图B”,图A是对象素材来源,图B是目标底图;最终结果以图B为基础,只修改用户指定区域,保留图B其余内容,不要反向替换,也不要输出拼图或并排对比图。",
"本说明只用于明确图片编号,不改变用户对主体、风格、构图、权重或融合方式的要求;若用户指定主参考或融合方式,以用户提示为准。",
normalizedPrompt,
]

View File

@ -2,7 +2,7 @@ import "server-only";
import sharp from "sharp";
import { uploadFile } from "@/lib/storage/service";
import { buildSnapgenReferenceOrderPrompt } from "@/lib/ai/providers/snapgen-reference-order";
import { buildReferenceImageOrderPrompt } from "@/lib/ai/providers/reference-image-order";
import {
SNAPGEN_GPT_IMAGE_2_REQUEST_ASPECT_RATIOS,
SNAPGEN_STANDARD_REQUEST_ASPECT_RATIOS,
@ -1020,7 +1020,7 @@ export function createSnapgenImage({ apiKey, baseURL }: SnapgenImageConfig) {
);
const maxImages = options.imageGenConfig?.maxImages || (apiFormat === "snapgen-grok-image" ? 8 : apiFormat === "snapgen-meta-ai-image" ? 4 : 1);
const imageCount = clampImageCount(options.imageParams?.imageCount, maxImages);
const promptForRequest = buildSnapgenReferenceOrderPrompt(prompt, imageUrls);
const promptForRequest = buildReferenceImageOrderPrompt(prompt, imageUrls);
const formData = await buildRequestForm({
apiFormat,
prompt: promptForRequest,

View File

@ -0,0 +1,81 @@
import type { Attachment } from "@/lib/types";
type MessageImageSource = {
parts?: unknown[] | null;
attachments?: unknown[] | null;
};
function normalizeImageContentType(value: unknown) {
const contentType = String(value || "").trim().toLowerCase();
return contentType.startsWith("image/") ? contentType : "image/jpeg";
}
export function getMessageImagePartUrl(part: unknown) {
const value = part as Record<string, any> | null;
if (!value) return "";
const imageUrl = value.image_url;
const candidate =
value.type === "image_url"
? typeof imageUrl === "string"
? imageUrl
: imageUrl?.url || value.url
: value.type === "image" || value.type === "input_image"
? value.image || value.url || value.data
: value.type === "file"
? value.url || value.data
: "";
return typeof candidate === "string" ? candidate.trim() : "";
}
export function collectMessageImageAttachments(
message: MessageImageSource | null | undefined
): Attachment[] {
if (!message) return [];
const result: Attachment[] = [];
const seen = new Set<string>();
const append = (value: unknown, fallbackIndex: number, fromPart: boolean) => {
const item = value as Record<string, any> | null;
if (!item) return;
const url = fromPart
? getMessageImagePartUrl(item)
: typeof item.url === "string"
? item.url.trim()
: "";
if (!url || seen.has(url)) return;
const explicitContentType = String(
item.contentType || item.mediaType || item.mimeType || ""
).trim();
if (explicitContentType && !explicitContentType.startsWith("image/")) {
return;
}
seen.add(url);
result.push({
name: String(item.name || item.filename || item.alt || `图片${fallbackIndex + 1}`),
url,
contentType: normalizeImageContentType(explicitContentType),
assetId:
typeof item.assetId === "string" && item.assetId.trim()
? item.assetId.trim()
: undefined,
width: Number.isFinite(Number(item.width)) ? Number(item.width) : undefined,
height: Number.isFinite(Number(item.height)) ? Number(item.height) : undefined,
});
};
(Array.isArray(message.parts) ? message.parts : []).forEach((part, index) => {
append(part, index, true);
});
(Array.isArray(message.attachments) ? message.attachments : []).forEach(
(attachment, index) => {
append(attachment, result.length + index, false);
}
);
return result;
}

View File

@ -5,6 +5,9 @@ export default defineConfig({
testMatch: [
/routes\/canvas-chat-helpers\.test\.ts/,
/routes\/canvas-agent-operations\.test\.ts/,
/routes\/canvas-overlay-density\.test\.ts/,
/routes\/image-message-contracts\.test\.ts/,
/routes\/production-local-fallbacks\.test\.ts/,
],
fullyParallel: true,
forbidOnly: !!process.env.CI,

View File

@ -0,0 +1,37 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
function readSource(path: string) {
return readFileSync(resolve(process.cwd(), path), "utf8");
}
test.describe("canvas overlay density", () => {
test("keeps the canvas model and mode menus compact", () => {
const source = readSource("components/canvas/CanvasChatPanel.tsx");
expect(source).toContain(
"max-h-[360px] w-[292px] max-w-[calc(100vw-24px)]"
);
expect(source).toContain(
"h-9 rounded-[var(--ui-radius-control)] px-2.5 text-[13px]"
);
expect(source).not.toContain(
"h-12 rounded-[14px] px-4 text-[17px]"
);
expect(source).not.toContain(
"text-[17px] font-medium leading-none tracking-[-0.01em]"
);
});
test("keeps shared dropdown and alert overlays on the global scale", () => {
const dropdownSource = readSource("components/ui/dropdown-menu.tsx");
const alertSource = readSource("components/ui/alert-dialog.tsx");
expect(dropdownSource).toContain("text-[13px]");
expect(dropdownSource).not.toContain("text-sm outline-none");
expect(alertSource).toContain("text-base font-semibold leading-tight");
expect(alertSource).toContain("text-[13px] leading-5");
expect(alertSource).not.toContain("text-lg font-semibold");
});
});

View File

@ -0,0 +1,83 @@
import { expect, test } from "@playwright/test";
import { buildReferenceImageOrderPrompt } from "@/lib/ai/providers/reference-image-order";
import { collectMessageImageAttachments } from "@/lib/chat/message-image-attachments";
test.describe("image message contracts", () => {
test("normalizes every supported image part shape for message rendering", () => {
const attachments = collectMessageImageAttachments({
parts: [
{
type: "file",
name: "图一",
mediaType: "image/png",
url: "https://cdn.example.com/one.png",
},
{
type: "image",
name: "图二",
image: "https://cdn.example.com/two.jpg",
},
{
type: "image_url",
name: "图三",
image_url: { url: "https://cdn.example.com/three.webp" },
},
{
type: "input_image",
data: "https://cdn.example.com/four.png",
},
{
type: "file",
mediaType: "video/mp4",
url: "https://cdn.example.com/video.mp4",
},
],
attachments: [
{
name: "重复图一",
contentType: "image/png",
url: "https://cdn.example.com/one.png",
},
{
name: "历史附件",
contentType: "image/jpeg",
url: "https://cdn.example.com/five.jpg",
},
],
});
expect(attachments.map((attachment) => attachment.url)).toEqual([
"https://cdn.example.com/one.png",
"https://cdn.example.com/two.jpg",
"https://cdn.example.com/three.webp",
"https://cdn.example.com/four.png",
"https://cdn.example.com/five.jpg",
]);
expect(attachments.map((attachment) => attachment.name)).toEqual([
"图一",
"图二",
"图三",
"图片4",
"历史附件",
]);
});
test("adds explicit ordinal guidance only for multi-image edits", () => {
const prompt = "把图二的横杆加到图一的桌子上,其他不变";
expect(
buildReferenceImageOrderPrompt(prompt, ["https://cdn.example.com/one.png"])
).toBe(prompt);
const multiImagePrompt = buildReferenceImageOrderPrompt(prompt, [
"https://cdn.example.com/one.png",
"https://cdn.example.com/two.png",
]);
expect(multiImagePrompt).toContain("本次请求包含 2 张参考图");
expect(multiImagePrompt).toContain("不得忽略后续图片");
expect(multiImagePrompt).toContain("第二张图/图2/图二");
expect(multiImagePrompt).toContain("图A是对象素材来源,图B是目标底图");
expect(multiImagePrompt).toContain("不要输出拼图或并排对比图");
expect(multiImagePrompt.endsWith(prompt)).toBe(true);
});
});

View File

@ -0,0 +1,23 @@
import { expect, test } from "@playwright/test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const ROUTES_WITH_LOCAL_FALLBACKS = [
"app/(chat)/api/models/route.ts",
"app/api/favicon/route.ts",
"app/api/site-settings/route.ts",
"app/api/user/membership-summary/route.ts",
];
test("requires an explicit environment switch for local fallbacks", () => {
for (const path of ROUTES_WITH_LOCAL_FALLBACKS) {
const source = readFileSync(resolve(process.cwd(), path), "utf8");
expect(source, path).toContain(
'process.env.CANVAS_E2E_LOCAL === "true"'
);
expect(source, path).not.toMatch(
/CANVAS_E2E_LOCAL[\s\S]{0,220}\|\|[\s\S]{0,120}(localhost|127\.0\.0\.1|::1)/
);
}
});