feat: add detail generation mode pipeline and solid select popovers
This commit is contained in:
parent
0a693fdb7d
commit
dcb82c73b5
@ -30,6 +30,12 @@ type DetailArchiveSlot = {
|
||||
contentType?: string;
|
||||
} | null;
|
||||
|
||||
type DetailGenerationMode = "classic" | "agent_layout";
|
||||
|
||||
function sanitizeGenerationMode(value: unknown): DetailGenerationMode {
|
||||
return value === "agent_layout" ? "agent_layout" : "classic";
|
||||
}
|
||||
|
||||
function sanitizeBrief(input: unknown): DetailArchiveBrief {
|
||||
const raw = (input || {}) as Record<string, unknown>;
|
||||
const normalize = (key: string) => String(raw[key] || "").replace(/\s+/g, " ").trim();
|
||||
@ -136,6 +142,7 @@ export async function POST(
|
||||
const summary = String(archiveRaw.summary || "").trim();
|
||||
const imageCount = Number(archiveRaw.imageCount);
|
||||
const coverUrl = String(archiveRaw.coverUrl || "").trim();
|
||||
const generationMode = sanitizeGenerationMode(archiveRaw.generationMode);
|
||||
|
||||
const brief = sanitizeBrief(archiveRaw.brief);
|
||||
const slots = sanitizeSlots(archiveRaw.slots);
|
||||
@ -147,6 +154,7 @@ export async function POST(
|
||||
userId: session.user.id,
|
||||
title,
|
||||
prompt,
|
||||
generationMode,
|
||||
brief,
|
||||
slots,
|
||||
summary: summary || null,
|
||||
@ -190,6 +198,7 @@ export async function PATCH(
|
||||
const summary = String(archiveRaw.summary || "").trim();
|
||||
const imageCount = Number(archiveRaw.imageCount);
|
||||
const coverUrl = String(archiveRaw.coverUrl || "").trim();
|
||||
const generationMode = sanitizeGenerationMode(archiveRaw.generationMode);
|
||||
const brief = sanitizeBrief(archiveRaw.brief);
|
||||
const slots = sanitizeSlots(archiveRaw.slots);
|
||||
|
||||
@ -198,6 +207,7 @@ export async function PATCH(
|
||||
.set({
|
||||
title,
|
||||
prompt,
|
||||
generationMode,
|
||||
brief,
|
||||
slots,
|
||||
summary: summary || null,
|
||||
|
||||
@ -86,6 +86,7 @@ async function generateTextWithTimeout(params: any, timeoutMs: number) {
|
||||
type DetailPlanRequest = {
|
||||
prompt?: string;
|
||||
count?: number;
|
||||
generationMode?: string;
|
||||
outputLanguage?: string;
|
||||
referenceImages?: string[];
|
||||
referenceImageNotes?: string[];
|
||||
@ -112,6 +113,8 @@ type DetailPlanRequest = {
|
||||
};
|
||||
};
|
||||
|
||||
type DetailGenerationMode = "classic" | "agent_layout";
|
||||
|
||||
type DetailBrief = {
|
||||
productSpu: string;
|
||||
demandType: string;
|
||||
@ -156,6 +159,7 @@ type DetailPlanScreen = {
|
||||
};
|
||||
|
||||
type DetailPlanResponse = {
|
||||
generationMode?: DetailGenerationMode;
|
||||
summary: string;
|
||||
prompts: string[];
|
||||
screenSize: {
|
||||
@ -786,6 +790,10 @@ function normalizeOutputLanguage(value: unknown): "zh" | "en" {
|
||||
return "zh";
|
||||
}
|
||||
|
||||
function normalizeDetailGenerationMode(value: unknown): DetailGenerationMode {
|
||||
return value === "agent_layout" ? "agent_layout" : "classic";
|
||||
}
|
||||
|
||||
function normalizeDetailBrief(value: unknown): DetailBrief {
|
||||
const raw = (value || {}) as Record<string, unknown>;
|
||||
const normalize = (key: string) => String(raw[key] || "").replace(/\s+/g, " ").trim();
|
||||
@ -1127,6 +1135,7 @@ function buildFallbackScreens(params: {
|
||||
function buildFallbackPlan(params: {
|
||||
prompt: string;
|
||||
count: number;
|
||||
generationMode: DetailGenerationMode;
|
||||
language: "zh" | "en";
|
||||
brief: DetailBrief;
|
||||
coreFocus: DetailCoreFocusContext;
|
||||
@ -1154,6 +1163,7 @@ function buildFallbackPlan(params: {
|
||||
coreFocus: params.coreFocus,
|
||||
});
|
||||
return {
|
||||
generationMode: params.generationMode,
|
||||
summary:
|
||||
language === "en"
|
||||
? `Generated ${count} storyboard frames with fixed size 790x1300.`
|
||||
@ -1653,6 +1663,7 @@ export async function POST(request: Request) {
|
||||
const hasModelImage = /^https?:\/\//i.test(modelImageUrl);
|
||||
const hasBundleImages = bundleImageUrls.length > 0;
|
||||
const count = hasBundleImages ? Math.min(20, baseCount + 1) : baseCount;
|
||||
const generationMode = normalizeDetailGenerationMode(body.generationMode);
|
||||
const language = normalizeOutputLanguage(body.outputLanguage);
|
||||
const brief = normalizeDetailBrief(body.brief);
|
||||
if (!brief.targetAudience || !brief.usageScenario || !brief.coreNeed) {
|
||||
@ -1668,6 +1679,7 @@ export async function POST(request: Request) {
|
||||
const fallback = buildFallbackPlan({
|
||||
prompt,
|
||||
count,
|
||||
generationMode,
|
||||
language,
|
||||
brief,
|
||||
coreFocus,
|
||||
@ -1734,6 +1746,7 @@ export async function POST(request: Request) {
|
||||
buildBriefText(brief, language),
|
||||
`目标分镜数量:${count}`,
|
||||
`输出语言:${language === "en" ? "English" : "中文"}`,
|
||||
`生成路线:${generationMode === "agent_layout" ? "AGENT排版(画面层+程序文字层)" : "传统生图(整页出图)"}`,
|
||||
"任务类型:电商详情页“AI详情生成”",
|
||||
"必须遵循“详情升级文案方案”:定位卡 -> 卖点到买点转译 -> 黄金五屏 -> FABE闭环。",
|
||||
`核心卖点锚点(整套必须围绕):${coreFocus.anchor}`,
|
||||
@ -1796,6 +1809,9 @@ export async function POST(request: Request) {
|
||||
`2) 保持同一产品一致性(结构、颜色、材质、Logo、比例)。\n` +
|
||||
`3) 所有分镜风格统一、构图统一、文案排版风格统一。主标题/副标题/正文的字体家族、字重、字号层级、行距必须全套统一。\n` +
|
||||
`4) 每屏尺寸固定 790x1300 像素,输出提示词必须显式包含该尺寸约束。\n` +
|
||||
(generationMode === "agent_layout"
|
||||
? `4.1) 当前为AGENT排版模式:visualPrompt 只描述画面层,禁止要求模型生成标题/副标题/正文等可见文字。\n`
|
||||
: "") +
|
||||
`5) 结构遵循“黄金五屏原则”:屏1-2核心卖点,屏3-4差异强化,屏5证据背书;更多屏作为延展。\n` +
|
||||
`5.1) 首屏主标题必须是“命名词+需求句”结构(6-20字,例如“甩甩杆,自拍不手抖”),并配一条结果副标题,直接回答“为什么值得买”。\n` +
|
||||
`5.2) 全套每一屏都必须服务同一核心卖点锚点,不允许出现与核心卖点无关的新主叙事。\n` +
|
||||
@ -1969,6 +1985,7 @@ export async function POST(request: Request) {
|
||||
const response: DetailPlanResponse = {
|
||||
summary,
|
||||
prompts,
|
||||
generationMode,
|
||||
screenSize: {
|
||||
width: 790,
|
||||
height: 1300,
|
||||
|
||||
@ -749,11 +749,13 @@ type DetailSlotConfig = {
|
||||
};
|
||||
|
||||
type DetailSectionKey = "required" | "strategy" | "assets" | "archives";
|
||||
type DetailGenerationMode = "classic" | "agent_layout";
|
||||
|
||||
type DetailArchive = {
|
||||
id: string;
|
||||
title: string;
|
||||
prompt: string;
|
||||
generationMode: DetailGenerationMode;
|
||||
brief: DetailBriefForm;
|
||||
slots: Array<PickedObjectAttachment | null>;
|
||||
createdAt: number;
|
||||
@ -764,6 +766,7 @@ type DetailArchive = {
|
||||
};
|
||||
|
||||
type DetailPlanResponse = {
|
||||
generationMode?: DetailGenerationMode;
|
||||
summary?: string;
|
||||
prompts?: string[];
|
||||
screenSize?: {
|
||||
@ -827,6 +830,7 @@ type DetailPlanResponse = {
|
||||
providerType: string;
|
||||
};
|
||||
};
|
||||
type DetailPlanScreenSpec = NonNullable<DetailPlanResponse["screens"]>[number];
|
||||
|
||||
const DETAIL_TARGET_WIDTH = 790;
|
||||
const DETAIL_TARGET_HEIGHT = 1300;
|
||||
@ -880,6 +884,22 @@ const DETAIL_FIELD_EXAMPLES: Record<keyof DetailBriefForm, string> = {
|
||||
brandTone: "示例:专业、可靠、亲和",
|
||||
bannedWords: "示例:最强、第一、100%治愈",
|
||||
};
|
||||
const DETAIL_GENERATION_MODE_OPTIONS: Array<{
|
||||
value: DetailGenerationMode;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: "classic",
|
||||
label: "传统生图",
|
||||
description: "整页直接生图,速度更快",
|
||||
},
|
||||
{
|
||||
value: "agent_layout",
|
||||
label: "AGENT排版",
|
||||
description: "生图只出画面,文字层可编辑",
|
||||
},
|
||||
];
|
||||
|
||||
const SESSION_TITLE_MAX_LEN = 18;
|
||||
|
||||
@ -936,6 +956,24 @@ type DetailScreenReviewResult = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
function normalizeDetailGenerationMode(value: unknown): DetailGenerationMode {
|
||||
return value === "agent_layout" ? "agent_layout" : "classic";
|
||||
}
|
||||
|
||||
function extractFirstNumber(value: string, fallback: number): number {
|
||||
const matched = String(value || "").match(/(\d{1,3})/);
|
||||
if (!matched) return fallback;
|
||||
const parsed = Number(matched[1]);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function extractFontWeight(value: string, fallback = "600"): string {
|
||||
const matched = String(value || "").match(/(?:\/|weight[::]?\s*)(\d{3})/i);
|
||||
if (!matched) return fallback;
|
||||
return matched[1] || fallback;
|
||||
}
|
||||
|
||||
function splitDetailKeywords(value: string, limit = 3): string[] {
|
||||
const raw = String(value || "")
|
||||
.split(/[\n,,、;;|]/)
|
||||
@ -1028,6 +1066,7 @@ function buildDetailReviewPlans(params: {
|
||||
}
|
||||
|
||||
function buildDetailResultMarkdown(params: {
|
||||
generationMode?: DetailGenerationMode;
|
||||
summary?: string;
|
||||
images: Array<{ originalUrl: string; thumbnailUrl: string }>;
|
||||
prompts: string[];
|
||||
@ -1077,6 +1116,11 @@ function buildDetailResultMarkdown(params: {
|
||||
}): string {
|
||||
const lines: string[] = [];
|
||||
lines.push("### AI详情生成完成");
|
||||
if (params.generationMode) {
|
||||
lines.push(
|
||||
`生成路线:${params.generationMode === "agent_layout" ? "AGENT排版(画面+可编辑文字层)" : "传统生图(整页生图)"}`
|
||||
);
|
||||
}
|
||||
if (params.summary) {
|
||||
lines.push(params.summary);
|
||||
}
|
||||
@ -1200,6 +1244,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const [attachments, setAttachments] = useState<PickedObjectAttachment[]>([]);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string>("");
|
||||
const [isDetailGenerationMode, setIsDetailGenerationMode] = useState(false);
|
||||
const [detailGenerationMode, setDetailGenerationMode] = useState<DetailGenerationMode>("classic");
|
||||
const [isDetailGenerating, setIsDetailGenerating] = useState(false);
|
||||
const [detailSections, setDetailSections] = useState<Record<DetailSectionKey, boolean>>({
|
||||
required: true,
|
||||
@ -2247,6 +2292,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
if (!selectedModelId && siteSettings) {
|
||||
// 使用系统设置的默认模型,如果没有设置则使用Agent模式
|
||||
const defaultModel = siteSettings.canvasDefaultModelId || "auto";
|
||||
currentModelIdRef.current = defaultModel;
|
||||
setSelectedModelId(defaultModel);
|
||||
}
|
||||
}, [selectedModelId, siteSettings]);
|
||||
@ -2259,6 +2305,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
|
||||
useEffect(() => {
|
||||
if (!modelSelectValue || modelSelectValue === selectedModelId) return;
|
||||
currentModelIdRef.current = modelSelectValue;
|
||||
setSelectedModelId(modelSelectValue);
|
||||
}, [modelSelectValue, selectedModelId]);
|
||||
|
||||
@ -2332,6 +2379,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const handleSelectGenerationMode = useCallback(
|
||||
(mode: "agent" | "image" | "video" | "detail") => {
|
||||
if (mode === "agent") {
|
||||
currentModelIdRef.current = "auto";
|
||||
setSelectedModelId("auto");
|
||||
setIsDetailGenerationMode(false);
|
||||
return;
|
||||
@ -2342,6 +2390,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
toast.error("AI详情生成功能未开启");
|
||||
return;
|
||||
}
|
||||
currentModelIdRef.current = "auto";
|
||||
setSelectedModelId("auto");
|
||||
setIsDetailGenerationMode(true);
|
||||
setIsDetailWorkbenchCollapsed(true);
|
||||
@ -2361,6 +2410,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
toast.error("当前没有可用的图像模型");
|
||||
return;
|
||||
}
|
||||
currentModelIdRef.current = preferredImageModelId;
|
||||
setSelectedModelId(preferredImageModelId);
|
||||
setIsDetailGenerationMode(false);
|
||||
return;
|
||||
@ -2375,6 +2425,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
toast.error("当前没有可用的视频模型");
|
||||
return;
|
||||
}
|
||||
currentModelIdRef.current = preferredVideoModelId;
|
||||
setSelectedModelId(preferredVideoModelId);
|
||||
setIsDetailGenerationMode(false);
|
||||
},
|
||||
@ -2468,6 +2519,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
id: String(item.id),
|
||||
title: String(item.title || "未命名详情项目"),
|
||||
prompt: String(item.prompt || ""),
|
||||
generationMode: normalizeDetailGenerationMode((item as any).generationMode),
|
||||
brief: {
|
||||
productSpu: String(item.brief?.productSpu || ""),
|
||||
demandType: String(item.brief?.demandType || ""),
|
||||
@ -2697,6 +2749,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
parts.push({ type: "text" as const, text: textContent });
|
||||
}
|
||||
|
||||
// 强制同步当前模型,避免模型切换后瞬时发送命中旧模型
|
||||
currentModelIdRef.current = selectedModelId;
|
||||
|
||||
sendMessage({
|
||||
role: "user",
|
||||
parts,
|
||||
@ -3383,6 +3438,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
imageCount?: number;
|
||||
coverUrl?: string;
|
||||
prompt: string;
|
||||
generationMode: DetailGenerationMode;
|
||||
brief: DetailBriefForm;
|
||||
slots: Array<PickedObjectAttachment | null>;
|
||||
}): Promise<DetailArchive | null> => {
|
||||
@ -3404,6 +3460,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
id: archiveId,
|
||||
title: archiveTitle,
|
||||
prompt: promptText,
|
||||
generationMode: params.generationMode,
|
||||
brief: { ...params.brief },
|
||||
slots: DETAIL_SLOT_CONFIGS.map((_, index) => {
|
||||
const slot = params.slots[index];
|
||||
@ -3438,6 +3495,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
|
||||
const loadDetailArchive = useCallback((archive: DetailArchive) => {
|
||||
setIsDetailGenerationMode(true);
|
||||
setDetailGenerationMode(normalizeDetailGenerationMode(archive.generationMode));
|
||||
setDetailBrief({ ...archive.brief });
|
||||
setDetailSlots(
|
||||
DETAIL_SLOT_CONFIGS.map((_, index) => {
|
||||
@ -3539,6 +3597,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
`${basePrompt},面向${brief.targetAudience},在${brief.usageScenario}场景解决${brief.coreNeed}${demandHint}`;
|
||||
const archiveSnapshot = {
|
||||
prompt: detailPrompt,
|
||||
generationMode: detailGenerationMode,
|
||||
brief: { ...detailBrief },
|
||||
slots: DETAIL_SLOT_CONFIGS.map((_, index) => {
|
||||
const slot = detailSlots[index];
|
||||
@ -3555,6 +3614,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const assistantMessageId = generateUUID();
|
||||
const skillLogicPreview = [
|
||||
"### AI详情生成",
|
||||
detailGenerationMode === "agent_layout"
|
||||
? "当前模式:AGENT排版(画面与文字分层生成)"
|
||||
: "当前模式:传统生图(整页直接生成)",
|
||||
"我们将基于《详情升级文案方案》执行以下技能流程:",
|
||||
"1. 定位卡:锁定人群/场景/触发时刻/风险与期望结果",
|
||||
"2. 需求拆解:卖点转买点,确定核心需求优先级",
|
||||
@ -3599,6 +3661,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
modelImageHint,
|
||||
bundleImageHint,
|
||||
`输出规格:每屏 ${DETAIL_TARGET_WIDTH}x${DETAIL_TARGET_HEIGHT} 像素`,
|
||||
`生成模式:${detailGenerationMode === "agent_layout" ? "AGENT排版(画面+可编辑文字层)" : "传统生图(整页出图)"}`,
|
||||
`字体规范:${DETAIL_TYPOGRAPHY_LOCK.title};${DETAIL_TYPOGRAPHY_LOCK.subtitle};${DETAIL_TYPOGRAPHY_LOCK.body}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
@ -3721,6 +3784,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
imageCount: 0,
|
||||
coverUrl: undefined,
|
||||
prompt: archiveSnapshot.prompt,
|
||||
generationMode: archiveSnapshot.generationMode,
|
||||
brief: archiveSnapshot.brief,
|
||||
slots: archiveSnapshot.slots,
|
||||
});
|
||||
@ -3776,6 +3840,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
body: JSON.stringify({
|
||||
prompt: detailPrompt,
|
||||
count: detailCount,
|
||||
generationMode: detailGenerationMode,
|
||||
referenceImages: detailReferenceAttachments.map((item) => item.url).slice(0, 12),
|
||||
referenceImageNotes,
|
||||
modelImageUrl: modelAttachment.url,
|
||||
@ -3791,6 +3856,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
|
||||
const planData = (planResult.data || {}) as DetailPlanResponse;
|
||||
const activeDetailGenerationMode = normalizeDetailGenerationMode(
|
||||
planData.generationMode || detailGenerationMode
|
||||
);
|
||||
const perScreenSteps = activeDetailGenerationMode === "agent_layout" ? 1 : 2;
|
||||
const prompts = Array.isArray(planData.prompts)
|
||||
? planData.prompts.map((item) => String(item || "").trim()).filter(Boolean)
|
||||
: [];
|
||||
@ -3804,7 +3873,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
language: outputLanguage,
|
||||
count: DETAIL_REVIEW_COUNT,
|
||||
});
|
||||
const totalSteps = prompts.length * 2 + 1 + reviewPlans.length;
|
||||
const totalSteps = prompts.length * perScreenSteps + 1 + reviewPlans.length;
|
||||
const qualityIssues = Array.isArray(planData.qualityChecks?.issues)
|
||||
? planData.qualityChecks?.issues.filter(Boolean)
|
||||
: [];
|
||||
@ -3837,6 +3906,110 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
if (detailPlaceholderNodeIds.length > prompts.length) {
|
||||
markExtraPlaceholders(prompts.length, "本轮未使用");
|
||||
}
|
||||
const isAgentLayoutMode = activeDetailGenerationMode === "agent_layout";
|
||||
const titleFontSize = Math.max(
|
||||
36,
|
||||
Math.min(
|
||||
80,
|
||||
extractFirstNumber(planData.globalStyle?.typography?.title || "", 64)
|
||||
)
|
||||
);
|
||||
const subtitleFontSize = Math.max(
|
||||
24,
|
||||
Math.min(
|
||||
50,
|
||||
extractFirstNumber(planData.globalStyle?.typography?.subtitle || "", 34)
|
||||
)
|
||||
);
|
||||
const bodyFontSize = Math.max(
|
||||
18,
|
||||
Math.min(
|
||||
36,
|
||||
extractFirstNumber(planData.globalStyle?.typography?.body || "", 24)
|
||||
)
|
||||
);
|
||||
const titleFontWeight = extractFontWeight(planData.globalStyle?.typography?.title || "", "700");
|
||||
const subtitleFontWeight = extractFontWeight(
|
||||
planData.globalStyle?.typography?.subtitle || "",
|
||||
"600"
|
||||
);
|
||||
const bodyFontWeight = extractFontWeight(planData.globalStyle?.typography?.body || "", "500");
|
||||
const createAgentLayoutTextLayers = (params: {
|
||||
nodeId: string;
|
||||
screenIndex: number;
|
||||
screen?: DetailPlanScreenSpec;
|
||||
}) => {
|
||||
if (!isAgentLayoutMode) return;
|
||||
const storeState = useCanvasStore.getState();
|
||||
const imageNode = storeState.getNode(params.nodeId);
|
||||
if (!imageNode) return;
|
||||
const x = Number(imageNode.position?.x || 0);
|
||||
const y = Number(imageNode.position?.y || 0);
|
||||
const width = Number(imageNode.size?.width || DETAIL_TARGET_WIDTH);
|
||||
const safeLeft = Math.round(width * 0.07);
|
||||
const textWidth = Math.max(240, Math.round(width - safeLeft * 2));
|
||||
const screen = params.screen;
|
||||
const titleText = String(screen?.title || "").trim();
|
||||
const subtitleText = String(screen?.subtitle || "").trim();
|
||||
const bodyText = String(screen?.copy || "").trim();
|
||||
|
||||
if (titleText) {
|
||||
addNode({
|
||||
type: "text",
|
||||
position: { x: x + safeLeft, y: y + 72 },
|
||||
size: { width: textWidth, height: 140 },
|
||||
data: {
|
||||
content: titleText,
|
||||
fontSize: params.screenIndex === 0 ? titleFontSize : Math.round(titleFontSize * 0.86),
|
||||
fontWeight: titleFontWeight,
|
||||
color: "#111827",
|
||||
lineHeight: 1.15,
|
||||
textAlign: "left",
|
||||
transparent: true,
|
||||
},
|
||||
connections: [],
|
||||
parentId: params.nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
if (subtitleText) {
|
||||
addNode({
|
||||
type: "text",
|
||||
position: { x: x + safeLeft, y: y + 214 },
|
||||
size: { width: textWidth, height: 120 },
|
||||
data: {
|
||||
content: subtitleText,
|
||||
fontSize: subtitleFontSize,
|
||||
fontWeight: subtitleFontWeight,
|
||||
color: "#374151",
|
||||
lineHeight: 1.22,
|
||||
textAlign: "left",
|
||||
transparent: true,
|
||||
},
|
||||
connections: [],
|
||||
parentId: params.nodeId,
|
||||
});
|
||||
}
|
||||
|
||||
if (bodyText) {
|
||||
addNode({
|
||||
type: "text",
|
||||
position: { x: x + safeLeft, y: y + 304 },
|
||||
size: { width: textWidth, height: 260 },
|
||||
data: {
|
||||
content: bodyText,
|
||||
fontSize: bodyFontSize,
|
||||
fontWeight: bodyFontWeight,
|
||||
color: "#4b5563",
|
||||
lineHeight: 1.38,
|
||||
textAlign: "left",
|
||||
transparent: true,
|
||||
},
|
||||
connections: [],
|
||||
parentId: params.nodeId,
|
||||
});
|
||||
}
|
||||
};
|
||||
for (let index = 0; index < prompts.length; index += 1) {
|
||||
const screen = screenSpecs[index];
|
||||
patchDetailPlaceholder(index, {
|
||||
@ -3852,6 +4025,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
detailPlanSummary: planData.summary || "",
|
||||
detailGlobalStyle: planData.globalStyle || null,
|
||||
detailPreflight: planData.preflight || null,
|
||||
detailGenerationMode: activeDetailGenerationMode,
|
||||
detailGeneratingPlaceholder: true,
|
||||
},
|
||||
});
|
||||
@ -3878,7 +4052,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const bundleReason = String((screen as any)?.bundleReason || "").trim();
|
||||
const promptText =
|
||||
String(screen?.visualPrompt || "").trim() || prompts[index]!;
|
||||
const generationStepCurrent = 2 + index * 2;
|
||||
const generationStepCurrent = 2 + index * perScreenSteps;
|
||||
const reviewStepCurrent = generationStepCurrent + 1;
|
||||
patchDetailPlaceholder(index, {
|
||||
label: `主图首稿生成中 ${index + 1}/${prompts.length}`,
|
||||
@ -3922,7 +4096,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `### AI详情生成\n正在生成第 ${index + 1}/${prompts.length} 屏,并锁定统一字体与版式...`,
|
||||
text: isAgentLayoutMode
|
||||
? `### AI详情生成\n正在生成第 ${index + 1}/${prompts.length} 屏画面层,随后自动渲染可编辑文字层...`
|
||||
: `### AI详情生成\n正在生成第 ${index + 1}/${prompts.length} 屏,并锁定统一字体与版式...`,
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -3947,16 +4123,22 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
? "搭配展示规则:本屏必须同场景展示主产品与搭配产品,主次层级清晰,突出组合购买价值与加购理由。"
|
||||
: "",
|
||||
bundleReason ? `搭配依据:${bundleReason}` : "",
|
||||
screen?.title ? `本屏标题:${screen.title}` : "",
|
||||
screen?.subtitle ? `本屏副标题:${screen.subtitle}` : "",
|
||||
screen?.copy ? `本屏文案说明:${screen.copy}` : "",
|
||||
isAgentLayoutMode
|
||||
? "生成策略:仅生成画面层(产品、光影、材质、场景),不要生成任何文字、数字、Logo、标语、水印。"
|
||||
: screen?.title
|
||||
? `本屏标题:${screen.title}`
|
||||
: "",
|
||||
isAgentLayoutMode ? "" : screen?.subtitle ? `本屏副标题:${screen.subtitle}` : "",
|
||||
isAgentLayoutMode ? "" : screen?.copy ? `本屏文案说明:${screen.copy}` : "",
|
||||
screen?.layoutNote ? `本屏排版说明:${screen.layoutNote}` : "",
|
||||
screen?.fabe?.b ? `B(利益):${screen.fabe.b}` : "",
|
||||
screen?.fabe?.f ? `F(特征):${screen.fabe.f}` : "",
|
||||
screen?.fabe?.a ? `A(优势):${screen.fabe.a}` : "",
|
||||
screen?.fabe?.e ? `E(证据):${screen.fabe.e}` : "",
|
||||
`输出为竖版电商详情图,尺寸固定 ${DETAIL_TARGET_WIDTH}x${DETAIL_TARGET_HEIGHT} 像素。`,
|
||||
"要求:整套详情图风格统一,产品外观严格一致,主标题/副标题/正文字体与层级全套一致,商业摄影质感,排版区域清晰,标题不得包含“第X屏”。",
|
||||
isAgentLayoutMode
|
||||
? "硬性约束:禁止输出任何可见文字或字符,排版文字由后续程序化图层渲染。"
|
||||
: "要求:整套详情图风格统一,产品外观严格一致,主标题/副标题/正文字体与层级全套一致,商业摄影质感,排版区域清晰,标题不得包含“第X屏”。",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
@ -3999,75 +4181,18 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
let finalGenerated = firstPassImage;
|
||||
let finalGenerationPrompt = generationPrompt;
|
||||
let screenReviewResult: DetailScreenReviewResult | null = null;
|
||||
patchDetailPlaceholder(index, {
|
||||
label: `双总监审稿中 ${index + 1}/${prompts.length}`,
|
||||
status: "generating",
|
||||
});
|
||||
|
||||
setDetailProgress({
|
||||
status: "running",
|
||||
stage: `第 ${index + 1}/${prompts.length} 屏质检中,Agent 正在检查标题与排版...`,
|
||||
current: reviewStepCurrent,
|
||||
total: totalSteps,
|
||||
});
|
||||
setMessages((prev) =>
|
||||
prev.map((message) =>
|
||||
message.id === assistantMessageId
|
||||
? {
|
||||
...message,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `### AI详情生成\n第 ${index + 1}/${prompts.length} 屏首稿完成,正在质检与排版复核...`,
|
||||
},
|
||||
],
|
||||
}
|
||||
: message,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
const reviewResponse = await fetch("/api/canvas/detail-review", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
outputLanguage,
|
||||
imageUrl: firstPassImage.originalUrl || firstPassImage.thumbnailUrl,
|
||||
referenceImages: generationReferenceImagesBase.slice(0, 6),
|
||||
brief,
|
||||
globalStyle: planData.globalStyle,
|
||||
screen: {
|
||||
index: index + 1,
|
||||
title: screen?.title || "",
|
||||
subtitle: screen?.subtitle || "",
|
||||
copy: screen?.copy || "",
|
||||
layoutNote: screen?.layoutNote || "",
|
||||
visualPrompt: generationPrompt,
|
||||
screenType: screen?.screenType || "",
|
||||
},
|
||||
}),
|
||||
});
|
||||
const reviewResult = await reviewResponse.json().catch(() => ({}));
|
||||
if (reviewResponse.ok && reviewResult?.success) {
|
||||
screenReviewResult = (reviewResult?.data || {}) as DetailScreenReviewResult;
|
||||
}
|
||||
} catch (reviewError) {
|
||||
console.error("[CanvasChat] Detail screen review failed:", reviewError);
|
||||
}
|
||||
|
||||
const shouldRegenerate =
|
||||
screenReviewResult?.pass === false &&
|
||||
typeof screenReviewResult?.revisedPrompt === "string" &&
|
||||
screenReviewResult.revisedPrompt.trim().length > 20;
|
||||
|
||||
if (shouldRegenerate) {
|
||||
if (!isAgentLayoutMode) {
|
||||
patchDetailPlaceholder(index, {
|
||||
label: `二次优化生成中 ${index + 1}/${prompts.length}`,
|
||||
label: `双总监审稿中 ${index + 1}/${prompts.length}`,
|
||||
status: "generating",
|
||||
});
|
||||
const issueHint = Array.isArray(screenReviewResult?.issues)
|
||||
? screenReviewResult.issues.filter(Boolean).slice(0, 2).join(";")
|
||||
: "";
|
||||
|
||||
setDetailProgress({
|
||||
status: "running",
|
||||
stage: `第 ${index + 1}/${prompts.length} 屏质检中,Agent 正在检查标题与排版...`,
|
||||
current: reviewStepCurrent,
|
||||
total: totalSteps,
|
||||
});
|
||||
setMessages((prev) =>
|
||||
prev.map((message) =>
|
||||
message.id === assistantMessageId
|
||||
@ -4076,7 +4201,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `### AI详情生成\n第 ${index + 1}/${prompts.length} 屏质检发现可优化项${issueHint ? `:${issueHint}` : ""},正在二次修图...`,
|
||||
text: `### AI详情生成\n第 ${index + 1}/${prompts.length} 屏首稿完成,正在质检与排版复核...`,
|
||||
},
|
||||
],
|
||||
}
|
||||
@ -4084,21 +4209,85 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
),
|
||||
);
|
||||
|
||||
for (let retry = 0; retry < DETAIL_SCREEN_REVIEW_MAX_ATTEMPTS; retry += 1) {
|
||||
try {
|
||||
const revisedPrompt = String(screenReviewResult?.revisedPrompt || "").trim();
|
||||
const regenerated = await generateDetailFrame(revisedPrompt, [
|
||||
firstPassImage.originalUrl || firstPassImage.thumbnailUrl,
|
||||
]);
|
||||
finalGenerated = regenerated;
|
||||
finalGenerationPrompt = revisedPrompt;
|
||||
break;
|
||||
} catch (regenError) {
|
||||
if (retry === DETAIL_SCREEN_REVIEW_MAX_ATTEMPTS - 1) {
|
||||
console.error("[CanvasChat] Detail second-pass generation failed:", regenError);
|
||||
try {
|
||||
const reviewResponse = await fetch("/api/canvas/detail-review", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
outputLanguage,
|
||||
imageUrl: firstPassImage.originalUrl || firstPassImage.thumbnailUrl,
|
||||
referenceImages: generationReferenceImagesBase.slice(0, 6),
|
||||
brief,
|
||||
globalStyle: planData.globalStyle,
|
||||
screen: {
|
||||
index: index + 1,
|
||||
title: screen?.title || "",
|
||||
subtitle: screen?.subtitle || "",
|
||||
copy: screen?.copy || "",
|
||||
layoutNote: screen?.layoutNote || "",
|
||||
visualPrompt: generationPrompt,
|
||||
screenType: screen?.screenType || "",
|
||||
},
|
||||
}),
|
||||
});
|
||||
const reviewResult = await reviewResponse.json().catch(() => ({}));
|
||||
if (reviewResponse.ok && reviewResult?.success) {
|
||||
screenReviewResult = (reviewResult?.data || {}) as DetailScreenReviewResult;
|
||||
}
|
||||
} catch (reviewError) {
|
||||
console.error("[CanvasChat] Detail screen review failed:", reviewError);
|
||||
}
|
||||
|
||||
const shouldRegenerate =
|
||||
screenReviewResult?.pass === false &&
|
||||
typeof screenReviewResult?.revisedPrompt === "string" &&
|
||||
screenReviewResult.revisedPrompt.trim().length > 20;
|
||||
|
||||
if (shouldRegenerate) {
|
||||
patchDetailPlaceholder(index, {
|
||||
label: `二次优化生成中 ${index + 1}/${prompts.length}`,
|
||||
status: "generating",
|
||||
});
|
||||
const issueHint = Array.isArray(screenReviewResult?.issues)
|
||||
? screenReviewResult.issues.filter(Boolean).slice(0, 2).join(";")
|
||||
: "";
|
||||
setMessages((prev) =>
|
||||
prev.map((message) =>
|
||||
message.id === assistantMessageId
|
||||
? {
|
||||
...message,
|
||||
parts: [
|
||||
{
|
||||
type: "text",
|
||||
text: `### AI详情生成\n第 ${index + 1}/${prompts.length} 屏质检发现可优化项${issueHint ? `:${issueHint}` : ""},正在二次修图...`,
|
||||
},
|
||||
],
|
||||
}
|
||||
: message,
|
||||
),
|
||||
);
|
||||
|
||||
for (let retry = 0; retry < DETAIL_SCREEN_REVIEW_MAX_ATTEMPTS; retry += 1) {
|
||||
try {
|
||||
const revisedPrompt = String(screenReviewResult?.revisedPrompt || "").trim();
|
||||
const regenerated = await generateDetailFrame(revisedPrompt, [
|
||||
firstPassImage.originalUrl || firstPassImage.thumbnailUrl,
|
||||
]);
|
||||
finalGenerated = regenerated;
|
||||
finalGenerationPrompt = revisedPrompt;
|
||||
break;
|
||||
} catch (regenError) {
|
||||
if (retry === DETAIL_SCREEN_REVIEW_MAX_ATTEMPTS - 1) {
|
||||
console.error("[CanvasChat] Detail second-pass generation failed:", regenError);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
patchDetailPlaceholder(index, {
|
||||
label: `排版层渲染中 ${index + 1}/${prompts.length}`,
|
||||
status: "generating",
|
||||
});
|
||||
}
|
||||
|
||||
generatedImages.push(finalGenerated);
|
||||
@ -4159,12 +4348,18 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
detailBundleShowcase: bundleShowcase,
|
||||
detailBundleReason: bundleReason,
|
||||
detailGlobalStyle: planData.globalStyle || null,
|
||||
detailGenerationMode: activeDetailGenerationMode,
|
||||
detailScreenReview: screenReviewResult || null,
|
||||
detailGeneratingPlaceholder: false,
|
||||
detailProgressLabel: `该屏完成 ${index + 1}/${prompts.length}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
createAgentLayoutTextLayers({
|
||||
nodeId: targetNodeId,
|
||||
screenIndex: index,
|
||||
screen,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (generatedImages.length !== prompts.length) {
|
||||
@ -4289,6 +4484,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
|
||||
const finalMarkdown = buildDetailResultMarkdown({
|
||||
generationMode: activeDetailGenerationMode,
|
||||
summary: planData.summary,
|
||||
images: generatedImages,
|
||||
prompts,
|
||||
@ -4347,6 +4543,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
imageCount: generatedImages.length,
|
||||
coverUrl: generatedImages[0]?.thumbnailUrl || generatedImages[0]?.originalUrl,
|
||||
prompt: archiveSnapshot.prompt,
|
||||
generationMode: archiveSnapshot.generationMode,
|
||||
brief: archiveSnapshot.brief,
|
||||
slots: archiveSnapshot.slots,
|
||||
});
|
||||
@ -4429,6 +4626,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
addNode,
|
||||
clearSelection,
|
||||
detailBrief,
|
||||
detailGenerationMode,
|
||||
detailSlots,
|
||||
imageParams.negativePrompt,
|
||||
input,
|
||||
@ -4764,7 +4962,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
}
|
||||
|
||||
if (!input.trim() && attachments.length === 0) return;
|
||||
if (!selectedModelId) {
|
||||
const effectiveModelId = currentModelIdRef.current || selectedModelId;
|
||||
if (!effectiveModelId) {
|
||||
toast.error("请选择一个模型");
|
||||
return;
|
||||
}
|
||||
@ -4883,6 +5082,11 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
parts.push({ type: "text" as const, text: textContent });
|
||||
}
|
||||
|
||||
// 若 ref 为空才回填,避免覆盖“刚切换但状态尚未刷新的新模型值”
|
||||
if (!currentModelIdRef.current) {
|
||||
currentModelIdRef.current = effectiveModelId;
|
||||
}
|
||||
|
||||
// 使用与 multimodal-input 相同的格式发送消息
|
||||
sendMessage({
|
||||
role: "user",
|
||||
@ -6226,6 +6430,43 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-xl border border-border/50 bg-zinc-50/40 p-3 dark:bg-zinc-900/40">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-[11px] font-medium text-foreground">生成路线</p>
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{detailGenerationMode === "classic" ? "当前:传统生图" : "当前:AGENT排版"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{DETAIL_GENERATION_MODE_OPTIONS.map((option) => {
|
||||
const selected = detailGenerationMode === option.value;
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isDetailGenerating) return;
|
||||
setDetailGenerationMode(option.value);
|
||||
}}
|
||||
disabled={isDetailGenerating}
|
||||
className={cn(
|
||||
"rounded-lg border px-3 py-2 text-left transition-colors",
|
||||
isDetailGenerating && "cursor-not-allowed opacity-60",
|
||||
selected
|
||||
? "border-primary/50 bg-primary/10 text-primary"
|
||||
: "border-border/50 bg-background text-muted-foreground hover:border-primary/30 hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
<div className="text-[11px] font-medium">{option.label}</div>
|
||||
<div className="mt-1 text-[10px] leading-snug opacity-80">
|
||||
{option.description}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
{/* Required Info */}
|
||||
<Collapsible
|
||||
@ -6669,6 +6910,8 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<span>{new Date(archive.createdAt).toLocaleString()}</span>
|
||||
<span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
|
||||
<span>{archive.imageCount || 0}张输出</span>
|
||||
<span className="h-0.5 w-0.5 rounded-full bg-zinc-300 dark:bg-zinc-600" />
|
||||
<span>{archive.generationMode === "agent_layout" ? "AGENT排版" : "传统生图"}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
@ -7127,6 +7370,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
<Select
|
||||
value={modelSelectValue}
|
||||
onValueChange={(value) => {
|
||||
currentModelIdRef.current = value;
|
||||
setSelectedModelId(value);
|
||||
if (value !== "auto" || !aiDetailGenerationEnabled) {
|
||||
setIsDetailGenerationMode(false);
|
||||
|
||||
@ -49,8 +49,11 @@ export function TextNode({ node, isSelected }: TextNodeProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full h-full bg-card rounded-lg overflow-hidden border shadow-sm p-3",
|
||||
isSelected && "border-primary"
|
||||
"relative w-full h-full overflow-hidden p-3",
|
||||
data.transparent
|
||||
? "rounded-none border-0 bg-transparent p-0 shadow-none"
|
||||
: "rounded-lg border bg-card shadow-sm",
|
||||
isSelected && (data.transparent ? "ring-1 ring-primary/50" : "border-primary")
|
||||
)}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
@ -73,6 +76,8 @@ export function TextNode({ node, isSelected }: TextNodeProps) {
|
||||
fontSize: data.fontSize || 14,
|
||||
color: data.color || 'inherit',
|
||||
fontWeight: data.fontWeight || 'normal',
|
||||
lineHeight: data.lineHeight || 1.35,
|
||||
textAlign: data.textAlign || "left",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@ -82,6 +87,8 @@ export function TextNode({ node, isSelected }: TextNodeProps) {
|
||||
fontSize: data.fontSize || 14,
|
||||
color: data.color || 'inherit',
|
||||
fontWeight: data.fontWeight || 'normal',
|
||||
lineHeight: data.lineHeight || 1.35,
|
||||
textAlign: data.textAlign || "left",
|
||||
}}
|
||||
>
|
||||
{data.content || '双击编辑文本'}
|
||||
|
||||
@ -70,25 +70,30 @@ SelectScrollDownButton.displayName =
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
>(({ className, children, position = "popper", style, ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-[10000] max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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-select-content-transform-origin]",
|
||||
"relative z-[10000] max-h-[var(--radix-select-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md ring-1 ring-border/50 !opacity-100 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-select-content-transform-origin]",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: "var(--popover)",
|
||||
opacity: 1,
|
||||
...style,
|
||||
}}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
"bg-inherit p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
"w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE "CanvasDetailArchive"
|
||||
ADD COLUMN IF NOT EXISTS "generationMode" varchar(32) NOT NULL DEFAULT 'classic';
|
||||
@ -225,6 +225,13 @@
|
||||
"when": 1772500000000,
|
||||
"tag": "0045_add_canvas_image_tasks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "7",
|
||||
"when": 1772517600000,
|
||||
"tag": "0046_add_canvas_detail_generation_mode",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -510,6 +510,9 @@ export const canvasDetailArchive = pgTable("CanvasDetailArchive", {
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
title: varchar("title", { length: 256 }).notNull().default("AI详情项目"),
|
||||
prompt: text("prompt").notNull().default(""),
|
||||
generationMode: varchar("generationMode", { length: 32 })
|
||||
.notNull()
|
||||
.default("classic"),
|
||||
brief: jsonb("brief").notNull(),
|
||||
slots: jsonb("slots").notNull(),
|
||||
summary: text("summary"),
|
||||
|
||||
@ -74,6 +74,9 @@ export interface TextNodeData {
|
||||
fontSize?: number;
|
||||
color?: string;
|
||||
fontWeight?: string;
|
||||
lineHeight?: number;
|
||||
textAlign?: "left" | "center" | "right";
|
||||
transparent?: boolean;
|
||||
}
|
||||
|
||||
// 场景裂变节点数据
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user