diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index fe1de3029..83cbec17e 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -23,9 +23,11 @@ import { Wand2, Grid3X3, Settings, + Pencil, PenTool, Box, Layers, + FolderOpen, Crosshair, Scan, } from "lucide-react"; @@ -190,6 +192,17 @@ function buildDefaultAgentProjectTitle() { return `生图项目 ${month}-${day} ${hour}:${minute}`; } +function buildSaveAsAgentProjectTitle(currentTitle?: string | null) { + const normalized = String(currentTitle || "").trim(); + if (!normalized) { + return `${buildDefaultAgentProjectTitle()} 副本`; + } + if (normalized.endsWith("副本")) { + return normalized; + } + return `${normalized} 副本`; +} + function areStringArraysEqual(left: string[], right: string[]) { if (left === right) return true; if (left.length !== right.length) return false; @@ -803,6 +816,7 @@ export default function CanvasEditorPage({ params }: PageProps) { throw new Error(result.error || "创建生图项目失败"); } if (cancelled) return; + toast.success("已为你创建新项目,可直接开始生图"); updateCanvasSearchParams({ mode: "agent", agentProjectId: result.data.id, @@ -919,15 +933,9 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowSelections, ]); - const handleManualSaveAgentProject = useCallback(async () => { - if (!agentProjectId) { - toast.error("当前还没有可保存的生图项目"); - return; - } - - const suggestedTitle = - String(agentProjectTitle || "").trim() || buildDefaultAgentProjectTitle(); - const promptedTitle = window.prompt("请输入要保存的项目名称", suggestedTitle); + const handleSaveAsAgentProject = useCallback(async () => { + const suggestedTitle = buildSaveAsAgentProjectTitle(agentProjectTitle); + const promptedTitle = window.prompt("请输入另存为的新项目名称", suggestedTitle); if (promptedTitle === null) { return; } @@ -939,16 +947,36 @@ export default function CanvasEditorPage({ params }: PageProps) { setIsAgentProjectSaving(true); try { - const success = await persistAgentProject("manual", { title: nextTitle }); - if (success) { - toast.success("生图项目已保存"); - } else { - toast.error("保存生图项目失败"); + if (agentProjectId) { + await persistAgentProject("switch"); } + + const result = await agentProjectService.createProject({ + title: nextTitle, + thumbnail: agentProjectThumbnail, + projectState: buildPersistedAgentProjectState(), + }); + if (!result.success || !result.data) { + toast.error(result.error || "另存为新项目失败"); + return; + } + + toast.success("已另存为新项目"); + updateCanvasSearchParams({ + mode: "agent", + agentProjectId: result.data.id, + }); } finally { setIsAgentProjectSaving(false); } - }, [agentProjectId, persistAgentProject]); + }, [ + agentProjectId, + agentProjectThumbnail, + agentProjectTitle, + buildPersistedAgentProjectState, + persistAgentProject, + updateCanvasSearchParams, + ]); const handleRenameAgentProject = useCallback( async (targetProjectId: string, currentTitle?: string | null) => { @@ -980,6 +1008,15 @@ export default function CanvasEditorPage({ params }: PageProps) { [agentProjectId] ); + const handleRenameCurrentAgentProject = useCallback(async () => { + if (!agentProjectId) { + toast.error("当前还没有可重命名的项目"); + return; + } + + await handleRenameAgentProject(agentProjectId, agentProjectTitle); + }, [agentProjectId, agentProjectTitle, handleRenameAgentProject]); + const handleCreateAgentProject = useCallback(async () => { if (agentProjectId) { await persistAgentProject("switch"); @@ -987,20 +1024,20 @@ export default function CanvasEditorPage({ params }: PageProps) { const result = await agentProjectService.createProject({ title: buildDefaultAgentProjectTitle(), - projectState: buildPersistedAgentProjectState(), + projectState: createEmptyAgentGenerationProjectPersistedState(), }); if (!result.success || !result.data) { toast.error(result.error || "创建生图项目失败"); return; } + toast.success("已新建空白项目"); updateCanvasSearchParams({ mode: "agent", agentProjectId: result.data.id, }); }, [ agentProjectId, - buildPersistedAgentProjectState, persistAgentProject, updateCanvasSearchParams, ]); @@ -1607,6 +1644,17 @@ export default function CanvasEditorPage({ params }: PageProps) { }), [resolvedWorkflowUiConfig, workflowSelections] ); + const resolvedAgentProjectTitle = + String(agentProjectTitle || "").trim() || "未命名生图项目"; + const agentProjectStatusLabel = isAgentProjectLoading + ? hasActiveAgentProject + ? "项目加载中..." + : "正在创建新项目..." + : isAgentProjectSaving + ? "自动保存中..." + : hasActiveAgentProject + ? "已自动保存" + : "准备创建项目..."; if (isLoading) { return ( @@ -1655,6 +1703,23 @@ export default function CanvasEditorPage({ params }: PageProps) { const agentProjectNavSlot = isAgentMode ? (
+
+
+ 当前项目 +
+
+ {resolvedAgentProjectTitle} +
+
+ {isAgentProjectLoading || isAgentProjectSaving ? ( + + ) : ( + + )} + {agentProjectStatusLabel} +
+
+ + { + event.preventDefault(); + void handleCreateAgentProject(); + }} + > + + 新建项目 + + { + event.preventDefault(); + void handleSaveAsAgentProject(); + }} + > + + 另存为新项目 + { event.preventDefault(); - void handleManualSaveAgentProject(); + void handleRenameCurrentAgentProject(); }} > - - 保存项目 + + 重命名当前项目 { - setPendingGeneration(null); - }, transport: new DefaultChatTransport({ api: "/api/chat", fetch: fetchWithErrorHandlers, @@ -1171,21 +1177,39 @@ export function AgentGenerationWorkspaceDarkV2({ () => messages.filter((message) => message.role === "user").length, [messages] ); - - const generatedGroupsAfterLastUser = useMemo(() => { + const assistantMessagesAfterLastUser = useMemo(() => { const lastUserIndex = [...messages] .map((message) => message.role) .lastIndexOf("user"); if (lastUserIndex < 0) { - return [] as GeneratedImageGroup[]; + return [] as ChatMessage[]; } return messages .slice(lastUserIndex + 1) - .filter((message) => message.role === "assistant") + .filter((message) => message.role === "assistant"); + }, [messages]); + const hasAssistantFailureAfterLastUser = useMemo( + () => + assistantMessagesAfterLastUser.some((message) => { + const text = (message.parts || []) + .filter((part: any) => part.type === "text" && part.text) + .map((part: any) => String(part.text)) + .join("\n\n"); + if (!hasLikelyGenerationErrorText(text)) { + return false; + } + const imageUrls = extractImageUrlsFromText(text); + return imageUrls.length === 0; + }), + [assistantMessagesAfterLastUser] + ); + + const generatedGroupsAfterLastUser = useMemo(() => { + return assistantMessagesAfterLastUser .map((message) => generatedGroups.find((group) => group.messageId === message.id) || null) .filter((group): group is GeneratedImageGroup => Boolean(group)); - }, [generatedGroups, messages]); + }, [assistantMessagesAfterLastUser, generatedGroups]); const allGeneratedImages = useMemo( () => Array.from(new Set(generatedGroups.flatMap((group) => group.imageUrls))), @@ -1196,19 +1220,10 @@ export function AgentGenerationWorkspaceDarkV2({ [generatedGroupsAfterLastUser] ); const replacementGeneratedGroupsAfterLastUser = useMemo(() => { - const lastUserIndex = [...messages] - .map((message) => message.role) - .lastIndexOf("user"); - if (lastUserIndex < 0) { - return [] as GeneratedImageGroup[]; - } - - return messages - .slice(lastUserIndex + 1) - .filter((message) => message.role === "assistant") + return assistantMessagesAfterLastUser .map((message) => generatedGroups.find((group) => group.messageId === message.id) || null) .filter((group): group is GeneratedImageGroup => Boolean(group)); - }, [generatedGroups, messages]); + }, [assistantMessagesAfterLastUser, generatedGroups]); const latestGeneratedGroup = useMemo(() => { const lastUserIndex = [...messages] .map((message) => message.role) @@ -1593,6 +1608,31 @@ export function AgentGenerationWorkspaceDarkV2({ } }, [pendingGeneration, pendingGenerationProgressCount]); + useEffect(() => { + if (!pendingGeneration || transportIsGenerating) { + return; + } + + if (!hasAssistantFailureAfterLastUser) { + return; + } + + const hasProgress = + pendingGeneration.modeType === "scene_generation" + ? currentSceneGeneratedUrls.length > 0 + : latestReplacementResults.length > 0; + + if (!hasProgress) { + setPendingGeneration(null); + } + }, [ + currentSceneGeneratedUrls.length, + hasAssistantFailureAfterLastUser, + latestReplacementResults.length, + pendingGeneration, + transportIsGenerating, + ]); + useEffect(() => { onResultPreviewUrlsChange?.( currentResultModeType, diff --git a/components/agents/AgentProjectListSheet.tsx b/components/agents/AgentProjectListSheet.tsx index 0a31cd6e7..338fb5d9a 100644 --- a/components/agents/AgentProjectListSheet.tsx +++ b/components/agents/AgentProjectListSheet.tsx @@ -72,7 +72,7 @@ export function AgentProjectListSheet({ 生图项目列表 - 这里保存独立于无限画布的生图项目,可随时继续生成。 + 当前工作台会自动保存到项目中,这里可以随时切换、重命名或继续生成。 @@ -87,7 +87,7 @@ export function AgentProjectListSheet({ className="h-10 w-full gap-2 bg-emerald-500 text-white hover:bg-emerald-400" > - 新建生图项目 + 新建空白项目
@@ -201,7 +201,7 @@ export function AgentProjectListSheet({ ) : (
还没有生图项目
-
点击上方按钮创建一个独立项目。
+
点击上方按钮创建一个空白项目开始工作。
)}