diff --git a/frontend/src/i18n/locales/en-US.json b/frontend/src/i18n/locales/en-US.json index 44492b1..0249e62 100644 --- a/frontend/src/i18n/locales/en-US.json +++ b/frontend/src/i18n/locales/en-US.json @@ -462,5 +462,25 @@ "messages.auth.provider_not_ready": "This sign-in service is not configured yet. Please try again later.", "messages.auth.human_verification_required": "Please complete the security check first.", "messages.auth.human_verification_failed": "Security check failed. Please verify again and retry.", + "agentError.title.image": "Image generation did not complete", + "agentError.title.partialImage": "Some images did not complete", + "agentError.title.imageAction": "Image processing did not complete", + "agentError.title.textExtraction": "Text extraction did not complete", + "agentError.title.mockupModel": "Mockup model did not complete", + "agentError.title.layerMerge": "Layer merge did not complete", + "agentError.content.partialImageTimeout": "Some images were generated, but the remaining images took too long to return. Successful results were kept, and you can retry only the failed parts.", + "agentError.content.partialImageGeneric": "Some images were generated, but the remaining images failed. Successful results were kept, and you can retry only the failed parts.", + "agentError.content.imageTimeout": "Image generation took too long and did not complete this time. Your canvas was preserved. You can retry directly, or lower the image count/size first.", + "agentError.content.imageTemporary": "The image generation service is temporarily busy. Your canvas was preserved. Please try again later.", + "agentError.content.imageGeneric": "Image generation did not complete. Your canvas was preserved, and you can start generation again.", + "agentError.content.imageActionTimeout": "Image processing took too long and did not complete this time. The original image was preserved. Please try again later.", + "agentError.content.imageActionTemporary": "The image processing service is temporarily busy. The original image was preserved. Please try again later.", + "agentError.content.imageActionGeneric": "Image processing did not complete. The original image was preserved, and you can start processing again.", + "agentError.content.textExtractionTimeout": "Text extraction took too long and did not complete this time. The image state was restored. Please try again later.", + "agentError.content.textExtractionGeneric": "Text extraction did not complete. The image state was restored. Please try again later.", + "agentError.content.mockupModelTimeout": "Mockup model generation took too long and did not complete this time. The original image was preserved. Please try again later.", + "agentError.content.mockupModelGeneric": "Mockup model generation failed. The original image was preserved. Please try again later.", + "agentError.content.layerMergeTimeout": "Layer merge took too long and did not complete this time. The original layers were preserved. Please try again later.", + "agentError.content.layerMergeGeneric": "Layer merge did not complete. The original layers were preserved. Please try again later.", "localeLabel": "中" } diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 9f86007..e5dbfde 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -462,5 +462,25 @@ "messages.auth.provider_not_ready": "当前登录服务暂未配置,请稍后重试。", "messages.auth.human_verification_required": "请先完成人机验证。", "messages.auth.human_verification_failed": "人机验证失败,请重新验证后再试。", + "agentError.title.image": "图片生成没有完成", + "agentError.title.partialImage": "图片生成没有全部完成", + "agentError.title.imageAction": "图片处理没有完成", + "agentError.title.textExtraction": "文字提取没有完成", + "agentError.title.mockupModel": "Mockup 模型没有完成", + "agentError.title.layerMerge": "图层合并没有完成", + "agentError.content.partialImageTimeout": "部分图片已经生成完成,剩余图片耗时较久没有返回。已保留成功结果,可以只重试失败的部分。", + "agentError.content.partialImageGeneric": "部分图片已经生成完成,剩余图片没有成功生成。已保留成功结果,可以只重试失败的部分。", + "agentError.content.imageTimeout": "图片生成耗时较久,这次没有成功完成。画布内容已保留,可以直接重试,或先降低图片数量/尺寸后再生成。", + "agentError.content.imageTemporary": "图片生成服务暂时繁忙。画布内容已保留,可以稍后重试。", + "agentError.content.imageGeneric": "图片生成没有成功完成。画布内容已保留,可以重新发起生成。", + "agentError.content.imageActionTimeout": "图片处理耗时较久,这次没有成功完成。原图已保留,可以稍后重试。", + "agentError.content.imageActionTemporary": "图片处理服务暂时繁忙。原图已保留,可以稍后重试。", + "agentError.content.imageActionGeneric": "图片处理没有成功完成。原图已保留,可以重新发起处理。", + "agentError.content.textExtractionTimeout": "文字提取耗时较久,这次没有成功完成。图片状态已恢复,可以稍后重试。", + "agentError.content.textExtractionGeneric": "文字提取没有成功完成。图片状态已恢复,可以稍后重试。", + "agentError.content.mockupModelTimeout": "Mockup 模型生成耗时较久,这次没有成功完成。原图已保留,可以稍后重试。", + "agentError.content.mockupModelGeneric": "Mockup 模型没有成功生成。原图已保留,可以稍后重试。", + "agentError.content.layerMergeTimeout": "图层合并耗时较久,这次没有成功完成。原图层已保留,可以稍后重试。", + "agentError.content.layerMergeGeneric": "图层合并没有成功完成。原图层已保留,可以稍后重试。", "localeLabel": "EN" } diff --git a/frontend/src/ui/components/CanvasAgentTimeline/index.tsx b/frontend/src/ui/components/CanvasAgentTimeline/index.tsx index 01c6fb5..dba454e 100644 --- a/frontend/src/ui/components/CanvasAgentTimeline/index.tsx +++ b/frontend/src/ui/components/CanvasAgentTimeline/index.tsx @@ -459,11 +459,12 @@ function parseResearchPayload(message: AgentMessage): AgentResearchPayload | nul } function MessageBubble({ message }: { message: AgentMessage }) { + const { t } = useI18n(); const [copied, setCopied] = useState(false); const [copyButtonVisible, setCopyButtonVisible] = useState(false); const hideCopyButtonTimerRef = useRef(null); const rawContent = cleanMessageContent(message.content); - const friendlyError = message.role === "error" ? friendlyAgentErrorMessage({ ...message, content: rawContent }) : null; + const friendlyError = message.role === "error" ? friendlyAgentErrorMessage({ ...message, content: rawContent }, t) : null; const content = friendlyError?.content || rawContent; const title = friendlyError?.title || message.title; diff --git a/frontend/src/ui/lib/friendlyAgentErrors.ts b/frontend/src/ui/lib/friendlyAgentErrors.ts index db5023b..24497c4 100644 --- a/frontend/src/ui/lib/friendlyAgentErrors.ts +++ b/frontend/src/ui/lib/friendlyAgentErrors.ts @@ -1,4 +1,5 @@ import type { AgentMessage } from "@/domain/design"; +import type { Translate } from "@/infrastructure/userMessages"; type ErrorScope = "image" | "partial-image" | "image-action" | "text-extraction" | "mockup-model" | "layer-merge"; @@ -7,27 +8,27 @@ export type FriendlyAgentError = { content: string; }; -export function friendlyAgentErrorMessage(message: Pick): FriendlyAgentError { +export function friendlyAgentErrorMessage(message: Pick, t: Translate = defaultFriendlyErrorTranslate): FriendlyAgentError { const rawTitle = cleanErrorText(message.title); const rawContent = cleanErrorText(message.content); const combined = `${rawTitle}\n${rawContent}`; const scope = inferErrorScope(message, combined); - const title = friendlyErrorTitle(scope); + const title = friendlyErrorTitle(scope, t); if (message.role === "error" && rawContent && !isInternalFailureText(combined)) { - return { title, content: rawContent }; + return { title, content: knownFriendlyFailureContent(scope, combined, t) || rawContent }; } return { title, - content: fallbackErrorContent(scope, combined) + content: fallbackErrorContent(scope, combined, t) }; } -export function friendlyProjectFailureMessage(messages: AgentMessage[]) { +export function friendlyProjectFailureMessage(messages: AgentMessage[], t: Translate = defaultFriendlyErrorTranslate) { const failure = [...messages].reverse().find((message) => message.role === "error" || message.status === "failed"); if (!failure) return ""; - const friendly = friendlyAgentErrorMessage(failure); + const friendly = friendlyAgentErrorMessage(failure, t); return friendly.content || friendly.title; } @@ -42,51 +43,60 @@ function inferErrorScope(message: Pick = { + "agentError.title.image": "图片生成没有完成", + "agentError.title.partialImage": "图片生成没有全部完成", + "agentError.title.imageAction": "图片处理没有完成", + "agentError.title.textExtraction": "文字提取没有完成", + "agentError.title.mockupModel": "Mockup 模型没有完成", + "agentError.title.layerMerge": "图层合并没有完成", + "agentError.content.partialImageTimeout": "部分图片已经生成完成,剩余图片耗时较久没有返回。已保留成功结果,可以只重试失败的部分。", + "agentError.content.partialImageGeneric": "部分图片已经生成完成,剩余图片没有成功生成。已保留成功结果,可以只重试失败的部分。", + "agentError.content.imageActionTimeout": "图片处理耗时较久,这次没有成功完成。原图已保留,可以稍后重试。", + "agentError.content.imageActionTemporary": "图片处理服务暂时繁忙。原图已保留,可以稍后重试。", + "agentError.content.imageActionGeneric": "图片处理没有成功完成。原图已保留,可以重新发起处理。", + "agentError.content.textExtractionTimeout": "文字提取耗时较久,这次没有成功完成。图片状态已恢复,可以稍后重试。", + "agentError.content.textExtractionGeneric": "文字提取没有成功完成。图片状态已恢复,可以稍后重试。", + "agentError.content.mockupModelTimeout": "Mockup 模型生成耗时较久,这次没有成功完成。原图已保留,可以稍后重试。", + "agentError.content.mockupModelGeneric": "Mockup 模型没有成功生成。原图已保留,可以稍后重试。", + "agentError.content.layerMergeTimeout": "图层合并耗时较久,这次没有成功完成。原图层已保留,可以稍后重试。", + "agentError.content.layerMergeGeneric": "图层合并没有成功完成。原图层已保留,可以稍后重试。", + "agentError.content.imageTimeout": "图片生成耗时较久,这次没有成功完成。画布内容已保留,可以直接重试,或先降低图片数量/尺寸后再生成。", + "agentError.content.imageTemporary": "图片生成服务暂时繁忙。画布内容已保留,可以稍后重试。", + "agentError.content.imageGeneric": "图片生成没有成功完成。画布内容已保留,可以重新发起生成。" +}; diff --git a/frontend/src/ui/pages/CanvasWorkspace/canvas/hooks/useGenerationStream.ts b/frontend/src/ui/pages/CanvasWorkspace/canvas/hooks/useGenerationStream.ts index 4bc6d2c..47c4cab 100644 --- a/frontend/src/ui/pages/CanvasWorkspace/canvas/hooks/useGenerationStream.ts +++ b/frontend/src/ui/pages/CanvasWorkspace/canvas/hooks/useGenerationStream.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, type Dispatch, type RefObject, type SetStateAction } from "react"; import type { AgentChatPayload, AgentMessage, Project } from "@/domain/design"; +import { useI18n } from "@/i18n/i18n"; import { designGateway } from "@/infrastructure/designGateway"; import { useUserMessage } from "@/ui/hooks/useUserMessage"; import { friendlyProjectFailureMessage } from "@/ui/lib/friendlyAgentErrors"; @@ -17,6 +18,7 @@ export function useGenerationStream({ setImageGeneratorGenerating, setCanvasActionGenerating, setError, + showGenerationError, messages }: { applyProject: (project: Project, options?: { preserveMessages?: boolean }) => void; @@ -26,11 +28,13 @@ export function useGenerationStream({ setImageGeneratorGenerating: Dispatch>; setCanvasActionGenerating: Dispatch>; setError: Dispatch>; + showGenerationError?: (message: string) => void; messages: { generateError: string; generateTimeout: string; }; }) { + const { t } = useI18n(); const toUserMessageText = useUserMessage(); const closeAgentEventsRef = useRef<(() => void) | null>(null); const closeAgentProjectPollRef = useRef<(() => void) | null>(null); @@ -39,6 +43,13 @@ export function useGenerationStream({ const agentStreamingProjectIdRef = useRef(null); const imageGeneratorStreamingProjectIdRef = useRef>(new Set()); const canvasActionStreamingProjectIdRef = useRef>(new Set()); + const reportError = useCallback( + (message: string) => { + setError(message); + showGenerationError?.(message); + }, + [setError, showGenerationError] + ); const completeCanvasActionStream = useCallback( (streamKey: string) => { @@ -93,7 +104,7 @@ export function useGenerationStream({ }; const finish = (nextProject?: Project, failedMessage?: string) => { if (nextProject) applyProject(nextProject, { preserveMessages: true }); - if (failedMessage) setError(failedMessage); + if (failedMessage) reportError(failedMessage); close(); if (channel === "canvas-action") { completeCanvasActionStream(streamKey); @@ -116,7 +127,7 @@ export function useGenerationStream({ if (task.data.status === "failed") { const { project } = await designGateway.getProject(nextProjectId); if (closed) return; - finish(project, projectFailureMessage(project) || messages.generateError); + finish(project, projectFailureMessage(project, t) || messages.generateError); return; } timer = window.setTimeout(poll, generatorTaskPollIntervalMs); @@ -139,7 +150,7 @@ export function useGenerationStream({ } poll(); }, - [applyProject, completeCanvasActionStream, completeImageGeneratorStream, toUserMessageText, messages.generateError, setCanvasActionGenerating, setError, setImageGeneratorGenerating] + [applyProject, completeCanvasActionStream, completeImageGeneratorStream, toUserMessageText, t, messages.generateError, reportError, setCanvasActionGenerating, setImageGeneratorGenerating] ); const startAgentProjectPolling = useCallback( @@ -160,7 +171,7 @@ export function useGenerationStream({ const finish = (nextProject: Project, failedMessage?: string) => { if (closed) return; applyProject(nextProject); - if (failedMessage) setError(failedMessage); + if (failedMessage) reportError(failedMessage); close(); clearCurrentPoll(); closeAgentEventsRef.current?.(); @@ -176,7 +187,7 @@ export function useGenerationStream({ retryCount = 0; applyProject(project); if (generationProjectSettled(project)) { - finish(project, project.status === "failed" ? projectFailureMessage(project) || messages.generateError : undefined); + finish(project, project.status === "failed" ? projectFailureMessage(project, t) || messages.generateError : undefined); return; } timer = window.setTimeout(poll, generatorTaskPollIntervalMs); @@ -190,7 +201,7 @@ export function useGenerationStream({ closeAgentEventsRef.current = null; agentStreamingProjectIdRef.current = null; completeChannel("agent"); - setError(toUserMessageText(error, "generateError")); + reportError(toUserMessageText(error, "generateError")); return; } timer = window.setTimeout(poll, generatorTaskPollIntervalMs); @@ -201,7 +212,7 @@ export function useGenerationStream({ timer = window.setTimeout(poll, threadId ? generatorTaskPollIntervalMs : 1000); return close; }, - [applyProject, completeChannel, messages.generateError, setError, toUserMessageText] + [applyProject, completeChannel, messages.generateError, reportError, t, toUserMessageText] ); const startProjectEvents = useCallback( @@ -226,7 +237,7 @@ export function useGenerationStream({ closeAgentProjectPollRef.current?.(); closeAgentProjectPollRef.current = null; applyProject(nextProject, { preserveMessages }); - if (nextProject.status === "failed") setError(projectFailureMessage(nextProject) || messages.generateError); + if (nextProject.status === "failed") reportError(projectFailureMessage(nextProject, t) || messages.generateError); completeChannel(channel); streamingRef.current = null; closeRef.current = null; @@ -236,7 +247,7 @@ export function useGenerationStream({ closeAgentProjectPollRef.current = null; applyProject(nextProject, { preserveMessages }); completeChannel(channel); - setError(messages.generateTimeout); + reportError(messages.generateTimeout); streamingRef.current = null; closeRef.current = null; }, @@ -247,13 +258,13 @@ export function useGenerationStream({ return; } completeChannel(channel); - setError(toUserMessageText(nextError, "generateError")); + reportError(toUserMessageText(nextError, "generateError")); streamingRef.current = null; closeRef.current = null; } }); }, - [appendThinkingMessage, applyProject, completeChannel, toUserMessageText, messages.generateError, messages.generateTimeout, setError, startAgentProjectPolling, startGeneratorTaskPolling] + [appendThinkingMessage, applyProject, completeChannel, toUserMessageText, t, messages.generateError, messages.generateTimeout, reportError, startAgentProjectPolling, startGeneratorTaskPolling] ); const startAgentSocket = useCallback( @@ -292,7 +303,7 @@ export function useGenerationStream({ } else { completeImageGeneratorStream(streamKey); } - setError(toUserMessageText(error, "generateError")); + reportError(toUserMessageText(error, "generateError")); }); return; } @@ -311,7 +322,7 @@ export function useGenerationStream({ closeAgentProjectPollRef.current?.(); closeAgentProjectPollRef.current = null; applyProject(nextProject, { preserveMessages }); - if (nextProject.status === "failed") setError(projectFailureMessage(nextProject) || messages.generateError); + if (nextProject.status === "failed") reportError(projectFailureMessage(nextProject, t) || messages.generateError); completeChannel(channel); streamingRef.current = null; closeRef.current = null; @@ -321,7 +332,7 @@ export function useGenerationStream({ closeAgentProjectPollRef.current = null; applyProject(nextProject, { preserveMessages }); completeChannel(channel); - setError(messages.generateTimeout); + reportError(messages.generateTimeout); streamingRef.current = null; closeRef.current = null; }, @@ -332,13 +343,13 @@ export function useGenerationStream({ return; } completeChannel(channel); - setError(toUserMessageText(nextError, "generateError")); + reportError(toUserMessageText(nextError, "generateError")); streamingRef.current = null; closeRef.current = null; } }); }, - [appendThinkingMessage, applyProject, completeCanvasActionStream, completeChannel, completeImageGeneratorStream, toUserMessageText, messages.generateError, messages.generateTimeout, setCanvasActionGenerating, setError, setImageGeneratorGenerating, startAgentProjectPolling, startGeneratorTaskPolling] + [appendThinkingMessage, applyProject, completeCanvasActionStream, completeChannel, completeImageGeneratorStream, toUserMessageText, t, messages.generateError, messages.generateTimeout, reportError, setCanvasActionGenerating, setImageGeneratorGenerating, startAgentProjectPolling, startGeneratorTaskPolling] ); const stopGenerationStream = useCallback( @@ -428,6 +439,6 @@ function isActiveCanvasWorkNode(node: Project["nodes"][number]) { return node.status === "generating" || node.status === "uploading" || node.status === "text-extracting" || node.status === "mockup-modeling"; } -function projectFailureMessage(project: Project) { - return friendlyProjectFailureMessage(project.messages); +function projectFailureMessage(project: Project, t: (key: string) => string) { + return friendlyProjectFailureMessage(project.messages, t); } diff --git a/frontend/src/ui/pages/CanvasWorkspace/index.tsx b/frontend/src/ui/pages/CanvasWorkspace/index.tsx index 0e721ae..ff5e648 100644 --- a/frontend/src/ui/pages/CanvasWorkspace/index.tsx +++ b/frontend/src/ui/pages/CanvasWorkspace/index.tsx @@ -547,6 +547,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) { const { isAuthenticated } = useAuth(); const toUserMessageText = useUserMessage(); const { showToast } = useGlobalToast(); + const showGenerationError = useCallback((message: string) => showToast(message, "error"), [showToast]); const containerRef = useRef(null); const canvasImageInputRef = useRef(null); const agentComposerRef = useRef(null); @@ -741,6 +742,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) { setImageGeneratorGenerating, setCanvasActionGenerating, setError, + showGenerationError, messages: { generateError: t("generateError"), generateTimeout: t("generateTimeout") @@ -2812,7 +2814,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) { if (task.data.status === "failed") { const { project: nextProject } = await designGateway.getProject(response.project.id); if (closed || mockupPrepareRequestRef.current !== requestId) return; - failPreparation(projectFailureMessage(nextProject) || t("generateError")); + failPreparation(projectFailureMessage(nextProject, t) || t("generateError")); return; } if (task.data.status === "completed") { @@ -2820,7 +2822,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) { if (closed || mockupPrepareRequestRef.current !== requestId) return; const ready = await finishFromProject(nextProject); if (ready) return; - failPreparation(projectFailureMessage(nextProject) || t("generateError")); + failPreparation(projectFailureMessage(nextProject, t) || t("generateError")); return; } timer = setTimeout(poll, generatorTaskPollIntervalMs); @@ -3020,7 +3022,7 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) { applyProject(nextProject, { preserveMessages: true }); setTextEditLoading(false); setCanvasActionGenerating(false); - setError(projectFailureMessage(nextProject) || t("editTextOcrFailed")); + setError(projectFailureMessage(nextProject, t) || t("editTextOcrFailed")); textExtractionEventsCloseRef.current = null; return; } @@ -3982,8 +3984,6 @@ export function CanvasWorkspace({ projectId }: { projectId: string }) { )}
- {error &&

{error}

} - string) { + return friendlyProjectFailureMessage(project.messages, t); } function runningCanvasActionThreadIds(project: Project) { diff --git a/frontend/src/ui/styles.css b/frontend/src/ui/styles.css index ed2f0f9..570262a 100644 --- a/frontend/src/ui/styles.css +++ b/frontend/src/ui/styles.css @@ -470,8 +470,7 @@ button:disabled { border-radius: 999px; } -.inline-error, -.panel-error { +.inline-error { margin: 0; color: #b42318; font-size: 13px; @@ -3743,10 +3742,6 @@ button:disabled { line-height: 1.62; } -.panel-error { - padding: 0 14px 8px; -} - .canvas-model-popover { width: 348px; }