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 { canvasProject } from '@/lib/db/schema';
|
||||
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) {
|
||||
@ -16,7 +16,12 @@ export async function GET(request: NextRequest) {
|
||||
const projects = await db
|
||||
.select()
|
||||
.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));
|
||||
|
||||
return NextResponse.json({ success: true, data: projects });
|
||||
|
||||
@ -70,6 +70,7 @@ import { AgentPresetPanelNew } from "@/components/agents/AgentPresetPanelNew";
|
||||
import { AgentPresetPanelDarkV2 } from "@/components/agents/AgentPresetPanelDarkV2";
|
||||
import { AgentWorkflowPanelDarkV2 } from "@/components/agents/AgentWorkflowPanelDarkV2";
|
||||
import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2";
|
||||
import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet";
|
||||
import { CanvasWorkspace } from "@/components/canvas";
|
||||
import { CanvasChatPanel } from "@/components/canvas/CanvasChatPanel";
|
||||
import { CanvasFileListPanel } from "@/components/canvas/CanvasFileListPanel";
|
||||
@ -94,7 +95,14 @@ import {
|
||||
createEmptyAgentModeWorkspaceState,
|
||||
normalizeAgentModeWorkspaceState,
|
||||
} from "@/lib/agents/mode-workspace-state";
|
||||
import {
|
||||
createEmptyAgentGenerationProjectPersistedState,
|
||||
createEmptyAgentGenerationProjectWorkspaceState,
|
||||
normalizeAgentGenerationProjectPersistedState,
|
||||
} from "@/lib/agents/generation-project-state";
|
||||
import { agentProjectService } from "@/services/agent-projects";
|
||||
import type {
|
||||
AgentGenerationProjectWorkspaceState,
|
||||
AgentModeWorkspaceState,
|
||||
AgentWorkflowComposerContext,
|
||||
AgentWorkflowMaterialSelections,
|
||||
@ -142,6 +150,15 @@ function buildScopedCanvasChatId(projectId: string, scope: "agent-mode") {
|
||||
].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) {
|
||||
// 使用 useParams 确保客户端导航时能正确获取更新的 id
|
||||
const routeParams = useParams();
|
||||
@ -212,10 +229,25 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
const [agentModeState, setAgentModeState] = useState<AgentModeWorkspaceState>(
|
||||
() => 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<
|
||||
"pending" | "project" | "local" | "empty"
|
||||
>("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<{
|
||||
siteName?: string;
|
||||
@ -290,6 +322,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
updateCanvasSearchParams({
|
||||
mode: null,
|
||||
agentPresetId: null,
|
||||
agentProjectId: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -297,10 +330,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
updateCanvasSearchParams({
|
||||
mode: "agent",
|
||||
agentPresetId: null,
|
||||
agentProjectId: agentProjectId || null,
|
||||
});
|
||||
setIsPanelOpen(true);
|
||||
setPanelMode("chat");
|
||||
}, [isAgentMode, updateCanvasSearchParams]);
|
||||
}, [agentProjectId, isAgentMode, updateCanvasSearchParams]);
|
||||
|
||||
// 加载画布数据
|
||||
useLayoutEffect(() => {
|
||||
@ -320,6 +354,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
setSelectedBrandModel(null);
|
||||
setSelectedAgentPresetId(null);
|
||||
setAgentModeState(createEmptyAgentModeWorkspaceState());
|
||||
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
|
||||
setAgentProjectTitle("未命名生图项目");
|
||||
setAgentProjectThumbnail(null);
|
||||
setAgentProjectResultUrls([]);
|
||||
|
||||
// 🆕 先清空当前画布状态,确保切换时不会显示旧数据
|
||||
useCanvasStore.getState().clearCanvas();
|
||||
@ -331,7 +369,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
if (result.data.canvasData) {
|
||||
useCanvasStore.getState().loadCanvas(result.data.canvasData);
|
||||
const persistedWorkflow = result.data.canvasData.workflowState;
|
||||
if (persistedWorkflow) {
|
||||
if (!requestedAgentMode && persistedWorkflow) {
|
||||
setWorkflowInputs({
|
||||
primarySku: String(persistedWorkflow.inputs?.primarySku || "").trim(),
|
||||
secondarySku: String(persistedWorkflow.inputs?.secondarySku || "").trim(),
|
||||
@ -388,20 +426,22 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
} else {
|
||||
// 画布不存在时,创建一个新画布
|
||||
console.log('[Canvas] 画布不存在,创建新画布:', id);
|
||||
useCanvasStore.getState().setProject(id, '未命名画布');
|
||||
useCanvasStore.getState().setProject(id, requestedAgentMode ? '智能体工作台' : '未命名画布');
|
||||
setWorkflowHydrationSource("empty");
|
||||
// 保存新画布到数据库
|
||||
try {
|
||||
await canvasService.saveCanvasState(id, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: 'select' });
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 创建新画布失败:', error);
|
||||
if (!requestedAgentMode) {
|
||||
// 保存新画布到数据库
|
||||
try {
|
||||
await canvasService.saveCanvasState(id, { nodes: [], edges: [], selectedNodeIds: [], viewport: { x: 0, y: 0, zoom: 1 }, tool: 'select' });
|
||||
} catch (error) {
|
||||
console.error('[Canvas] 创建新画布失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
loadProject();
|
||||
}, [id]); // 只依赖 id,使用 getState() 直接调用 store actions
|
||||
}, [id, requestedAgentMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const updateViewportWidth = () => setViewportWidth(window.innerWidth);
|
||||
@ -425,6 +465,14 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
|
||||
const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]);
|
||||
const buildPersistedCanvasData = useCallback(() => {
|
||||
return {
|
||||
...getCanvasState(),
|
||||
};
|
||||
}, [
|
||||
getCanvasState,
|
||||
]);
|
||||
|
||||
const buildPersistedAgentProjectState = useCallback(() => {
|
||||
const workflowState: AgentWorkflowPersistedState = {
|
||||
inputs: {
|
||||
primarySku: workflowInputs.primarySku.trim(),
|
||||
@ -441,21 +489,24 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
};
|
||||
|
||||
return {
|
||||
...getCanvasState(),
|
||||
workflowState,
|
||||
workspaceState: agentWorkspaceState,
|
||||
resultImageUrls: agentProjectResultUrls,
|
||||
};
|
||||
}, [
|
||||
agentModeState,
|
||||
agentProjectResultUrls,
|
||||
agentWorkspaceState,
|
||||
currentAgentStage,
|
||||
getCanvasState,
|
||||
selectedAgentPresetId,
|
||||
selectedBrandModel,
|
||||
agentModeState,
|
||||
workflowInputs,
|
||||
workflowMaterials,
|
||||
workflowSelections,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAgentMode) return;
|
||||
if (typeof window === "undefined") return;
|
||||
if (workflowHydrationSource !== "empty") return;
|
||||
|
||||
@ -499,9 +550,10 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
} finally {
|
||||
setWorkflowHydrationSource("local");
|
||||
}
|
||||
}, [workflowHydrationSource, workflowStorageKey]);
|
||||
}, [isAgentMode, workflowHydrationSource, workflowStorageKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAgentMode) return;
|
||||
if (typeof window === "undefined" || workflowHydrationSource === "pending") return;
|
||||
|
||||
try {
|
||||
@ -521,6 +573,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
console.warn("[Canvas] Failed to persist workflow state:", error);
|
||||
}
|
||||
}, [
|
||||
isAgentMode,
|
||||
workflowHydrationSource,
|
||||
workflowInputs,
|
||||
workflowMaterials,
|
||||
@ -532,6 +585,247 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
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(() => {
|
||||
if (!selectedBrandModel) return;
|
||||
const normalizedBrand = workflowInputs.brand.trim().toLowerCase();
|
||||
@ -558,7 +852,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
|
||||
// 自动保存逻辑
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (isLoading || isAgentMode) return;
|
||||
|
||||
const saveTimer = setTimeout(async () => {
|
||||
setIsSaving(true);
|
||||
@ -571,11 +865,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
}, 3000); // 3秒自动保存
|
||||
|
||||
return () => clearTimeout(saveTimer);
|
||||
}, [nodes, workflowInputs, workflowSelections, isLoading, persistCanvasState]);
|
||||
}, [isAgentMode, nodes, isLoading, persistCanvasState]);
|
||||
|
||||
// 删除节点时立即保存(按节点ID差集检测),避免删除后快速刷新被回滚
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
if (isLoading || isAgentMode) {
|
||||
previousNodeIdsRef.current = new Set(nodes.map((node) => node.id));
|
||||
return;
|
||||
}
|
||||
@ -602,11 +896,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
};
|
||||
void persistAfterDelete();
|
||||
}
|
||||
}, [nodes, isLoading, persistCanvasState]);
|
||||
}, [isAgentMode, nodes, isLoading, persistCanvasState]);
|
||||
|
||||
// 页面关闭/刷新前尝试做一次 keepalive 保存,降低“刚删就刷新”回滚概率
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (isLoading || isAgentMode) return;
|
||||
|
||||
const handleBeforeUnload = () => {
|
||||
try {
|
||||
@ -628,10 +922,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
window.removeEventListener("pagehide", handleBeforeUnload);
|
||||
};
|
||||
}, [buildPersistedCanvasData, id, isLoading]);
|
||||
}, [buildPersistedCanvasData, id, isAgentMode, isLoading]);
|
||||
|
||||
// 智能命名与缩略图逻辑
|
||||
useEffect(() => {
|
||||
if (isAgentMode) return;
|
||||
const autoTitleAndThumbnail = async () => {
|
||||
// 如果标题是默认值,且有图片节点
|
||||
if ((projectTitle === "未命名画布" || projectTitle === "新画布") && nodes.length > 0) {
|
||||
@ -664,7 +959,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
// 使用防抖,避免频繁触发
|
||||
const timer = setTimeout(autoTitleAndThumbnail, 5000); // 5秒后触发,给用户一点时间操作
|
||||
return () => clearTimeout(timer);
|
||||
}, [nodes, projectTitle, id, setProjectTitle]);
|
||||
}, [id, isAgentMode, nodes, projectTitle, setProjectTitle]);
|
||||
|
||||
// 保存画布 (手动触发保留作为备用,但UI上移除)
|
||||
const handleSave = useCallback(async () => {
|
||||
@ -1039,6 +1334,34 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
</div>
|
||||
) : 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) {
|
||||
// 深色主题智能体模式 V2 - 完整功能版本
|
||||
|
||||
@ -1057,6 +1380,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
siteLogo={siteSettingsData?.siteLogo}
|
||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||
showcaseConfig={siteSettingsData?.agentWorkflowShowcaseConfig}
|
||||
topNavRightSlot={agentProjectNavSlot}
|
||||
onNextStep={() => setCurrentAgentStage("mode")}
|
||||
onSkipStep={() => setCurrentAgentStage("mode")}
|
||||
onReset={() => {
|
||||
@ -1066,6 +1390,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
setSelectedBrandModel(null);
|
||||
setSelectedAgentPresetId(null);
|
||||
setAgentModeState(createEmptyAgentModeWorkspaceState());
|
||||
setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState());
|
||||
setCurrentAgentStage("workflow");
|
||||
}}
|
||||
/>
|
||||
@ -1082,9 +1407,18 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
workflowContext={workflowContext}
|
||||
agentModeState={agentModeState}
|
||||
onAgentModeStateChange={setAgentModeState}
|
||||
workspaceState={agentWorkspaceState}
|
||||
onWorkspaceStateChange={setAgentWorkspaceState}
|
||||
onResultPreviewUrlsChange={(urls) => {
|
||||
setAgentProjectResultUrls(urls);
|
||||
if (urls[0]) {
|
||||
setAgentProjectThumbnail(urls[0]);
|
||||
}
|
||||
}}
|
||||
siteName={siteSettingsData?.siteName}
|
||||
siteLogo={siteSettingsData?.siteLogo}
|
||||
siteLogoDark={siteSettingsData?.siteLogoDark}
|
||||
topNavRightSlot={agentProjectNavSlot}
|
||||
syncGeneratedResultsToCanvas={false}
|
||||
onBack={() => setCurrentAgentStage("workflow")}
|
||||
onOpenCanvasEditor={() => {
|
||||
|
||||
@ -42,6 +42,7 @@ import type { Attachment, ChatMessage } from "@/lib/types";
|
||||
import { cn, fetchWithErrorHandlers, fetcher, generateUUID } from "@/lib/utils";
|
||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||
import type {
|
||||
AgentGenerationProjectWorkspaceState,
|
||||
AgentModeReferenceCandidate,
|
||||
AgentModeWorkspaceState,
|
||||
AgentPreset,
|
||||
@ -52,6 +53,10 @@ import type {
|
||||
import {
|
||||
createEmptyAgentModeWorkspaceState,
|
||||
} from "@/lib/agents/mode-workspace-state";
|
||||
import {
|
||||
createEmptyAgentGenerationProjectWorkspaceState,
|
||||
normalizeAgentGenerationProjectPersistedState,
|
||||
} from "@/lib/agents/generation-project-state";
|
||||
|
||||
interface AgentGenerationWorkspaceDarkV2Props {
|
||||
chatId: string;
|
||||
@ -71,6 +76,10 @@ interface AgentGenerationWorkspaceDarkV2Props {
|
||||
agentModeState?: AgentModeWorkspaceState | null;
|
||||
onAgentModeStateChange?: (next: AgentModeWorkspaceState) => void;
|
||||
onOpenCanvasEditor?: () => void;
|
||||
workspaceState?: AgentGenerationProjectWorkspaceState | null;
|
||||
onWorkspaceStateChange?: (next: AgentGenerationProjectWorkspaceState) => void;
|
||||
onResultPreviewUrlsChange?: (urls: string[]) => void;
|
||||
topNavRightSlot?: ReactNode;
|
||||
}
|
||||
|
||||
interface PickedAttachment extends Attachment {}
|
||||
@ -244,6 +253,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
agentModeState = null,
|
||||
onAgentModeStateChange,
|
||||
onOpenCanvasEditor,
|
||||
workspaceState = null,
|
||||
onWorkspaceStateChange,
|
||||
onResultPreviewUrlsChange,
|
||||
topNavRightSlot,
|
||||
}: AgentGenerationWorkspaceDarkV2Props) {
|
||||
const rawChatId = useMemo(
|
||||
() => (chatId.startsWith("canvas-") ? chatId.replace("canvas-", "") : chatId),
|
||||
@ -257,15 +270,38 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const hiddenPromptRef = useRef<string | null>(null);
|
||||
const generatedImageKeysRef = useRef<Set<string>>(new Set());
|
||||
const lastSyncedBrandModelUrlRef = useRef<string | null>(null);
|
||||
const workspaceStateSignatureRef = useRef<string>("");
|
||||
const [localAgentModeState, setLocalAgentModeState] = useState<AgentModeWorkspaceState>(
|
||||
() => createEmptyAgentModeWorkspaceState()
|
||||
);
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [attachments, setAttachments] = useState<Array<PickedAttachment | null>>([]);
|
||||
const normalizedIncomingWorkspaceState = useMemo(
|
||||
() =>
|
||||
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 [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 [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
|
||||
const [batchDownloadState, setBatchDownloadState] = useState<null | "scene" | "detail" | "sku">(null);
|
||||
@ -284,9 +320,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
lastQuery: null,
|
||||
});
|
||||
const [imageParams, setImageParams] = useState<ImageGenerationParams>({
|
||||
aspectRatio: "auto",
|
||||
imageSize: "2K",
|
||||
imageCount: 1,
|
||||
aspectRatio: normalizedIncomingWorkspaceState.imageParams?.aspectRatio || "auto",
|
||||
imageSize: normalizedIncomingWorkspaceState.imageParams?.imageSize || "2K",
|
||||
imageCount: normalizedIncomingWorkspaceState.imageParams?.imageCount || 1,
|
||||
});
|
||||
|
||||
const { data: historyData } = useSWR<{ messages: ChatMessage[] }>(
|
||||
@ -514,6 +550,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}, [historyMessages, messages.length, setMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (normalizedIncomingWorkspaceState.selectedModelId) {
|
||||
setSelectedModelId(normalizedIncomingWorkspaceState.selectedModelId);
|
||||
return;
|
||||
}
|
||||
if (selectedAgentPreset?.defaultModelId) {
|
||||
setSelectedModelId(selectedAgentPreset.defaultModelId);
|
||||
return;
|
||||
@ -521,7 +561,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
if (modelsData?.defaultModelId) {
|
||||
setSelectedModelId(modelsData.defaultModelId);
|
||||
}
|
||||
}, [modelsData?.defaultModelId, selectedAgentPreset?.defaultModelId]);
|
||||
}, [
|
||||
modelsData?.defaultModelId,
|
||||
normalizedIncomingWorkspaceState.selectedModelId,
|
||||
selectedAgentPreset?.defaultModelId,
|
||||
]);
|
||||
|
||||
// 初始化附件
|
||||
useEffect(() => {
|
||||
@ -761,6 +805,71 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const nextToolStep =
|
||||
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(() => {
|
||||
if (!selectedResultUrl && latestGeneratedGroup?.imageUrls[0]) {
|
||||
setSelectedResultUrl(latestGeneratedGroup.imageUrls[0]);
|
||||
@ -1323,7 +1432,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
siteLogoDark={siteLogoDark}
|
||||
onBack={onBack}
|
||||
activeKey="agent"
|
||||
rightSlot={<div className="w-10" />}
|
||||
rightSlot={topNavRightSlot || <div className="w-10" />}
|
||||
/>
|
||||
|
||||
{/* 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";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@ -11,6 +11,7 @@ import { cn } from "@/lib/utils";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenAgentWorkflowSteps,
|
||||
flattenVisibleAgentWorkflowSteps,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||
@ -49,6 +50,7 @@ interface AgentWorkflowPanelDarkV2Props {
|
||||
siteLogo?: string | null;
|
||||
siteLogoDark?: string | null;
|
||||
showcaseConfig?: AgentWorkflowShowcaseConfig | null;
|
||||
topNavRightSlot?: ReactNode;
|
||||
}
|
||||
|
||||
function buildSelectionSummary(params: {
|
||||
@ -56,7 +58,7 @@ function buildSelectionSummary(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}) {
|
||||
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];
|
||||
if (!selectedOptionKey) continue;
|
||||
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
||||
@ -173,6 +175,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
siteLogo,
|
||||
siteLogoDark,
|
||||
showcaseConfig,
|
||||
topNavRightSlot,
|
||||
}: AgentWorkflowPanelDarkV2Props) {
|
||||
const brandOptions = useMemo(
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
@ -192,10 +195,10 @@ export function AgentWorkflowPanelDarkV2({
|
||||
}, [config]);
|
||||
const flattenedResolvedSteps = useMemo(
|
||||
() =>
|
||||
flattenAgentWorkflowSteps(resolvedConfig.steps).filter(
|
||||
flattenVisibleAgentWorkflowSteps(resolvedConfig.steps, selections).filter(
|
||||
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
||||
),
|
||||
[resolvedConfig.steps]
|
||||
[resolvedConfig.steps, selections]
|
||||
);
|
||||
|
||||
const summary = useMemo(
|
||||
@ -239,6 +242,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
siteLogo={siteLogo}
|
||||
siteLogoDark={siteLogoDark}
|
||||
activeKey="agent"
|
||||
rightSlot={topNavRightSlot}
|
||||
/>
|
||||
|
||||
<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 {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenAgentWorkflowSteps,
|
||||
flattenVisibleAgentWorkflowSteps,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import type {
|
||||
@ -27,8 +27,9 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}): AgentWorkflowComposerContext | null {
|
||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||
const steps = flattenAgentWorkflowSteps(
|
||||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
|
||||
const steps = flattenVisibleAgentWorkflowSteps(
|
||||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
|
||||
params.selections
|
||||
)
|
||||
.map(({ step }) => step)
|
||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||
@ -223,8 +224,9 @@ export function getAgentWorkflowProgress(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}) {
|
||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||
const enabledSteps = flattenAgentWorkflowSteps(
|
||||
getEnabledAgentWorkflowStepTree(config.steps || [])
|
||||
const enabledSteps = flattenVisibleAgentWorkflowSteps(
|
||||
getEnabledAgentWorkflowStepTree(config.steps || []),
|
||||
params.selections
|
||||
)
|
||||
.map(({ step }) => step)
|
||||
.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(
|
||||
steps: AgentWorkflowStepUiConfig[],
|
||||
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;
|
||||
}
|
||||
|
||||
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 {
|
||||
inputs: {
|
||||
primarySku: string;
|
||||
@ -253,6 +273,23 @@ export interface AgentWorkflowPersistedState {
|
||||
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 {
|
||||
productCategories: string[];
|
||||
audienceTags: string[];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user