feat: separate agent projects from canvas storage
This commit is contained in:
parent
5be4d0e9df
commit
98fe210b64
179
app/api/agent-projects/[id]/route.ts
Normal file
179
app/api/agent-projects/[id]/route.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { canvasProject } from "@/lib/db/schema";
|
||||||
|
import {
|
||||||
|
createEmptyAgentGenerationProjectPersistedState,
|
||||||
|
normalizeAgentGenerationProjectPersistedState,
|
||||||
|
} from "@/lib/agents/generation-project-state";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
|
function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
||||||
|
const canvasData =
|
||||||
|
row.canvasData && typeof row.canvasData === "object" && !Array.isArray(row.canvasData)
|
||||||
|
? (row.canvasData as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
userId: row.userId,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description || null,
|
||||||
|
thumbnail: row.thumbnail || null,
|
||||||
|
projectState: normalizeAgentGenerationProjectPersistedState(
|
||||||
|
canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
||||||
|
),
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const [project] = await db
|
||||||
|
.select()
|
||||||
|
.from(canvasProject)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(canvasProject.id, id),
|
||||||
|
eq(canvasProject.userId, session.user.id),
|
||||||
|
eq(canvasProject.workspaceType, "agent")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, data: mapAgentProjectRow(project) });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentProjects API] 获取项目失败:", error);
|
||||||
|
return NextResponse.json({ success: false, error: "获取生图项目失败" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
const [existingProject] = await db
|
||||||
|
.select()
|
||||||
|
.from(canvasProject)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(canvasProject.id, id),
|
||||||
|
eq(canvasProject.userId, session.user.id),
|
||||||
|
eq(canvasProject.workspaceType, "agent")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingProject) {
|
||||||
|
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = body.title !== undefined ? String(body.title || "").trim() || "未命名生图项目" : undefined;
|
||||||
|
const description =
|
||||||
|
body.description !== undefined ? String(body.description || "").trim() || null : undefined;
|
||||||
|
const thumbnail =
|
||||||
|
body.thumbnail !== undefined ? String(body.thumbnail || "").trim() || null : undefined;
|
||||||
|
|
||||||
|
const existingCanvasData =
|
||||||
|
existingProject.canvasData &&
|
||||||
|
typeof existingProject.canvasData === "object" &&
|
||||||
|
!Array.isArray(existingProject.canvasData)
|
||||||
|
? (existingProject.canvasData as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const nextProjectState =
|
||||||
|
body.projectState !== undefined
|
||||||
|
? normalizeAgentGenerationProjectPersistedState(body.projectState)
|
||||||
|
: normalizeAgentGenerationProjectPersistedState(
|
||||||
|
existingCanvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
||||||
|
);
|
||||||
|
|
||||||
|
const [project] = await db
|
||||||
|
.update(canvasProject)
|
||||||
|
.set({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
thumbnail,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
canvasData: {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
selectedNodeIds: [],
|
||||||
|
viewport: { x: 0, y: 0, zoom: 1 },
|
||||||
|
tool: "select",
|
||||||
|
agentProjectState: nextProjectState,
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(canvasProject.id, id),
|
||||||
|
eq(canvasProject.userId, session.user.id),
|
||||||
|
eq(canvasProject.workspaceType, "agent")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, data: mapAgentProjectRow(project) });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentProjects API] 保存项目失败:", error);
|
||||||
|
return NextResponse.json({ success: false, error: "保存生图项目失败" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await params;
|
||||||
|
const [project] = await db
|
||||||
|
.delete(canvasProject)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(canvasProject.id, id),
|
||||||
|
eq(canvasProject.userId, session.user.id),
|
||||||
|
eq(canvasProject.workspaceType, "agent")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return NextResponse.json({ success: false, error: "生图项目不存在" }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, data: mapAgentProjectRow(project) });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentProjects API] 删除项目失败:", error);
|
||||||
|
return NextResponse.json({ success: false, error: "删除生图项目失败" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
99
app/api/agent-projects/route.ts
Normal file
99
app/api/agent-projects/route.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
|
import { auth } from "@/app/(auth)/auth";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
|
import { canvasProject } from "@/lib/db/schema";
|
||||||
|
import {
|
||||||
|
createEmptyAgentGenerationProjectPersistedState,
|
||||||
|
normalizeAgentGenerationProjectPersistedState,
|
||||||
|
} from "@/lib/agents/generation-project-state";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
export const revalidate = 0;
|
||||||
|
|
||||||
|
function mapAgentProjectRow(row: typeof canvasProject.$inferSelect) {
|
||||||
|
const canvasData =
|
||||||
|
row.canvasData && typeof row.canvasData === "object" && !Array.isArray(row.canvasData)
|
||||||
|
? (row.canvasData as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
userId: row.userId,
|
||||||
|
title: row.title,
|
||||||
|
description: row.description || null,
|
||||||
|
thumbnail: row.thumbnail || null,
|
||||||
|
projectState: normalizeAgentGenerationProjectPersistedState(
|
||||||
|
canvasData.agentProjectState || createEmptyAgentGenerationProjectPersistedState()
|
||||||
|
),
|
||||||
|
createdAt: row.createdAt,
|
||||||
|
updatedAt: row.updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const projects = await db
|
||||||
|
.select()
|
||||||
|
.from(canvasProject)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(canvasProject.userId, session.user.id),
|
||||||
|
eq(canvasProject.workspaceType, "agent")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(desc(canvasProject.updatedAt));
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: projects.map(mapAgentProjectRow),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentProjects API] 获取项目列表失败:", error);
|
||||||
|
return NextResponse.json({ success: false, error: "获取生图项目列表失败" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return NextResponse.json({ success: false, error: "未登录" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json().catch(() => ({}));
|
||||||
|
const title = String(body.title || "").trim() || "未命名生图项目";
|
||||||
|
const description = String(body.description || "").trim() || null;
|
||||||
|
const thumbnail = String(body.thumbnail || "").trim() || null;
|
||||||
|
const projectState = normalizeAgentGenerationProjectPersistedState(body.projectState);
|
||||||
|
|
||||||
|
const [project] = await db
|
||||||
|
.insert(canvasProject)
|
||||||
|
.values({
|
||||||
|
userId: session.user.id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
thumbnail,
|
||||||
|
workspaceType: "agent",
|
||||||
|
canvasData: {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
selectedNodeIds: [],
|
||||||
|
viewport: { x: 0, y: 0, zoom: 1 },
|
||||||
|
tool: "select",
|
||||||
|
agentProjectState: projectState,
|
||||||
|
} as any,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true, data: mapAgentProjectRow(project) });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[AgentProjects API] 创建项目失败:", error);
|
||||||
|
return NextResponse.json({ success: false, error: "创建生图项目失败" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,7 +3,7 @@ import { auth } from '@/app/(auth)/auth';
|
|||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { canvasProject } from '@/lib/db/schema';
|
import { canvasProject } from '@/lib/db/schema';
|
||||||
import { syncCanvasNodesForProject } from '@/lib/db/queries';
|
import { syncCanvasNodesForProject } from '@/lib/db/queries';
|
||||||
import { eq, desc } from 'drizzle-orm';
|
import { and, eq, desc } from 'drizzle-orm';
|
||||||
|
|
||||||
// 获取画布列表
|
// 获取画布列表
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@ -16,7 +16,12 @@ export async function GET(request: NextRequest) {
|
|||||||
const projects = await db
|
const projects = await db
|
||||||
.select()
|
.select()
|
||||||
.from(canvasProject)
|
.from(canvasProject)
|
||||||
.where(eq(canvasProject.userId, session.user.id))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(canvasProject.userId, session.user.id),
|
||||||
|
eq(canvasProject.workspaceType, "canvas")
|
||||||
|
)
|
||||||
|
)
|
||||||
.orderBy(desc(canvasProject.updatedAt));
|
.orderBy(desc(canvasProject.updatedAt));
|
||||||
|
|
||||||
return NextResponse.json({ success: true, data: projects });
|
return NextResponse.json({ success: true, data: projects });
|
||||||
|
|||||||
@ -70,6 +70,7 @@ import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew";
|
|||||||
import { AgentPresetPanelDarkV2 } from "@/components/agents/AgentPresetPanelDarkV2";
|
import { AgentPresetPanelDarkV2 } from "@/components/agents/AgentPresetPanelDarkV2";
|
||||||
import { AgentWorkflowPanelDarkV2 } from "@/components/agents/AgentWorkflowPanelDarkV2";
|
import { AgentWorkflowPanelDarkV2 } from "@/components/agents/AgentWorkflowPanelDarkV2";
|
||||||
import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2";
|
import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2";
|
||||||
|
import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet";
|
||||||
import { CanvasWorkspace } from "@/components/canvas";
|
import { CanvasWorkspace } from "@/components/canvas";
|
||||||
import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel";
|
import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel";
|
||||||
import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel";
|
import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel";
|
||||||
@ -94,7 +95,14 @@ import {
|
|||||||
createEmptyAgentModeWorkspaceState,
|
createEmptyAgentModeWorkspaceState,
|
||||||
normalizeAgentModeWorkspaceState,
|
normalizeAgentModeWorkspaceState,
|
||||||
} from "@/lib/agents/mode-workspace-state";
|
} from "@/lib/agents/mode-workspace-state";
|
||||||
|
import {
|
||||||
|
createEmptyAgentGenerationProjectPersistedState,
|
||||||
|
createEmptyAgentGenerationProjectWorkspaceState,
|
||||||
|
normalizeAgentGenerationProjectPersistedState,
|
||||||
|
} from "@/lib/agents/generation-project-state";
|
||||||
|
import { agentProjectService } from "@/services/agent-projects";
|
||||||
import type {
|
import type {
|
||||||
|
AgentGenerationProjectWorkspaceState,
|
||||||
AgentModeWorkspaceState,
|
AgentModeWorkspaceState,
|
||||||
AgentWorkflowComposerContext,
|
AgentWorkflowComposerContext,
|
||||||
AgentWorkflowMaterialSelections,
|
AgentWorkflowMaterialSelections,
|
||||||
@ -142,6 +150,15 @@ function buildScopedCanvasChatId(projectId: string, scope: "agent-mode") {
|
|||||||
].join("-");
|
].join("-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDefaultAgentProjectTitle() {
|
||||||
|
const now = new Date();
|
||||||
|
const month = `${now.getMonth() + 1}`.padStart(2, "0");
|
||||||
|
const day = `${now.getDate()}`.padStart(2, "0");
|
||||||
|
const hour = `${now.getHours()}`.padStart(2, "0");
|
||||||
|
const minute = `${now.getMinutes()}`.padStart(2, "0");
|
||||||
|
return `生图项目 ${month}-${day} ${hour}:${minute}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function CanvasEditorPage({ params }: PageProps) {
|
export default function CanvasEditorPage({ params }: PageProps) {
|
||||||
// 使用 useParams 确保客户端导航时能正确获取更新的 id
|
// 使用 useParams 确保客户端导航时能正确获取更新的 id
|
||||||
const routeParams = useParams();
|
const routeParams = useParams();
|
||||||
@ -212,10 +229,25 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
const [agentModeState, setAgentModeState] = useState<AgentModeWorkspaceState>(
|
const [agentModeState, setAgentModeState] = useState<AgentModeWorkspaceState>(
|
||||||
() => createEmptyAgentModeWorkspaceState()
|
() => createEmptyAgentModeWorkspaceState()
|
||||||
);
|
);
|
||||||
|
const [agentWorkspaceState, setAgentWorkspaceState] =
|
||||||
|
useState<AgentGenerationProjectWorkspaceState>(
|
||||||
|
() => createEmptyAgentGenerationProjectWorkspaceState()
|
||||||
|
);
|
||||||
|
const [agentProjectTitle, setAgentProjectTitle] = useState("未命名生图项目");
|
||||||
|
const [agentProjectThumbnail, setAgentProjectThumbnail] = useState<string | null>(null);
|
||||||
|
const [agentProjectResultUrls, setAgentProjectResultUrls] = useState<string[]>([]);
|
||||||
|
const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false);
|
||||||
|
const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false);
|
||||||
const [workflowHydrationSource, setWorkflowHydrationSource] = useState<
|
const [workflowHydrationSource, setWorkflowHydrationSource] = useState<
|
||||||
"pending" | "project" | "local" | "empty"
|
"pending" | "project" | "local" | "empty"
|
||||||
>("pending");
|
>("pending");
|
||||||
const agentModeChatId = useMemo(() => buildScopedCanvasChatId(id, "agent-mode"), [id]);
|
const requestedAgentProjectId = searchParams.get("agentProjectId");
|
||||||
|
const agentProjectId = requestedAgentProjectId ? requestedAgentProjectId.trim() : "";
|
||||||
|
const hasActiveAgentProject = Boolean(agentProjectId);
|
||||||
|
const agentModeChatId = useMemo(
|
||||||
|
() => buildScopedCanvasChatId(agentProjectId || id, "agent-mode"),
|
||||||
|
[agentProjectId, id]
|
||||||
|
);
|
||||||
|
|
||||||
const { data: siteSettingsData } = useSWR<{
|
const { data: siteSettingsData } = useSWR<{
|
||||||
siteName?: string;
|
siteName?: string;
|
||||||
@ -290,6 +322,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
updateCanvasSearchParams({
|
updateCanvasSearchParams({
|
||||||
mode: null,
|
mode: null,
|
||||||
agentPresetId: null,
|
agentPresetId: null,
|
||||||
|
agentProjectId: null,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -297,10 +330,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
updateCanvasSearchParams({
|
updateCanvasSearchParams({
|
||||||
mode: "agent",
|
mode: "agent",
|
||||||
agentPresetId: null,
|
agentPresetId: null,
|
||||||
|
agentProjectId: agentProjectId || null,
|
||||||
});
|
});
|
||||||
setIsPanelOpen(true);
|
setIsPanelOpen(true);
|
||||||
setPanelMode("chat");
|
setPanelMode("chat");
|
||||||
}, [isAgentMode, updateCanvasSearchParams]);
|
}, [agentProjectId, isAgentMode, updateCanvasSearchParams]);
|
||||||
|
|
||||||
// 加载画布数据
|
// 加载画布数据
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@ -320,6 +354,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
setSelectedBrandModel(null);
|
setSelectedBrandModel(null);
|
||||||
setSelectedAgentPresetId(null);
|
setSelectedAgentPresetId(null);
|
||||||
setAgentModeState(createEmptyAgentModeWorkspaceState());
|
setAgentModeState(createEmptyAgentModeWorkspaceState());
|
||||||
|
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
|
||||||
|
setAgentProjectTitle("未命名生图项目");
|
||||||
|
setAgentProjectThumbnail(null);
|
||||||
|
setAgentProjectResultUrls([]);
|
||||||
|
|
||||||
// 🆕 先清空当前画布状态,确保切换时不会显示旧数据
|
// 🆕 先清空当前画布状态,确保切换时不会显示旧数据
|
||||||
useCanvasStore.getState().clearCanvas();
|
useCanvasStore.getState().clearCanvas();
|
||||||
@ -331,7 +369,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
if (result.data.canvasData) {
|
if (result.data.canvasData) {
|
||||||
useCanvasStore.getState().loadCanvas(result.data.canvasData);
|
useCanvasStore.getState().loadCanvas(result.data.canvasData);
|
||||||
const persistedWorkflow = result.data.canvasData.workflowState;
|
const persistedWorkflow = result.data.canvasData.workflowState;
|
||||||
if (persistedWorkflow) {
|
if (!requestedAgentMode && persistedWorkflow) {
|
||||||
setWorkflowInputs({
|
setWorkflowInputs({
|
||||||
primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(),
|
primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(),
|
||||||
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
|
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
|
||||||
@ -388,20 +426,22 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
} else {
|
} else {
|
||||||
// 画布不存在时,创建一个新画布
|
// 画布不存在时,创建一个新画布
|
||||||
console.log('[Canvas] 画布不存在,创建新画布:', id);
|
console.log('[Canvas] 画布不存在,创建新画布:', id);
|
||||||
useCanvasStore.getState().setProject(id, '未命名画布');
|
useCanvasStore.getState().setProject(id, requestedAgentMode ? '智能体工作台' : '未命名画布');
|
||||||
setWorkflowHydrationSource("empty");
|
setWorkflowHydrationSource("empty");
|
||||||
// 保存新画布到数据库
|
if (!requestedAgentMode) {
|
||||||
try {
|
// 保存新画布到数据库
|
||||||
await canvasService.saveCanvasState(id, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: 'select' });
|
try {
|
||||||
} catch (error) {
|
await canvasService.saveCanvasState(id, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: 'select' });
|
||||||
console.error('[Canvas] 创建新画布失败:', error);
|
} catch (error) {
|
||||||
|
console.error('[Canvas] 创建新画布失败:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
loadProject();
|
loadProject();
|
||||||
}, [id]); // 只依赖 id,使用 getState() 直接调用 store actions
|
}, [id, requestedAgentMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateViewportWidth = () => setViewportWidth(window.innerWidth);
|
const updateViewportWidth = () => setViewportWidth(window.innerWidth);
|
||||||
@ -425,6 +465,14 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
|
|
||||||
const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]);
|
const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]);
|
||||||
const buildPersistedCanvasData = useCallback(() => {
|
const buildPersistedCanvasData = useCallback(() => {
|
||||||
|
return {
|
||||||
|
...getCanvasState(),
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
getCanvasState,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const buildPersistedAgentProjectState = useCallback(() => {
|
||||||
const workflowState: AgentWorkflowPersistedState = {
|
const workflowState: AgentWorkflowPersistedState = {
|
||||||
inputs: {
|
inputs: {
|
||||||
primarySku: workflowInputs.primarySku.trim(),
|
primarySku: workflowInputs.primarySku.trim(),
|
||||||
@ -441,21 +489,24 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getCanvasState(),
|
|
||||||
workflowState,
|
workflowState,
|
||||||
|
workspaceState: agentWorkspaceState,
|
||||||
|
resultImageUrls: agentProjectResultUrls,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
agentModeState,
|
||||||
|
agentProjectResultUrls,
|
||||||
|
agentWorkspaceState,
|
||||||
currentAgentStage,
|
currentAgentStage,
|
||||||
getCanvasState,
|
|
||||||
selectedAgentPresetId,
|
selectedAgentPresetId,
|
||||||
selectedBrandModel,
|
selectedBrandModel,
|
||||||
agentModeState,
|
|
||||||
workflowInputs,
|
workflowInputs,
|
||||||
workflowMaterials,
|
workflowMaterials,
|
||||||
workflowSelections,
|
workflowSelections,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isAgentMode) return;
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
if (workflowHydrationSource !== "empty") return;
|
if (workflowHydrationSource !== "empty") return;
|
||||||
|
|
||||||
@ -499,9 +550,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setWorkflowHydrationSource("local");
|
setWorkflowHydrationSource("local");
|
||||||
}
|
}
|
||||||
}, [workflowHydrationSource, workflowStorageKey]);
|
}, [isAgentMode, workflowHydrationSource, workflowStorageKey]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isAgentMode) return;
|
||||||
if (typeof window === "undefined" || workflowHydrationSource === "pending") return;
|
if (typeof window === "undefined" || workflowHydrationSource === "pending") return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -521,6 +573,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
console.warn("[Canvas] Failed to persist workflow state:", error);
|
console.warn("[Canvas] Failed to persist workflow state:", error);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
isAgentMode,
|
||||||
workflowHydrationSource,
|
workflowHydrationSource,
|
||||||
workflowInputs,
|
workflowInputs,
|
||||||
workflowMaterials,
|
workflowMaterials,
|
||||||
@ -532,6 +585,247 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
agentModeState,
|
agentModeState,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const persistAgentProject = useCallback(
|
||||||
|
async (reason: "autosave" | "manual" | "switch") => {
|
||||||
|
if (!isAgentMode || !agentProjectId) return false;
|
||||||
|
|
||||||
|
const result = await agentProjectService.updateProject(agentProjectId, {
|
||||||
|
title: agentProjectTitle || buildDefaultAgentProjectTitle(),
|
||||||
|
thumbnail: agentProjectThumbnail,
|
||||||
|
projectState: buildPersistedAgentProjectState(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`[Agent Project] ${reason} save failed:`, result.error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data) {
|
||||||
|
setAgentProjectTitle(result.data.title || buildDefaultAgentProjectTitle());
|
||||||
|
setAgentProjectThumbnail(result.data.thumbnail || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
agentProjectId,
|
||||||
|
agentProjectThumbnail,
|
||||||
|
agentProjectTitle,
|
||||||
|
buildPersistedAgentProjectState,
|
||||||
|
isAgentMode,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requestedAgentMode || !isAgentFeatureEnabled || agentProjectId) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const createAgentProject = async () => {
|
||||||
|
setIsAgentProjectLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await agentProjectService.createProject({
|
||||||
|
title: buildDefaultAgentProjectTitle(),
|
||||||
|
projectState: createEmptyAgentGenerationProjectPersistedState(),
|
||||||
|
});
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
throw new Error(result.error || "创建生图项目失败");
|
||||||
|
}
|
||||||
|
if (cancelled) return;
|
||||||
|
updateCanvasSearchParams({
|
||||||
|
mode: "agent",
|
||||||
|
agentProjectId: result.data.id,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "创建生图项目失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsAgentProjectLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void createAgentProject();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [agentProjectId, isAgentFeatureEnabled, requestedAgentMode, updateCanvasSearchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requestedAgentMode || !agentProjectId) return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const loadAgentProject = async () => {
|
||||||
|
setIsAgentProjectLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await agentProjectService.getProject(agentProjectId);
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
throw new Error(result.error || "加载生图项目失败");
|
||||||
|
}
|
||||||
|
if (cancelled) return;
|
||||||
|
|
||||||
|
const persistedState = normalizeAgentGenerationProjectPersistedState(
|
||||||
|
result.data.projectState
|
||||||
|
);
|
||||||
|
const persistedWorkflow = persistedState.workflowState;
|
||||||
|
|
||||||
|
setWorkflowInputs({
|
||||||
|
primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(),
|
||||||
|
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
|
||||||
|
brand: String(persistedWorkflow.inputs?.brand || "").trim(),
|
||||||
|
store: String(persistedWorkflow.inputs?.store || "").trim(),
|
||||||
|
});
|
||||||
|
setWorkflowMaterials(normalizeAgentWorkflowMaterialSelections(persistedWorkflow.materials));
|
||||||
|
setWorkflowSelections(persistedWorkflow.selections || {});
|
||||||
|
setSelectedBrandModel(persistedWorkflow.selectedBrandModel || null);
|
||||||
|
setSelectedAgentPresetId(String(persistedWorkflow.selectedAgentPresetId || "").trim() || null);
|
||||||
|
setCurrentAgentStage(persistedWorkflow.currentStage === "mode" ? "mode" : "workflow");
|
||||||
|
setAgentModeState(normalizeAgentModeWorkspaceState(persistedWorkflow.agentModeState));
|
||||||
|
setAgentWorkspaceState(
|
||||||
|
persistedState.workspaceState || createEmptyAgentGenerationProjectWorkspaceState()
|
||||||
|
);
|
||||||
|
setAgentProjectTitle(result.data.title || "未命名生图项目");
|
||||||
|
setAgentProjectThumbnail(result.data.thumbnail || null);
|
||||||
|
setAgentProjectResultUrls(persistedState.resultImageUrls || []);
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "加载生图项目失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsAgentProjectLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadAgentProject();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [agentProjectId, requestedAgentMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAgentMode || !agentProjectId || isAgentProjectLoading) return;
|
||||||
|
|
||||||
|
const timer = window.setTimeout(async () => {
|
||||||
|
setIsAgentProjectSaving(true);
|
||||||
|
try {
|
||||||
|
await persistAgentProject("autosave");
|
||||||
|
} finally {
|
||||||
|
setIsAgentProjectSaving(false);
|
||||||
|
}
|
||||||
|
}, 2500);
|
||||||
|
|
||||||
|
return () => window.clearTimeout(timer);
|
||||||
|
}, [
|
||||||
|
agentModeState,
|
||||||
|
agentProjectId,
|
||||||
|
agentProjectResultUrls,
|
||||||
|
agentProjectThumbnail,
|
||||||
|
agentProjectTitle,
|
||||||
|
agentWorkspaceState,
|
||||||
|
currentAgentStage,
|
||||||
|
isAgentMode,
|
||||||
|
isAgentProjectLoading,
|
||||||
|
persistAgentProject,
|
||||||
|
selectedAgentPresetId,
|
||||||
|
selectedBrandModel,
|
||||||
|
workflowInputs,
|
||||||
|
workflowMaterials,
|
||||||
|
workflowSelections,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleManualSaveAgentProject = useCallback(async () => {
|
||||||
|
if (!agentProjectId) {
|
||||||
|
toast.error("当前还没有可保存的生图项目");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsAgentProjectSaving(true);
|
||||||
|
try {
|
||||||
|
const success = await persistAgentProject("manual");
|
||||||
|
if (success) {
|
||||||
|
toast.success("生图项目已保存");
|
||||||
|
} else {
|
||||||
|
toast.error("保存生图项目失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsAgentProjectSaving(false);
|
||||||
|
}
|
||||||
|
}, [agentProjectId, persistAgentProject]);
|
||||||
|
|
||||||
|
const handleCreateAgentProject = useCallback(async () => {
|
||||||
|
if (agentProjectId) {
|
||||||
|
await persistAgentProject("switch");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await agentProjectService.createProject({
|
||||||
|
title: buildDefaultAgentProjectTitle(),
|
||||||
|
projectState: createEmptyAgentGenerationProjectPersistedState(),
|
||||||
|
});
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
toast.error(result.error || "创建生图项目失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCanvasSearchParams({
|
||||||
|
mode: "agent",
|
||||||
|
agentProjectId: result.data.id,
|
||||||
|
});
|
||||||
|
}, [agentProjectId, persistAgentProject, updateCanvasSearchParams]);
|
||||||
|
|
||||||
|
const handleSelectAgentProject = useCallback(
|
||||||
|
async (nextProjectId: string) => {
|
||||||
|
if (!nextProjectId || nextProjectId === agentProjectId) return;
|
||||||
|
if (agentProjectId) {
|
||||||
|
await persistAgentProject("switch");
|
||||||
|
}
|
||||||
|
updateCanvasSearchParams({
|
||||||
|
mode: "agent",
|
||||||
|
agentProjectId: nextProjectId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[agentProjectId, persistAgentProject, updateCanvasSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleDeleteAgentProject = useCallback(
|
||||||
|
async (targetProjectId: string) => {
|
||||||
|
const result = await agentProjectService.deleteProject(targetProjectId);
|
||||||
|
if (!result.success) {
|
||||||
|
toast.error(result.error || "删除生图项目失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetProjectId === agentProjectId) {
|
||||||
|
const listResult = await agentProjectService.getProjects();
|
||||||
|
const nextProject = listResult.success
|
||||||
|
? listResult.data?.find((project) => project.id !== targetProjectId) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (nextProject) {
|
||||||
|
updateCanvasSearchParams({
|
||||||
|
mode: "agent",
|
||||||
|
agentProjectId: nextProject.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const createResult = await agentProjectService.createProject({
|
||||||
|
title: buildDefaultAgentProjectTitle(),
|
||||||
|
projectState: createEmptyAgentGenerationProjectPersistedState(),
|
||||||
|
});
|
||||||
|
if (createResult.success && createResult.data) {
|
||||||
|
updateCanvasSearchParams({
|
||||||
|
mode: "agent",
|
||||||
|
agentProjectId: createResult.data.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("生图项目已删除");
|
||||||
|
},
|
||||||
|
[agentProjectId, updateCanvasSearchParams]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedBrandModel) return;
|
if (!selectedBrandModel) return;
|
||||||
const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
|
const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
|
||||||
@ -558,7 +852,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
|
|
||||||
// 自动保存逻辑
|
// 自动保存逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
if (isLoading || isAgentMode) return;
|
||||||
|
|
||||||
const saveTimer = setTimeout(async () => {
|
const saveTimer = setTimeout(async () => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
@ -571,11 +865,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
}, 3000); // 3秒自动保存
|
}, 3000); // 3秒自动保存
|
||||||
|
|
||||||
return () => clearTimeout(saveTimer);
|
return () => clearTimeout(saveTimer);
|
||||||
}, [nodes, workflowInputs, workflowSelections, isLoading, persistCanvasState]);
|
}, [isAgentMode, nodes, isLoading, persistCanvasState]);
|
||||||
|
|
||||||
// 删除节点时立即保存(按节点ID差集检测),避免删除后快速刷新被回滚
|
// 删除节点时立即保存(按节点ID差集检测),避免删除后快速刷新被回滚
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) {
|
if (isLoading || isAgentMode) {
|
||||||
previousNodeIdsRef.current = new Set(nodes.map((node) => node.id));
|
previousNodeIdsRef.current = new Set(nodes.map((node) => node.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -602,11 +896,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
};
|
};
|
||||||
void persistAfterDelete();
|
void persistAfterDelete();
|
||||||
}
|
}
|
||||||
}, [nodes, isLoading, persistCanvasState]);
|
}, [isAgentMode, nodes, isLoading, persistCanvasState]);
|
||||||
|
|
||||||
// 页面关闭/刷新前尝试做一次 keepalive 保存,降低“刚删就刷新”回滚概率
|
// 页面关闭/刷新前尝试做一次 keepalive 保存,降低“刚删就刷新”回滚概率
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
if (isLoading || isAgentMode) return;
|
||||||
|
|
||||||
const handleBeforeUnload = () => {
|
const handleBeforeUnload = () => {
|
||||||
try {
|
try {
|
||||||
@ -628,10 +922,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
window.removeEventListener("pagehide", handleBeforeUnload);
|
window.removeEventListener("pagehide", handleBeforeUnload);
|
||||||
};
|
};
|
||||||
}, [buildPersistedCanvasData, id, isLoading]);
|
}, [buildPersistedCanvasData, id, isAgentMode, isLoading]);
|
||||||
|
|
||||||
// 智能命名与缩略图逻辑
|
// 智能命名与缩略图逻辑
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isAgentMode) return;
|
||||||
const autoTitleAndThumbnail = async () => {
|
const autoTitleAndThumbnail = async () => {
|
||||||
// 如果标题是默认值,且有图片节点
|
// 如果标题是默认值,且有图片节点
|
||||||
if ((projectTitle === "未命名画布" || projectTitle === "新画布") && nodes.length > 0) {
|
if ((projectTitle === "未命名画布" || projectTitle === "新画布") && nodes.length > 0) {
|
||||||
@ -664,7 +959,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
// 使用防抖,避免频繁触发
|
// 使用防抖,避免频繁触发
|
||||||
const timer = setTimeout(autoTitleAndThumbnail, 5000); // 5秒后触发,给用户一点时间操作
|
const timer = setTimeout(autoTitleAndThumbnail, 5000); // 5秒后触发,给用户一点时间操作
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [nodes, projectTitle, id, setProjectTitle]);
|
}, [id, isAgentMode, nodes, projectTitle, setProjectTitle]);
|
||||||
|
|
||||||
// 保存画布 (手动触发保留作为备用,但UI上移除)
|
// 保存画布 (手动触发保留作为备用,但UI上移除)
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
@ -1039,6 +1334,34 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
const agentProjectNavSlot = isAgentMode ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
void handleManualSaveAgentProject();
|
||||||
|
}}
|
||||||
|
disabled={!hasActiveAgentProject || isAgentProjectLoading || isAgentProjectSaving}
|
||||||
|
className="h-9 gap-2 border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
||||||
|
>
|
||||||
|
{isAgentProjectSaving ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
保存项目
|
||||||
|
</Button>
|
||||||
|
<AgentProjectListSheet
|
||||||
|
currentProjectId={agentProjectId || null}
|
||||||
|
currentProjectTitle={agentProjectTitle}
|
||||||
|
onCreateProject={handleCreateAgentProject}
|
||||||
|
onSelectProject={handleSelectAgentProject}
|
||||||
|
onDeleteProject={handleDeleteAgentProject}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
if (isAgentMode) {
|
if (isAgentMode) {
|
||||||
// 深色主题智能体模式 V2 - 完整功能版本
|
// 深色主题智能体模式 V2 - 完整功能版本
|
||||||
|
|
||||||
@ -1057,6 +1380,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
siteLogo={siteSettingsData?.siteLogo}
|
siteLogo={siteSettingsData?.siteLogo}
|
||||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||||
showcaseConfig={siteSettingsData?.agentWorkflowShowcaseConfig}
|
showcaseConfig={siteSettingsData?.agentWorkflowShowcaseConfig}
|
||||||
|
topNavRightSlot={agentProjectNavSlot}
|
||||||
onNextStep={() => setCurrentAgentStage("mode")}
|
onNextStep={() => setCurrentAgentStage("mode")}
|
||||||
onSkipStep={() => setCurrentAgentStage("mode")}
|
onSkipStep={() => setCurrentAgentStage("mode")}
|
||||||
onReset={() => {
|
onReset={() => {
|
||||||
@ -1066,6 +1390,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
setSelectedBrandModel(null);
|
setSelectedBrandModel(null);
|
||||||
setSelectedAgentPresetId(null);
|
setSelectedAgentPresetId(null);
|
||||||
setAgentModeState(createEmptyAgentModeWorkspaceState());
|
setAgentModeState(createEmptyAgentModeWorkspaceState());
|
||||||
|
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
|
||||||
setCurrentAgentStage("workflow");
|
setCurrentAgentStage("workflow");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -1082,9 +1407,18 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
workflowContext={workflowContext}
|
workflowContext={workflowContext}
|
||||||
agentModeState={agentModeState}
|
agentModeState={agentModeState}
|
||||||
onAgentModeStateChange={setAgentModeState}
|
onAgentModeStateChange={setAgentModeState}
|
||||||
|
workspaceState={agentWorkspaceState}
|
||||||
|
onWorkspaceStateChange={setAgentWorkspaceState}
|
||||||
|
onResultPreviewUrlsChange={(urls) => {
|
||||||
|
setAgentProjectResultUrls(urls);
|
||||||
|
if (urls[0]) {
|
||||||
|
setAgentProjectThumbnail(urls[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
siteName={siteSettingsData?.siteName}
|
siteName={siteSettingsData?.siteName}
|
||||||
siteLogo={siteSettingsData?.siteLogo}
|
siteLogo={siteSettingsData?.siteLogo}
|
||||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||||
|
topNavRightSlot={agentProjectNavSlot}
|
||||||
syncGeneratedResultsToCanvas={false}
|
syncGeneratedResultsToCanvas={false}
|
||||||
onBack={() => setCurrentAgentStage("workflow")}
|
onBack={() => setCurrentAgentStage("workflow")}
|
||||||
onOpenCanvasEditor={() => {
|
onOpenCanvasEditor={() => {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import type { Attachment, ChatMessage } from "@/lib/types";
|
|||||||
import { cn, fetchWithErrorHandlers, fetcher, generateUUID } from "@/lib/utils";
|
import { cn, fetchWithErrorHandlers, fetcher, generateUUID } from "@/lib/utils";
|
||||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||||
import type {
|
import type {
|
||||||
|
AgentGenerationProjectWorkspaceState,
|
||||||
AgentModeReferenceCandidate,
|
AgentModeReferenceCandidate,
|
||||||
AgentModeWorkspaceState,
|
AgentModeWorkspaceState,
|
||||||
AgentPreset,
|
AgentPreset,
|
||||||
@ -52,6 +53,10 @@ import type {
|
|||||||
import {
|
import {
|
||||||
createEmptyAgentModeWorkspaceState,
|
createEmptyAgentModeWorkspaceState,
|
||||||
} from "@/lib/agents/mode-workspace-state";
|
} from "@/lib/agents/mode-workspace-state";
|
||||||
|
import {
|
||||||
|
createEmptyAgentGenerationProjectWorkspaceState,
|
||||||
|
normalizeAgentGenerationProjectPersistedState,
|
||||||
|
} from "@/lib/agents/generation-project-state";
|
||||||
|
|
||||||
interface AgentGenerationWorkspaceDarkV2Props {
|
interface AgentGenerationWorkspaceDarkV2Props {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -71,6 +76,10 @@ interface AgentGenerationWorkspaceDarkV2Props {
|
|||||||
agentModeState?: AgentModeWorkspaceState | null;
|
agentModeState?: AgentModeWorkspaceState | null;
|
||||||
onAgentModeStateChange?: (next: AgentModeWorkspaceState) => void;
|
onAgentModeStateChange?: (next: AgentModeWorkspaceState) => void;
|
||||||
onOpenCanvasEditor?: () => void;
|
onOpenCanvasEditor?: () => void;
|
||||||
|
workspaceState?: AgentGenerationProjectWorkspaceState | null;
|
||||||
|
onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void;
|
||||||
|
onResultPreviewUrlsChange?: (urls: string[]) => void;
|
||||||
|
topNavRightSlot?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PickedAttachment extends Attachment {}
|
interface PickedAttachment extends Attachment {}
|
||||||
@ -244,6 +253,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
agentModeState = null,
|
agentModeState = null,
|
||||||
onAgentModeStateChange,
|
onAgentModeStateChange,
|
||||||
onOpenCanvasEditor,
|
onOpenCanvasEditor,
|
||||||
|
workspaceState = null,
|
||||||
|
onWorkspaceStateChange,
|
||||||
|
onResultPreviewUrlsChange,
|
||||||
|
topNavRightSlot,
|
||||||
}: AgentGenerationWorkspaceDarkV2Props) {
|
}: AgentGenerationWorkspaceDarkV2Props) {
|
||||||
const rawChatId = useMemo(
|
const rawChatId = useMemo(
|
||||||
() => (chatId.startsWith("canvas-") ? chatId.replace("canvas-", "") : chatId),
|
() => (chatId.startsWith("canvas-") ? chatId.replace("canvas-", "") : chatId),
|
||||||
@ -257,15 +270,38 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
const hiddenPromptRef = useRef<string | null>(null);
|
const hiddenPromptRef = useRef<string | null>(null);
|
||||||
const generatedImageKeysRef = useRef<Set<string>>(new Set());
|
const generatedImageKeysRef = useRef<Set<string>>(new Set());
|
||||||
const lastSyncedBrandModelUrlRef = useRef<string | null>(null);
|
const lastSyncedBrandModelUrlRef = useRef<string | null>(null);
|
||||||
|
const workspaceStateSignatureRef = useRef<string>("");
|
||||||
const [localAgentModeState, setLocalAgentModeState] = useState<AgentModeWorkspaceState>(
|
const [localAgentModeState, setLocalAgentModeState] = useState<AgentModeWorkspaceState>(
|
||||||
() => createEmptyAgentModeWorkspaceState()
|
() => createEmptyAgentModeWorkspaceState()
|
||||||
);
|
);
|
||||||
|
const normalizedIncomingWorkspaceState = useMemo(
|
||||||
const [input, setInput] = useState("");
|
() =>
|
||||||
const [attachments, setAttachments] = useState<Array<PickedAttachment | null>>([]);
|
normalizeAgentGenerationProjectPersistedState({
|
||||||
|
workflowState: {},
|
||||||
|
workspaceState,
|
||||||
|
resultImageUrls: [],
|
||||||
|
}).workspaceState || createEmptyAgentGenerationProjectWorkspaceState(),
|
||||||
|
[workspaceState]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [input, setInput] = useState(normalizedIncomingWorkspaceState.input);
|
||||||
|
const [attachments, setAttachments] = useState<Array<PickedAttachment | null>>(
|
||||||
|
normalizedIncomingWorkspaceState.attachments.map((item) =>
|
||||||
|
item
|
||||||
|
? {
|
||||||
|
url: item.url,
|
||||||
|
name: item.name || "参考图",
|
||||||
|
contentType: item.contentType || "image/png",
|
||||||
|
assetId: item.assetId || undefined,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
);
|
||||||
const [activeAssignmentSlotIndex, setActiveAssignmentSlotIndex] = useState<number | null>(null);
|
const [activeAssignmentSlotIndex, setActiveAssignmentSlotIndex] = useState<number | null>(null);
|
||||||
const [slotUploading, setSlotUploading] = useState<number | null>(null);
|
const [slotUploading, setSlotUploading] = useState<number | null>(null);
|
||||||
const [selectedModelId, setSelectedModelId] = useState<string>("auto");
|
const [selectedModelId, setSelectedModelId] = useState<string>(
|
||||||
|
normalizedIncomingWorkspaceState.selectedModelId || "auto"
|
||||||
|
);
|
||||||
const [selectedResultUrl, setSelectedResultUrl] = useState<string | null>(null);
|
const [selectedResultUrl, setSelectedResultUrl] = useState<string | null>(null);
|
||||||
const [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
|
const [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
|
||||||
const [batchDownloadState, setBatchDownloadState] = useState<null | "scene" | "detail" | "sku">(null);
|
const [batchDownloadState, setBatchDownloadState] = useState<null | "scene" | "detail" | "sku">(null);
|
||||||
@ -284,9 +320,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
lastQuery: null,
|
lastQuery: null,
|
||||||
});
|
});
|
||||||
const [imageParams, setImageParams] = useState<ImageGenerationParams>({
|
const [imageParams, setImageParams] = useState<ImageGenerationParams>({
|
||||||
aspectRatio: "auto",
|
aspectRatio: normalizedIncomingWorkspaceState.imageParams?.aspectRatio || "auto",
|
||||||
imageSize: "2K",
|
imageSize: normalizedIncomingWorkspaceState.imageParams?.imageSize || "2K",
|
||||||
imageCount: 1,
|
imageCount: normalizedIncomingWorkspaceState.imageParams?.imageCount || 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: historyData } = useSWR<{ messages: ChatMessage[] }>(
|
const { data: historyData } = useSWR<{ messages: ChatMessage[] }>(
|
||||||
@ -514,6 +550,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
}, [historyMessages, messages.length, setMessages]);
|
}, [historyMessages, messages.length, setMessages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (normalizedIncomingWorkspaceState.selectedModelId) {
|
||||||
|
setSelectedModelId(normalizedIncomingWorkspaceState.selectedModelId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (selectedAgentPreset?.defaultModelId) {
|
if (selectedAgentPreset?.defaultModelId) {
|
||||||
setSelectedModelId(selectedAgentPreset.defaultModelId);
|
setSelectedModelId(selectedAgentPreset.defaultModelId);
|
||||||
return;
|
return;
|
||||||
@ -521,7 +561,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
if (modelsData?.defaultModelId) {
|
if (modelsData?.defaultModelId) {
|
||||||
setSelectedModelId(modelsData.defaultModelId);
|
setSelectedModelId(modelsData.defaultModelId);
|
||||||
}
|
}
|
||||||
}, [modelsData?.defaultModelId, selectedAgentPreset?.defaultModelId]);
|
}, [
|
||||||
|
modelsData?.defaultModelId,
|
||||||
|
normalizedIncomingWorkspaceState.selectedModelId,
|
||||||
|
selectedAgentPreset?.defaultModelId,
|
||||||
|
]);
|
||||||
|
|
||||||
// 初始化附件
|
// 初始化附件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -761,6 +805,71 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
const nextToolStep =
|
const nextToolStep =
|
||||||
activeToolStepIndex < toolSteps.length - 1 ? toolSteps[activeToolStepIndex + 1] : null;
|
activeToolStepIndex < toolSteps.length - 1 ? toolSteps[activeToolStepIndex + 1] : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const incomingSignature = JSON.stringify(normalizedIncomingWorkspaceState);
|
||||||
|
if (incomingSignature === workspaceStateSignatureRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workspaceStateSignatureRef.current = incomingSignature;
|
||||||
|
setInput(normalizedIncomingWorkspaceState.input);
|
||||||
|
setAttachments(
|
||||||
|
normalizedIncomingWorkspaceState.attachments.map((item) =>
|
||||||
|
item
|
||||||
|
? {
|
||||||
|
url: item.url,
|
||||||
|
name: item.name || "参考图",
|
||||||
|
contentType: item.contentType || "image/png",
|
||||||
|
assetId: item.assetId || undefined,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setSelectedModelId(normalizedIncomingWorkspaceState.selectedModelId || "auto");
|
||||||
|
setImageParams({
|
||||||
|
aspectRatio: normalizedIncomingWorkspaceState.imageParams?.aspectRatio || "auto",
|
||||||
|
imageSize: normalizedIncomingWorkspaceState.imageParams?.imageSize || "2K",
|
||||||
|
imageCount: normalizedIncomingWorkspaceState.imageParams?.imageCount || 1,
|
||||||
|
});
|
||||||
|
}, [normalizedIncomingWorkspaceState]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextWorkspaceState = {
|
||||||
|
input,
|
||||||
|
attachments: attachments.map((item) =>
|
||||||
|
item
|
||||||
|
? {
|
||||||
|
url: item.url,
|
||||||
|
name: item.name || null,
|
||||||
|
contentType: item.contentType || null,
|
||||||
|
assetId: item.assetId || null,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
),
|
||||||
|
selectedModelId,
|
||||||
|
imageParams: {
|
||||||
|
aspectRatio: imageParams.aspectRatio,
|
||||||
|
imageSize: imageParams.imageSize,
|
||||||
|
imageCount: imageParams.imageCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
workspaceStateSignatureRef.current = JSON.stringify(nextWorkspaceState);
|
||||||
|
onWorkspaceStateChange?.(nextWorkspaceState);
|
||||||
|
}, [
|
||||||
|
attachments,
|
||||||
|
imageParams.aspectRatio,
|
||||||
|
imageParams.imageCount,
|
||||||
|
imageParams.imageSize,
|
||||||
|
input,
|
||||||
|
onWorkspaceStateChange,
|
||||||
|
selectedModelId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onResultPreviewUrlsChange?.(
|
||||||
|
Array.from(new Set(isReplacementPreset ? latestReplacementResultUrls : allGeneratedImages))
|
||||||
|
);
|
||||||
|
}, [allGeneratedImages, isReplacementPreset, latestReplacementResultUrls, onResultPreviewUrlsChange]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedResultUrl && latestGeneratedGroup?.imageUrls[0]) {
|
if (!selectedResultUrl && latestGeneratedGroup?.imageUrls[0]) {
|
||||||
setSelectedResultUrl(latestGeneratedGroup.imageUrls[0]);
|
setSelectedResultUrl(latestGeneratedGroup.imageUrls[0]);
|
||||||
@ -1323,7 +1432,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
siteLogoDark={siteLogoDark}
|
siteLogoDark={siteLogoDark}
|
||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
activeKey="agent"
|
activeKey="agent"
|
||||||
rightSlot={<div className="w-10" />}
|
rightSlot={topNavRightSlot || <div className="w-10" />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
|
|||||||
195
components/agents/AgentProjectListSheet.tsx
Normal file
195
components/agents/AgentProjectListSheet.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { zhCN } from "date-fns/locale";
|
||||||
|
import { FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { agentProjectService } from "@/services/agent-projects";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||||
|
import type { AgentGenerationProject } from "@/types/agent";
|
||||||
|
|
||||||
|
interface AgentProjectListSheetProps {
|
||||||
|
currentProjectId?: string | null;
|
||||||
|
currentProjectTitle?: string | null;
|
||||||
|
onCreateProject: () => Promise<void> | void;
|
||||||
|
onSelectProject: (projectId: string) => Promise<void> | void;
|
||||||
|
onDeleteProject: (projectId: string) => Promise<void> | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentProjectListSheet({
|
||||||
|
currentProjectId,
|
||||||
|
currentProjectTitle,
|
||||||
|
onCreateProject,
|
||||||
|
onSelectProject,
|
||||||
|
onDeleteProject,
|
||||||
|
}: AgentProjectListSheetProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [projects, setProjects] = useState<AgentGenerationProject[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const loadProjects = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await agentProjectService.getProjects();
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
throw new Error(result.error || "加载失败");
|
||||||
|
}
|
||||||
|
setProjects(result.data);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "加载生图项目失败");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
void loadProjects();
|
||||||
|
}, [open, currentProjectId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 max-w-[260px] gap-2 border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
||||||
|
>
|
||||||
|
<FolderOpen className="h-4 w-4 shrink-0" />
|
||||||
|
<span className="truncate">{currentProjectTitle || "生图项目列表"}</span>
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent
|
||||||
|
side="right"
|
||||||
|
className="w-[380px] border-white/10 bg-[#0b0d0f] p-0 text-white sm:max-w-[380px]"
|
||||||
|
>
|
||||||
|
<SheetHeader className="border-b border-white/10 px-5 py-4 text-left">
|
||||||
|
<SheetTitle className="text-white">生图项目列表</SheetTitle>
|
||||||
|
<SheetDescription className="text-white/45">
|
||||||
|
这里保存独立于无限画布的生图项目,可随时继续生成。
|
||||||
|
</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
|
||||||
|
<div className="border-b border-white/10 px-5 py-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
await onCreateProject();
|
||||||
|
await loadProjects();
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className="h-10 w-full gap-2 bg-emerald-500 text-white hover:bg-emerald-400"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
新建生图项目
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScrollArea className="h-[calc(100vh-150px)]">
|
||||||
|
<div className="space-y-2 p-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex justify-center py-12">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-white/50" />
|
||||||
|
</div>
|
||||||
|
) : projects.length > 0 ? (
|
||||||
|
projects.map((project) => (
|
||||||
|
<button
|
||||||
|
key={project.id}
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
await onSelectProject(project.id);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"group flex w-full items-center gap-3 rounded-2xl border p-3 text-left transition-colors",
|
||||||
|
project.id === currentProjectId
|
||||||
|
? "border-emerald-400/30 bg-emerald-500/[0.08]"
|
||||||
|
: "border-white/10 bg-white/[0.03] hover:bg-white/[0.06]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="h-14 w-14 shrink-0 overflow-hidden rounded-xl border border-white/10 bg-black/30">
|
||||||
|
{project.thumbnail ? (
|
||||||
|
<img
|
||||||
|
src={buildImageProxyUrl(project.thumbnail, {
|
||||||
|
width: 160,
|
||||||
|
quality: 82,
|
||||||
|
format: "webp",
|
||||||
|
})}
|
||||||
|
alt={project.title}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<FolderOpen className="h-5 w-5 text-white/25" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"truncate text-sm font-medium",
|
||||||
|
project.id === currentProjectId ? "text-emerald-200" : "text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{project.title || "未命名生图项目"}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-white/45">
|
||||||
|
{formatDistanceToNow(new Date(project.updatedAt), {
|
||||||
|
addSuffix: true,
|
||||||
|
locale: zhCN,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 shrink-0 text-white/45 opacity-0 transition-opacity hover:text-rose-300 group-hover:opacity-100"
|
||||||
|
disabled={pendingDeleteId === project.id}
|
||||||
|
onClick={async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (!window.confirm("确定删除这个生图项目吗?")) return;
|
||||||
|
setPendingDeleteId(project.id);
|
||||||
|
try {
|
||||||
|
await onDeleteProject(project.id);
|
||||||
|
await loadProjects();
|
||||||
|
} finally {
|
||||||
|
setPendingDeleteId(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{pendingDeleteId === project.id ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] px-4 py-12 text-center">
|
||||||
|
<div className="text-sm text-white/70">还没有生图项目</div>
|
||||||
|
<div className="mt-2 text-xs text-white/40">点击上方按钮创建一个独立项目。</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo, type ReactNode } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react";
|
import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -11,6 +11,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import {
|
import {
|
||||||
defaultAgentWorkflowUiConfig,
|
defaultAgentWorkflowUiConfig,
|
||||||
flattenAgentWorkflowSteps,
|
flattenAgentWorkflowSteps,
|
||||||
|
flattenVisibleAgentWorkflowSteps,
|
||||||
getEnabledAgentWorkflowStepTree,
|
getEnabledAgentWorkflowStepTree,
|
||||||
} from "@/lib/agents/workflow-ui-config";
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||||
@ -49,6 +50,7 @@ interface AgentWorkflowPanelDarkV2Props {
|
|||||||
siteLogo?: string | null;
|
siteLogo?: string | null;
|
||||||
siteLogoDark?: string | null;
|
siteLogoDark?: string | null;
|
||||||
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
|
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
|
||||||
|
topNavRightSlot?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSelectionSummary(params: {
|
function buildSelectionSummary(params: {
|
||||||
@ -56,7 +58,7 @@ function buildSelectionSummary(params: {
|
|||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
}) {
|
}) {
|
||||||
const result: AgentWorkflowComposerSelection[] = [];
|
const result: AgentWorkflowComposerSelection[] = [];
|
||||||
for (const { step } of flattenAgentWorkflowSteps(params.config.steps)) {
|
for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) {
|
||||||
const selectedOptionKey = params.selections[step.key];
|
const selectedOptionKey = params.selections[step.key];
|
||||||
if (!selectedOptionKey) continue;
|
if (!selectedOptionKey) continue;
|
||||||
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
||||||
@ -173,6 +175,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
siteLogo,
|
siteLogo,
|
||||||
siteLogoDark,
|
siteLogoDark,
|
||||||
showcaseConfig,
|
showcaseConfig,
|
||||||
|
topNavRightSlot,
|
||||||
}: AgentWorkflowPanelDarkV2Props) {
|
}: AgentWorkflowPanelDarkV2Props) {
|
||||||
const brandOptions = useMemo(
|
const brandOptions = useMemo(
|
||||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||||
@ -192,10 +195,10 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
}, [config]);
|
}, [config]);
|
||||||
const flattenedResolvedSteps = useMemo(
|
const flattenedResolvedSteps = useMemo(
|
||||||
() =>
|
() =>
|
||||||
flattenAgentWorkflowSteps(resolvedConfig.steps).filter(
|
flattenVisibleAgentWorkflowSteps(resolvedConfig.steps, selections).filter(
|
||||||
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
||||||
),
|
),
|
||||||
[resolvedConfig.steps]
|
[resolvedConfig.steps, selections]
|
||||||
);
|
);
|
||||||
|
|
||||||
const summary = useMemo(
|
const summary = useMemo(
|
||||||
@ -239,6 +242,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
siteLogo={siteLogo}
|
siteLogo={siteLogo}
|
||||||
siteLogoDark={siteLogoDark}
|
siteLogoDark={siteLogoDark}
|
||||||
activeKey="agent"
|
activeKey="agent"
|
||||||
|
rightSlot={topNavRightSlot}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto overscroll-contain pt-16">
|
<div className="flex-1 overflow-y-auto overscroll-contain pt-16">
|
||||||
|
|||||||
159
lib/agents/generation-project-state.ts
Normal file
159
lib/agents/generation-project-state.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
createEmptyAgentModeWorkspaceState,
|
||||||
|
normalizeAgentModeWorkspaceState,
|
||||||
|
} from "@/lib/agents/mode-workspace-state";
|
||||||
|
import {
|
||||||
|
createEmptyAgentWorkflowMaterialSelections,
|
||||||
|
normalizeAgentWorkflowMaterialSelections,
|
||||||
|
} from "@/lib/agents/workflow-material-config";
|
||||||
|
import type {
|
||||||
|
AgentGenerationProjectPersistedState,
|
||||||
|
AgentGenerationProjectWorkspaceState,
|
||||||
|
AgentWorkflowPersistedState,
|
||||||
|
} from "@/types/agent";
|
||||||
|
|
||||||
|
export function createEmptyAgentGenerationProjectWorkspaceState(): AgentGenerationProjectWorkspaceState {
|
||||||
|
return {
|
||||||
|
input: "",
|
||||||
|
attachments: [],
|
||||||
|
selectedModelId: "auto",
|
||||||
|
imageParams: {
|
||||||
|
aspectRatio: "auto",
|
||||||
|
imageSize: "2K",
|
||||||
|
imageCount: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEmptyAgentWorkflowPersistedState(): AgentWorkflowPersistedState {
|
||||||
|
return {
|
||||||
|
inputs: {
|
||||||
|
primarySku: "",
|
||||||
|
secondarySku: "",
|
||||||
|
brand: "",
|
||||||
|
store: "",
|
||||||
|
},
|
||||||
|
materials: createEmptyAgentWorkflowMaterialSelections(),
|
||||||
|
selections: {},
|
||||||
|
selectedBrandModel: null,
|
||||||
|
selectedAgentPresetId: null,
|
||||||
|
currentStage: "workflow",
|
||||||
|
agentModeState: createEmptyAgentModeWorkspaceState(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEmptyAgentGenerationProjectPersistedState(): AgentGenerationProjectPersistedState {
|
||||||
|
return {
|
||||||
|
workflowState: createEmptyAgentWorkflowPersistedState(),
|
||||||
|
workspaceState: createEmptyAgentGenerationProjectWorkspaceState(),
|
||||||
|
resultImageUrls: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorkspaceState(value: unknown): AgentGenerationProjectWorkspaceState {
|
||||||
|
const raw =
|
||||||
|
value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const attachments = Array.isArray(raw.attachments)
|
||||||
|
? raw.attachments
|
||||||
|
.map((item) => {
|
||||||
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const record = item as Record<string, unknown>;
|
||||||
|
const url = String(record.url || "").trim();
|
||||||
|
if (!url) return null;
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
name: String(record.name || "").trim() || null,
|
||||||
|
contentType: String(record.contentType || "").trim() || null,
|
||||||
|
assetId: String(record.assetId || "").trim() || null,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.slice(0, 16)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const rawImageParams =
|
||||||
|
raw.imageParams && typeof raw.imageParams === "object" && !Array.isArray(raw.imageParams)
|
||||||
|
? (raw.imageParams as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const imageCount = Number(rawImageParams.imageCount);
|
||||||
|
|
||||||
|
return {
|
||||||
|
input: String(raw.input || "").trim(),
|
||||||
|
attachments,
|
||||||
|
selectedModelId: String(raw.selectedModelId || "").trim() || "auto",
|
||||||
|
imageParams: {
|
||||||
|
aspectRatio: String(rawImageParams.aspectRatio || "").trim() || "auto",
|
||||||
|
imageSize: String(rawImageParams.imageSize || "").trim() || "2K",
|
||||||
|
imageCount: Number.isFinite(imageCount) ? Math.max(1, Math.min(8, Math.round(imageCount))) : 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorkflowState(value: unknown): AgentWorkflowPersistedState {
|
||||||
|
const raw =
|
||||||
|
value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputs: {
|
||||||
|
primarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).primarySku || "" : "").trim(),
|
||||||
|
secondarySku: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).secondarySku || "" : "").trim(),
|
||||||
|
brand: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).brand || "" : "").trim(),
|
||||||
|
store: String(raw.inputs && typeof raw.inputs === "object" ? (raw.inputs as Record<string, unknown>).store || "" : "").trim(),
|
||||||
|
},
|
||||||
|
materials: normalizeAgentWorkflowMaterialSelections(raw.materials),
|
||||||
|
selections:
|
||||||
|
raw.selections && typeof raw.selections === "object" && !Array.isArray(raw.selections)
|
||||||
|
? (raw.selections as Record<string, string>)
|
||||||
|
: {},
|
||||||
|
selectedBrandModel:
|
||||||
|
raw.selectedBrandModel && typeof raw.selectedBrandModel === "object" && !Array.isArray(raw.selectedBrandModel)
|
||||||
|
? {
|
||||||
|
...(raw.selectedBrandModel as Record<string, unknown>),
|
||||||
|
brandName: String((raw.selectedBrandModel as Record<string, unknown>).brandName || "").trim(),
|
||||||
|
id: String((raw.selectedBrandModel as Record<string, unknown>).id || "").trim(),
|
||||||
|
imageUrl: String((raw.selectedBrandModel as Record<string, unknown>).imageUrl || "").trim(),
|
||||||
|
assetId: String((raw.selectedBrandModel as Record<string, unknown>).assetId || "").trim() || null,
|
||||||
|
displayName: String((raw.selectedBrandModel as Record<string, unknown>).displayName || "").trim() || null,
|
||||||
|
mimeType: String((raw.selectedBrandModel as Record<string, unknown>).mimeType || "").trim() || null,
|
||||||
|
sortOrder: Number.isFinite(Number((raw.selectedBrandModel as Record<string, unknown>).sortOrder))
|
||||||
|
? Math.max(0, Math.min(999, Math.round(Number((raw.selectedBrandModel as Record<string, unknown>).sortOrder))))
|
||||||
|
: 0,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
selectedAgentPresetId: String(raw.selectedAgentPresetId || "").trim() || null,
|
||||||
|
currentStage: raw.currentStage === "mode" ? "mode" : "workflow",
|
||||||
|
agentModeState: normalizeAgentModeWorkspaceState(raw.agentModeState),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAgentGenerationProjectPersistedState(
|
||||||
|
value: unknown
|
||||||
|
): AgentGenerationProjectPersistedState {
|
||||||
|
const raw =
|
||||||
|
value && typeof value === "object" && !Array.isArray(value)
|
||||||
|
? (value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const resultImageUrls = Array.isArray(raw.resultImageUrls)
|
||||||
|
? Array.from(
|
||||||
|
new Set(
|
||||||
|
raw.resultImageUrls
|
||||||
|
.map((item) => String(item || "").trim())
|
||||||
|
.filter((item) => /^https?:\/\//i.test(item))
|
||||||
|
)
|
||||||
|
).slice(0, 64)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
workflowState: normalizeWorkflowState(raw.workflowState),
|
||||||
|
workspaceState: normalizeWorkspaceState(raw.workspaceState),
|
||||||
|
resultImageUrls,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
defaultAgentWorkflowUiConfig,
|
defaultAgentWorkflowUiConfig,
|
||||||
flattenAgentWorkflowSteps,
|
flattenVisibleAgentWorkflowSteps,
|
||||||
getEnabledAgentWorkflowStepTree,
|
getEnabledAgentWorkflowStepTree,
|
||||||
} from "@/lib/agents/workflow-ui-config";
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import type {
|
import type {
|
||||||
@ -27,8 +27,9 @@ export function buildAgentWorkflowComposerContext(params: {
|
|||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
}): AgentWorkflowComposerContext | null {
|
}): AgentWorkflowComposerContext | null {
|
||||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||||
const steps = flattenAgentWorkflowSteps(
|
const steps = flattenVisibleAgentWorkflowSteps(
|
||||||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
|
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
|
||||||
|
params.selections
|
||||||
)
|
)
|
||||||
.map(({ step }) => step)
|
.map(({ step }) => step)
|
||||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||||
@ -223,8 +224,9 @@ export function getAgentWorkflowProgress(params: {
|
|||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
}) {
|
}) {
|
||||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||||
const enabledSteps = flattenAgentWorkflowSteps(
|
const enabledSteps = flattenVisibleAgentWorkflowSteps(
|
||||||
getEnabledAgentWorkflowStepTree(config.steps || [])
|
getEnabledAgentWorkflowStepTree(config.steps || []),
|
||||||
|
params.selections
|
||||||
)
|
)
|
||||||
.map(({ step }) => step)
|
.map(({ step }) => step)
|
||||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||||
|
|||||||
@ -153,6 +153,47 @@ export function flattenAgentWorkflowSteps(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function flattenVisibleAgentWorkflowSteps(
|
||||||
|
steps: AgentWorkflowStepUiConfig[],
|
||||||
|
selections: Partial<Record<string, string>>,
|
||||||
|
depth = 1,
|
||||||
|
parentKeys: string[] = [],
|
||||||
|
ancestorsSatisfied = true
|
||||||
|
): FlattenedAgentWorkflowStep[] {
|
||||||
|
return [...steps]
|
||||||
|
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||||
|
.flatMap((step) => {
|
||||||
|
const normalizedStep = {
|
||||||
|
...step,
|
||||||
|
options: [...(step.options || [])].sort((left, right) => left.sortOrder - right.sortOrder),
|
||||||
|
children: [...(step.children || [])].sort((left, right) => left.sortOrder - right.sortOrder),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ancestorsSatisfied) {
|
||||||
|
return [] as FlattenedAgentWorkflowStep[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEntry: FlattenedAgentWorkflowStep = {
|
||||||
|
step: normalizedStep,
|
||||||
|
depth,
|
||||||
|
parentKeys,
|
||||||
|
};
|
||||||
|
const currentSatisfied =
|
||||||
|
normalizedStep.options.length === 0 || Boolean(String(selections[normalizedStep.key] || "").trim());
|
||||||
|
|
||||||
|
return [
|
||||||
|
currentEntry,
|
||||||
|
...flattenVisibleAgentWorkflowSteps(
|
||||||
|
normalizedStep.children,
|
||||||
|
selections,
|
||||||
|
depth + 1,
|
||||||
|
[...parentKeys, normalizedStep.key],
|
||||||
|
currentSatisfied
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function findAgentWorkflowStepByKey(
|
export function findAgentWorkflowStepByKey(
|
||||||
steps: AgentWorkflowStepUiConfig[],
|
steps: AgentWorkflowStepUiConfig[],
|
||||||
stepKey: string
|
stepKey: string
|
||||||
|
|||||||
80
services/agent-projects/index.ts
Normal file
80
services/agent-projects/index.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import type {
|
||||||
|
AgentGenerationProject,
|
||||||
|
AgentGenerationProjectPersistedState,
|
||||||
|
} from "@/types/agent";
|
||||||
|
|
||||||
|
interface ApiResponse<T = unknown> {
|
||||||
|
success: boolean;
|
||||||
|
data?: T;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agentProjectService = {
|
||||||
|
async getProjects(): Promise<ApiResponse<AgentGenerationProject[]>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/agent-projects", { cache: "no-store" });
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: "获取生图项目列表失败" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getProject(id: string): Promise<ApiResponse<AgentGenerationProject>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/agent-projects/${id}`, { cache: "no-store" });
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: "获取生图项目失败" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async createProject(payload: {
|
||||||
|
title?: string;
|
||||||
|
description?: string | null;
|
||||||
|
thumbnail?: string | null;
|
||||||
|
projectState?: AgentGenerationProjectPersistedState;
|
||||||
|
}): Promise<ApiResponse<AgentGenerationProject>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/agent-projects", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: "创建生图项目失败" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateProject(
|
||||||
|
id: string,
|
||||||
|
payload: {
|
||||||
|
title?: string;
|
||||||
|
description?: string | null;
|
||||||
|
thumbnail?: string | null;
|
||||||
|
projectState?: AgentGenerationProjectPersistedState;
|
||||||
|
}
|
||||||
|
): Promise<ApiResponse<AgentGenerationProject>> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/agent-projects/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: "保存生图项目失败" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteProject(id: string): Promise<ApiResponse> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/agent-projects/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: "删除生图项目失败" };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -238,6 +238,26 @@ export interface AgentModeWorkspaceState {
|
|||||||
skuReplacement: AgentSkuReplacementState;
|
skuReplacement: AgentSkuReplacementState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentGenerationProjectWorkspaceAttachment {
|
||||||
|
url: string;
|
||||||
|
name?: string | null;
|
||||||
|
contentType?: string | null;
|
||||||
|
assetId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentGenerationProjectImageParams {
|
||||||
|
aspectRatio?: string;
|
||||||
|
imageSize?: string;
|
||||||
|
imageCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentGenerationProjectWorkspaceState {
|
||||||
|
input: string;
|
||||||
|
attachments: Array<AgentGenerationProjectWorkspaceAttachment | null>;
|
||||||
|
selectedModelId?: string | null;
|
||||||
|
imageParams?: AgentGenerationProjectImageParams;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentWorkflowPersistedState {
|
export interface AgentWorkflowPersistedState {
|
||||||
inputs: {
|
inputs: {
|
||||||
primarySku: string;
|
primarySku: string;
|
||||||
@ -253,6 +273,23 @@ export interface AgentWorkflowPersistedState {
|
|||||||
agentModeState?: AgentModeWorkspaceState;
|
agentModeState?: AgentModeWorkspaceState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentGenerationProjectPersistedState {
|
||||||
|
workflowState: AgentWorkflowPersistedState;
|
||||||
|
workspaceState?: AgentGenerationProjectWorkspaceState;
|
||||||
|
resultImageUrls?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentGenerationProject {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
title: string;
|
||||||
|
description?: string | null;
|
||||||
|
thumbnail?: string | null;
|
||||||
|
projectState: AgentGenerationProjectPersistedState;
|
||||||
|
createdAt: Date | string;
|
||||||
|
updatedAt: Date | string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentCategoryTreeConfig {
|
export interface AgentCategoryTreeConfig {
|
||||||
productCategories: string[];
|
productCategories: string[];
|
||||||
audienceTags: string[];
|
audienceTags: string[];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user