fix: stabilize canvas agent chat feedback
This commit is contained in:
parent
2d6289f0fa
commit
7e1fd15ebd
@ -792,6 +792,8 @@ const CANVAS_AGENT_SECTION_TITLES = new Set([
|
||||
"需求理解",
|
||||
CANVAS_IMAGE_ANALYSIS_TITLE,
|
||||
"执行计划",
|
||||
"执行结果",
|
||||
"后续操作",
|
||||
"优化后提示词",
|
||||
"输出设置",
|
||||
]);
|
||||
@ -2303,6 +2305,7 @@ export function CanvasChatPanel({
|
||||
const [analysisCollapseState, setAnalysisCollapseState] = useState<Record<string, boolean>>({});
|
||||
const [typedAssistantSections, setTypedAssistantSections] = useState<Record<string, boolean>>({});
|
||||
const [activeTypingSections, setActiveTypingSections] = useState<Record<string, boolean>>({});
|
||||
const [agentTypewriterMessageIds, setAgentTypewriterMessageIds] = useState<Record<string, boolean>>({});
|
||||
const [clawdTypewriterMessageIds, setClawdTypewriterMessageIds] = useState<Record<string, boolean>>({});
|
||||
const [insufficientPointsState, setInsufficientPointsState] = useState<{
|
||||
balance: number;
|
||||
@ -6466,6 +6469,18 @@ export function CanvasChatPanel({
|
||||
const normalizedAttachments = Array.isArray(messageData.attachments)
|
||||
? messageData.attachments
|
||||
: [];
|
||||
const appendedMessageId = String(messageData.id || "").trim();
|
||||
const appendedAssistantText = normalizedParts
|
||||
.filter(
|
||||
(part: any) =>
|
||||
part?.type === "text" && typeof part?.text === "string"
|
||||
)
|
||||
.map((part: any) => String(part.text || ""))
|
||||
.join("\n\n");
|
||||
const appendedAssistantVisibleText = normalizeCanvasAgentText(
|
||||
stripCanvasModelRoutingLines(stripMediaMarkup(appendedAssistantText))
|
||||
.replace(/<!--\s*IMAGE_GENERATION_(?:START|END)\s*-->/g, "")
|
||||
);
|
||||
const emptyNearestUserImageKeys = new Set<string>();
|
||||
const incomingImageUrls = normalizedParts.flatMap((part: any) =>
|
||||
part?.type === "text" && typeof part?.text === "string"
|
||||
@ -6525,6 +6540,18 @@ export function CanvasChatPanel({
|
||||
if (!isEventForActiveChat) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
messageData.role === "assistant" &&
|
||||
appendedMessageId &&
|
||||
appendedAssistantVisibleText
|
||||
) {
|
||||
setAgentTypewriterMessageIds((previousIds) =>
|
||||
previousIds[appendedMessageId]
|
||||
? previousIds
|
||||
: { ...previousIds, [appendedMessageId]: true }
|
||||
);
|
||||
}
|
||||
|
||||
setMessages((prevMessages) => {
|
||||
const existingIndex = prevMessages.findIndex(
|
||||
@ -6993,11 +7020,20 @@ export function CanvasChatPanel({
|
||||
currentModelIdRef.current = selectedModelId;
|
||||
inFlightChatIdRef.current = activeChatId;
|
||||
inFlightRawChatIdRef.current = rawChatId;
|
||||
const launchUserMessageId = generateUUID();
|
||||
const launchCreatedAt = new Date();
|
||||
|
||||
sendMessage({
|
||||
id: launchUserMessageId,
|
||||
role: "user",
|
||||
parts,
|
||||
});
|
||||
createdAt: launchCreatedAt,
|
||||
metadata: {
|
||||
createdAt: launchCreatedAt.toISOString(),
|
||||
composerMode: activeComposerModeRef.current || activeComposerMode,
|
||||
modelId: selectedModelId,
|
||||
},
|
||||
} as any);
|
||||
hasHydratedHistoryRef.current = true;
|
||||
|
||||
pendingLaunchRef.current = null;
|
||||
@ -7010,7 +7046,7 @@ export function CanvasChatPanel({
|
||||
if (editableDiv) {
|
||||
editableDiv.innerHTML = "";
|
||||
}
|
||||
}, [activeChatId, rawChatId, selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick]);
|
||||
}, [activeChatId, rawChatId, selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick, activeComposerMode]);
|
||||
|
||||
// 当历史消息加载完成后,更新消息列表(支持同一会话的增量刷新)
|
||||
const loadedHistorySignatureRef = useRef<string>("");
|
||||
@ -10821,6 +10857,20 @@ export function CanvasChatPanel({
|
||||
!shouldTreatAsCanvasAgentControlIntent &&
|
||||
effectiveComposerMode === "image" &&
|
||||
Boolean(requestModel && isCanvasImageModel(requestModel));
|
||||
const userMessageId = generateUUID();
|
||||
const latestMessageTimestamp = messages.reduce(
|
||||
(latest, message) => Math.max(latest, getMessageTimestamp(message) ?? 0),
|
||||
0
|
||||
);
|
||||
const optimisticCreatedAt = new Date(
|
||||
Math.max(Date.now(), latestMessageTimestamp + 1)
|
||||
);
|
||||
const optimisticUserMetadata = {
|
||||
createdAt: optimisticCreatedAt.toISOString(),
|
||||
composerMode: effectiveComposerMode,
|
||||
agentEntryMode: effectiveAgentEntryMode,
|
||||
modelId: effectiveModelId,
|
||||
};
|
||||
|
||||
// 🆕 隐藏提示词(不在 UI 中展示)
|
||||
const hiddenSegments: string[] = [];
|
||||
@ -10933,9 +10983,12 @@ export function CanvasChatPanel({
|
||||
// 使用与 multimodal-input 相同的格式发送消息
|
||||
try {
|
||||
sendMessage({
|
||||
id: userMessageId,
|
||||
role: "user",
|
||||
parts: parts,
|
||||
});
|
||||
createdAt: optimisticCreatedAt,
|
||||
metadata: optimisticUserMetadata,
|
||||
} as any);
|
||||
} catch (error) {
|
||||
releaseSendGuard(activeChatId);
|
||||
if (shouldShowOptimisticImageGeneration) {
|
||||
@ -11019,6 +11072,7 @@ export function CanvasChatPanel({
|
||||
markSessionClawdSkillTagPromptApplied,
|
||||
buildHistoryMessagesUrl,
|
||||
workflowContext,
|
||||
messages,
|
||||
]);
|
||||
|
||||
const handleOpenClawQuickAction = useCallback(
|
||||
@ -12953,6 +13007,11 @@ export function CanvasChatPanel({
|
||||
const shouldTypeClawAssistantMessage =
|
||||
message.role === "assistant" &&
|
||||
Boolean(clawdTypewriterMessageIds[message.id]);
|
||||
const shouldTypeAgentAssistantMessage =
|
||||
message.role === "assistant" &&
|
||||
Boolean(agentTypewriterMessageIds[message.id]);
|
||||
const shouldForceTypeAssistantMessage =
|
||||
shouldTypeClawAssistantMessage || shouldTypeAgentAssistantMessage;
|
||||
const isClawAssistantMessage =
|
||||
message.role === "assistant" &&
|
||||
(isClawdMode || isClawdChatMessage(message));
|
||||
@ -13161,16 +13220,16 @@ export function CanvasChatPanel({
|
||||
!typedAssistantSections[analysisTypingKey] &&
|
||||
((shouldTypeAssistantMessage &&
|
||||
!hasCanvasRenderableMarkdown(analysisSection?.analysis || "")) ||
|
||||
shouldTypeClawAssistantMessage);
|
||||
shouldForceTypeAssistantMessage);
|
||||
const shouldAnimatePlainText =
|
||||
!analysisSection &&
|
||||
Boolean(plainText) &&
|
||||
!typedAssistantSections[plainTextTypingKey] &&
|
||||
((shouldTypeAssistantMessage &&
|
||||
!hasCanvasRenderableMarkdown(plainText)) ||
|
||||
shouldTypeClawAssistantMessage);
|
||||
shouldForceTypeAssistantMessage);
|
||||
const shouldDeferAssistantMedia =
|
||||
isClawAssistantMessage &&
|
||||
(isClawAssistantMessage || shouldTypeAgentAssistantMessage) &&
|
||||
(shouldAnimateAnalysis || shouldAnimatePlainText);
|
||||
const visibleImageUrls = shouldDeferAssistantMedia
|
||||
? []
|
||||
@ -13182,6 +13241,7 @@ export function CanvasChatPanel({
|
||||
!isDetailGenerationMode &&
|
||||
!isClawAssistantMessage &&
|
||||
visibleImageUrls.length > 0 &&
|
||||
!plainText.trim() &&
|
||||
!plainText.includes(CANVAS_IMAGE_DONE_SUMMARY) &&
|
||||
!isLatestAssistantDuringGeneration;
|
||||
const renderPlainText = (text: string, key: string) => (
|
||||
|
||||
@ -10,6 +10,56 @@ import {
|
||||
type DirectAgentToolCapability,
|
||||
} from "@/lib/ai/runtime/direct-agent-execution";
|
||||
|
||||
function normalizeDirectAgentResultSentence(value: string) {
|
||||
const normalized = String(value || "")
|
||||
.replace(/结果如下[::]?\s*$/u, "")
|
||||
.replace(/[,,。::]\s*$/u, "")
|
||||
.trim();
|
||||
return normalized || "已完成处理";
|
||||
}
|
||||
|
||||
function buildDirectAgentResultCard(params: {
|
||||
result: string;
|
||||
mediaMarkdown?: string | null;
|
||||
followUp: string;
|
||||
}) {
|
||||
return [
|
||||
"### 执行结果",
|
||||
`- ${normalizeDirectAgentResultSentence(params.result)},结果如下。`,
|
||||
params.mediaMarkdown,
|
||||
"### 后续操作",
|
||||
`- ${params.followUp}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
function getDirectAgentFollowUpMessage(capability: DirectAgentToolCapability) {
|
||||
switch (capability) {
|
||||
case "remove_background":
|
||||
return "如需微调边缘、替换背景或继续合成,可以直接告诉我。";
|
||||
case "remove_watermark":
|
||||
return "如需继续修补局部痕迹或统一画面质感,可以直接告诉我。";
|
||||
case "enhance":
|
||||
return "如需继续调整清晰度、色彩或细节强度,可以直接告诉我。";
|
||||
case "resize":
|
||||
return "如需继续改尺寸、比例或输出规格,可以直接告诉我。";
|
||||
case "extend":
|
||||
return "如需继续扩展画面、调整构图或补充边缘内容,可以直接告诉我。";
|
||||
case "angle_variant":
|
||||
return "如需继续换角度、换机位或统一系列视角,可以直接告诉我。";
|
||||
case "local_edit":
|
||||
case "box_cutout":
|
||||
return "如需继续修改局部、替换元素或细化边缘,可以直接告诉我。";
|
||||
case "generate_video":
|
||||
return "如需继续调整镜头、节奏、比例或时长,可以直接告诉我。";
|
||||
case "generate_image":
|
||||
case "generic_edit":
|
||||
default:
|
||||
return "如需继续调整风格、构图、主体细节或尺寸,可以直接告诉我。";
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDirectAgentAnalysisAssistantText(params: {
|
||||
summary: string;
|
||||
answer?: string | null;
|
||||
@ -32,12 +82,11 @@ export function buildDirectAgentImageGenerationAssistantText(params: {
|
||||
modelDisplayName?: string | null;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
[
|
||||
getDirectAgentToolSuccessMessage("generate_image"),
|
||||
params.imageMarkdown,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage("generate_image"),
|
||||
mediaMarkdown: params.imageMarkdown,
|
||||
followUp: getDirectAgentFollowUpMessage("generate_image"),
|
||||
}),
|
||||
params.modelDisplayName,
|
||||
);
|
||||
}
|
||||
@ -47,12 +96,11 @@ export function buildDirectAgentVideoGenerationAssistantText(params: {
|
||||
modelDisplayName?: string | null;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
[
|
||||
getDirectAgentToolSuccessMessage("generate_video"),
|
||||
buildAgentToolVideoMarkdown(params.videoUrl),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage("generate_video"),
|
||||
mediaMarkdown: buildAgentToolVideoMarkdown(params.videoUrl),
|
||||
followUp: getDirectAgentFollowUpMessage("generate_video"),
|
||||
}),
|
||||
params.modelDisplayName,
|
||||
);
|
||||
}
|
||||
@ -64,16 +112,15 @@ export function buildDirectAgentImageEditAssistantText(params: {
|
||||
imageSize?: string;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
[
|
||||
getDirectAgentToolSuccessMessage(params.capability),
|
||||
buildAgentToolImageMarkdown({
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage(params.capability),
|
||||
mediaMarkdown: buildAgentToolImageMarkdown({
|
||||
originalUrl: params.originalUrl,
|
||||
thumbnailUrl: params.thumbnailUrl,
|
||||
imageSize: params.imageSize,
|
||||
}),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
followUp: getDirectAgentFollowUpMessage(params.capability),
|
||||
}),
|
||||
getDirectAgentToolLabel(params.capability),
|
||||
);
|
||||
}
|
||||
@ -87,12 +134,11 @@ export function buildDirectAgentBatchImageEditAssistantText(params: {
|
||||
}>;
|
||||
}) {
|
||||
return prependImageModelBadge(
|
||||
[
|
||||
getDirectAgentToolSuccessMessage(params.capability),
|
||||
buildAgentToolImagesMarkdown(params.results),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n\n"),
|
||||
buildDirectAgentResultCard({
|
||||
result: getDirectAgentToolSuccessMessage(params.capability),
|
||||
mediaMarkdown: buildAgentToolImagesMarkdown(params.results),
|
||||
followUp: getDirectAgentFollowUpMessage(params.capability),
|
||||
}),
|
||||
getDirectAgentToolLabel(params.capability),
|
||||
);
|
||||
}
|
||||
|
||||
@ -131,6 +131,10 @@ import {
|
||||
resolveDirectAgentToolExecution,
|
||||
shouldRunDirectAgentBatchImageEdit,
|
||||
} from "@/lib/ai/runtime/direct-agent-execution";
|
||||
import {
|
||||
buildDirectAgentImageEditAssistantText,
|
||||
buildDirectAgentImageGenerationAssistantText,
|
||||
} from "@/lib/ai/runtime/direct-agent-response-builders";
|
||||
import { inferAngleEditOptionsFromText } from "@/lib/ai/runtime/angle-edit-options";
|
||||
import {
|
||||
hasDirectAgentLocalEditIntent,
|
||||
@ -1179,6 +1183,28 @@ test.describe("canvas chat pure helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("formats direct agent image results as structured markdown cards", () => {
|
||||
const generatedText = buildDirectAgentImageGenerationAssistantText({
|
||||
imageMarkdown: "",
|
||||
modelDisplayName: "Test Image Model",
|
||||
});
|
||||
|
||||
expect(generatedText).toContain("### 执行结果");
|
||||
expect(generatedText).toContain("- 已完成图片生成,结果如下。");
|
||||
expect(generatedText).toContain("");
|
||||
expect(generatedText).toContain("### 后续操作");
|
||||
|
||||
const editText = buildDirectAgentImageEditAssistantText({
|
||||
capability: "remove_background",
|
||||
originalUrl: "https://cdn.example.com/cutout.png",
|
||||
});
|
||||
|
||||
expect(editText).toContain("### 执行结果");
|
||||
expect(editText).toContain("- 已完成抠图处理,结果如下。");
|
||||
expect(editText).toContain("https://cdn.example.com/cutout.png");
|
||||
expect(editText).toContain("替换背景");
|
||||
});
|
||||
|
||||
test("only runs direct image edit batches for explicit safe batch requests", () => {
|
||||
expect(
|
||||
shouldRunDirectAgentBatchImageEdit({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user