fix: preserve canvas image context and compact overlays
This commit is contained in:
parent
c044ee2c40
commit
988e1fefed
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}`}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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) {
|
||||
userImageAttachments.forEach((attachment, idx) => {
|
||||
renderedUserParts.push(
|
||||
<span
|
||||
key={`part-${idx}`}
|
||||
key={`image-${attachment.url}-${idx}`}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-xl px-2 py-1 text-xs",
|
||||
"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(fileUrl, 48)}
|
||||
src={buildPreviewImageUrl(attachment.url, 48)}
|
||||
alt=""
|
||||
className="w-5 h-5 object-cover rounded"
|
||||
className="h-4 w-4 rounded-[3px] object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
onError={(event) => {
|
||||
if (event.currentTarget.src !== fileUrl) {
|
||||
event.currentTarget.src = fileUrl;
|
||||
if (event.currentTarget.src !== attachment.url) {
|
||||
event.currentTarget.src = attachment.url;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="max-w-[80px] truncate text-current">{part.name || 'Image'}</span>
|
||||
<span className="max-w-[72px] truncate text-current">
|
||||
{attachment.name || "图片"}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
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"
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 ? (
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
))
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
]
|
||||
@ -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,
|
||||
|
||||
81
lib/chat/message-image-attachments.ts
Normal file
81
lib/chat/message-image-attachments.ts
Normal 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;
|
||||
}
|
||||
@ -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,
|
||||
|
||||
37
tests/routes/canvas-overlay-density.test.ts
Normal file
37
tests/routes/canvas-overlay-density.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
83
tests/routes/image-message-contracts.test.ts
Normal file
83
tests/routes/image-message-contracts.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
23
tests/routes/production-local-fallbacks.test.ts
Normal file
23
tests/routes/production-local-fallbacks.test.ts
Normal 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)/
|
||||
);
|
||||
}
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user