feat(ui): localize friendly agent errors and surface them as toasts
Thread a translate function through friendlyAgentErrorMessage/ friendlyProjectFailureMessage and move the hard-coded zh-CN error copy into the i18n locale files (with a zh-CN fallback map). Generation stream failures now report through a toast via showGenerationError in addition to setError, so the removed inline .panel-error footer is gone. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<SetStateAction<boolean>>;
|
||||
setCanvasActionGenerating: Dispatch<SetStateAction<boolean>>;
|
||||
setError: Dispatch<SetStateAction<string>>;
|
||||
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<string | null>(null);
|
||||
const imageGeneratorStreamingProjectIdRef = useRef<Set<string>>(new Set());
|
||||
const canvasActionStreamingProjectIdRef = useRef<Set<string>>(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);
|
||||
}
|
||||
|
||||
@@ -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<HTMLDivElement | null>(null);
|
||||
const canvasImageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const agentComposerRef = useRef<PromptComposerHandle | null>(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 }) {
|
||||
)}
|
||||
|
||||
<div className="assistant-footer-dock" ref={assistantFooterRef}>
|
||||
{error && <p className="panel-error">{error}</p>}
|
||||
|
||||
<CanvasAgentComposer
|
||||
ref={agentComposerRef}
|
||||
text={prompt}
|
||||
@@ -5512,8 +5512,8 @@ function projectHasWebSearchEnabled(project: Project) {
|
||||
});
|
||||
}
|
||||
|
||||
function projectFailureMessage(project: Project) {
|
||||
return friendlyProjectFailureMessage(project.messages);
|
||||
function projectFailureMessage(project: Project, t: (key: string) => string) {
|
||||
return friendlyProjectFailureMessage(project.messages, t);
|
||||
}
|
||||
|
||||
function runningCanvasActionThreadIds(project: Project) {
|
||||
|
||||
Reference in New Issue
Block a user