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,
|
CANVAS_IMAGE_ANALYSIS_TITLE,
|
||||||
"执行计划",
|
"执行计划",
|
||||||
|
"执行结果",
|
||||||
|
"后续操作",
|
||||||
"优化后提示词",
|
"优化后提示词",
|
||||||
"输出设置",
|
"输出设置",
|
||||||
]);
|
]);
|
||||||
@ -2303,6 +2305,7 @@ export function CanvasChatPanel({
|
|||||||
const [analysisCollapseState, setAnalysisCollapseState] = useState<Record<string, boolean>>({});
|
const [analysisCollapseState, setAnalysisCollapseState] = useState<Record<string, boolean>>({});
|
||||||
const [typedAssistantSections, setTypedAssistantSections] = useState<Record<string, boolean>>({});
|
const [typedAssistantSections, setTypedAssistantSections] = useState<Record<string, boolean>>({});
|
||||||
const [activeTypingSections, setActiveTypingSections] = 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 [clawdTypewriterMessageIds, setClawdTypewriterMessageIds] = useState<Record<string, boolean>>({});
|
||||||
const [insufficientPointsState, setInsufficientPointsState] = useState<{
|
const [insufficientPointsState, setInsufficientPointsState] = useState<{
|
||||||
balance: number;
|
balance: number;
|
||||||
@ -6466,6 +6469,18 @@ export function CanvasChatPanel({
|
|||||||
const normalizedAttachments = Array.isArray(messageData.attachments)
|
const normalizedAttachments = Array.isArray(messageData.attachments)
|
||||||
? 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 emptyNearestUserImageKeys = new Set<string>();
|
||||||
const incomingImageUrls = normalizedParts.flatMap((part: any) =>
|
const incomingImageUrls = normalizedParts.flatMap((part: any) =>
|
||||||
part?.type === "text" && typeof part?.text === "string"
|
part?.type === "text" && typeof part?.text === "string"
|
||||||
@ -6526,6 +6541,18 @@ export function CanvasChatPanel({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
messageData.role === "assistant" &&
|
||||||
|
appendedMessageId &&
|
||||||
|
appendedAssistantVisibleText
|
||||||
|
) {
|
||||||
|
setAgentTypewriterMessageIds((previousIds) =>
|
||||||
|
previousIds[appendedMessageId]
|
||||||
|
? previousIds
|
||||||
|
: { ...previousIds, [appendedMessageId]: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setMessages((prevMessages) => {
|
setMessages((prevMessages) => {
|
||||||
const existingIndex = prevMessages.findIndex(
|
const existingIndex = prevMessages.findIndex(
|
||||||
(message) => message.id === messageData.id
|
(message) => message.id === messageData.id
|
||||||
@ -6993,11 +7020,20 @@ export function CanvasChatPanel({
|
|||||||
currentModelIdRef.current = selectedModelId;
|
currentModelIdRef.current = selectedModelId;
|
||||||
inFlightChatIdRef.current = activeChatId;
|
inFlightChatIdRef.current = activeChatId;
|
||||||
inFlightRawChatIdRef.current = rawChatId;
|
inFlightRawChatIdRef.current = rawChatId;
|
||||||
|
const launchUserMessageId = generateUUID();
|
||||||
|
const launchCreatedAt = new Date();
|
||||||
|
|
||||||
sendMessage({
|
sendMessage({
|
||||||
|
id: launchUserMessageId,
|
||||||
role: "user",
|
role: "user",
|
||||||
parts,
|
parts,
|
||||||
});
|
createdAt: launchCreatedAt,
|
||||||
|
metadata: {
|
||||||
|
createdAt: launchCreatedAt.toISOString(),
|
||||||
|
composerMode: activeComposerModeRef.current || activeComposerMode,
|
||||||
|
modelId: selectedModelId,
|
||||||
|
},
|
||||||
|
} as any);
|
||||||
hasHydratedHistoryRef.current = true;
|
hasHydratedHistoryRef.current = true;
|
||||||
|
|
||||||
pendingLaunchRef.current = null;
|
pendingLaunchRef.current = null;
|
||||||
@ -7010,7 +7046,7 @@ export function CanvasChatPanel({
|
|||||||
if (editableDiv) {
|
if (editableDiv) {
|
||||||
editableDiv.innerHTML = "";
|
editableDiv.innerHTML = "";
|
||||||
}
|
}
|
||||||
}, [activeChatId, rawChatId, selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick]);
|
}, [activeChatId, rawChatId, selectedModelId, status, sendMessage, clearSelection, launchAutoSendTick, activeComposerMode]);
|
||||||
|
|
||||||
// 当历史消息加载完成后,更新消息列表(支持同一会话的增量刷新)
|
// 当历史消息加载完成后,更新消息列表(支持同一会话的增量刷新)
|
||||||
const loadedHistorySignatureRef = useRef<string>("");
|
const loadedHistorySignatureRef = useRef<string>("");
|
||||||
@ -10821,6 +10857,20 @@ export function CanvasChatPanel({
|
|||||||
!shouldTreatAsCanvasAgentControlIntent &&
|
!shouldTreatAsCanvasAgentControlIntent &&
|
||||||
effectiveComposerMode === "image" &&
|
effectiveComposerMode === "image" &&
|
||||||
Boolean(requestModel && isCanvasImageModel(requestModel));
|
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 中展示)
|
// 🆕 隐藏提示词(不在 UI 中展示)
|
||||||
const hiddenSegments: string[] = [];
|
const hiddenSegments: string[] = [];
|
||||||
@ -10933,9 +10983,12 @@ export function CanvasChatPanel({
|
|||||||
// 使用与 multimodal-input 相同的格式发送消息
|
// 使用与 multimodal-input 相同的格式发送消息
|
||||||
try {
|
try {
|
||||||
sendMessage({
|
sendMessage({
|
||||||
|
id: userMessageId,
|
||||||
role: "user",
|
role: "user",
|
||||||
parts: parts,
|
parts: parts,
|
||||||
});
|
createdAt: optimisticCreatedAt,
|
||||||
|
metadata: optimisticUserMetadata,
|
||||||
|
} as any);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
releaseSendGuard(activeChatId);
|
releaseSendGuard(activeChatId);
|
||||||
if (shouldShowOptimisticImageGeneration) {
|
if (shouldShowOptimisticImageGeneration) {
|
||||||
@ -11019,6 +11072,7 @@ export function CanvasChatPanel({
|
|||||||
markSessionClawdSkillTagPromptApplied,
|
markSessionClawdSkillTagPromptApplied,
|
||||||
buildHistoryMessagesUrl,
|
buildHistoryMessagesUrl,
|
||||||
workflowContext,
|
workflowContext,
|
||||||
|
messages,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleOpenClawQuickAction = useCallback(
|
const handleOpenClawQuickAction = useCallback(
|
||||||
@ -12953,6 +13007,11 @@ export function CanvasChatPanel({
|
|||||||
const shouldTypeClawAssistantMessage =
|
const shouldTypeClawAssistantMessage =
|
||||||
message.role === "assistant" &&
|
message.role === "assistant" &&
|
||||||
Boolean(clawdTypewriterMessageIds[message.id]);
|
Boolean(clawdTypewriterMessageIds[message.id]);
|
||||||
|
const shouldTypeAgentAssistantMessage =
|
||||||
|
message.role === "assistant" &&
|
||||||
|
Boolean(agentTypewriterMessageIds[message.id]);
|
||||||
|
const shouldForceTypeAssistantMessage =
|
||||||
|
shouldTypeClawAssistantMessage || shouldTypeAgentAssistantMessage;
|
||||||
const isClawAssistantMessage =
|
const isClawAssistantMessage =
|
||||||
message.role === "assistant" &&
|
message.role === "assistant" &&
|
||||||
(isClawdMode || isClawdChatMessage(message));
|
(isClawdMode || isClawdChatMessage(message));
|
||||||
@ -13161,16 +13220,16 @@ export function CanvasChatPanel({
|
|||||||
!typedAssistantSections[analysisTypingKey] &&
|
!typedAssistantSections[analysisTypingKey] &&
|
||||||
((shouldTypeAssistantMessage &&
|
((shouldTypeAssistantMessage &&
|
||||||
!hasCanvasRenderableMarkdown(analysisSection?.analysis || "")) ||
|
!hasCanvasRenderableMarkdown(analysisSection?.analysis || "")) ||
|
||||||
shouldTypeClawAssistantMessage);
|
shouldForceTypeAssistantMessage);
|
||||||
const shouldAnimatePlainText =
|
const shouldAnimatePlainText =
|
||||||
!analysisSection &&
|
!analysisSection &&
|
||||||
Boolean(plainText) &&
|
Boolean(plainText) &&
|
||||||
!typedAssistantSections[plainTextTypingKey] &&
|
!typedAssistantSections[plainTextTypingKey] &&
|
||||||
((shouldTypeAssistantMessage &&
|
((shouldTypeAssistantMessage &&
|
||||||
!hasCanvasRenderableMarkdown(plainText)) ||
|
!hasCanvasRenderableMarkdown(plainText)) ||
|
||||||
shouldTypeClawAssistantMessage);
|
shouldForceTypeAssistantMessage);
|
||||||
const shouldDeferAssistantMedia =
|
const shouldDeferAssistantMedia =
|
||||||
isClawAssistantMessage &&
|
(isClawAssistantMessage || shouldTypeAgentAssistantMessage) &&
|
||||||
(shouldAnimateAnalysis || shouldAnimatePlainText);
|
(shouldAnimateAnalysis || shouldAnimatePlainText);
|
||||||
const visibleImageUrls = shouldDeferAssistantMedia
|
const visibleImageUrls = shouldDeferAssistantMedia
|
||||||
? []
|
? []
|
||||||
@ -13182,6 +13241,7 @@ export function CanvasChatPanel({
|
|||||||
!isDetailGenerationMode &&
|
!isDetailGenerationMode &&
|
||||||
!isClawAssistantMessage &&
|
!isClawAssistantMessage &&
|
||||||
visibleImageUrls.length > 0 &&
|
visibleImageUrls.length > 0 &&
|
||||||
|
!plainText.trim() &&
|
||||||
!plainText.includes(CANVAS_IMAGE_DONE_SUMMARY) &&
|
!plainText.includes(CANVAS_IMAGE_DONE_SUMMARY) &&
|
||||||
!isLatestAssistantDuringGeneration;
|
!isLatestAssistantDuringGeneration;
|
||||||
const renderPlainText = (text: string, key: string) => (
|
const renderPlainText = (text: string, key: string) => (
|
||||||
|
|||||||
@ -10,6 +10,56 @@ import {
|
|||||||
type DirectAgentToolCapability,
|
type DirectAgentToolCapability,
|
||||||
} from "@/lib/ai/runtime/direct-agent-execution";
|
} 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: {
|
export function buildDirectAgentAnalysisAssistantText(params: {
|
||||||
summary: string;
|
summary: string;
|
||||||
answer?: string | null;
|
answer?: string | null;
|
||||||
@ -32,12 +82,11 @@ export function buildDirectAgentImageGenerationAssistantText(params: {
|
|||||||
modelDisplayName?: string | null;
|
modelDisplayName?: string | null;
|
||||||
}) {
|
}) {
|
||||||
return prependImageModelBadge(
|
return prependImageModelBadge(
|
||||||
[
|
buildDirectAgentResultCard({
|
||||||
getDirectAgentToolSuccessMessage("generate_image"),
|
result: getDirectAgentToolSuccessMessage("generate_image"),
|
||||||
params.imageMarkdown,
|
mediaMarkdown: params.imageMarkdown,
|
||||||
]
|
followUp: getDirectAgentFollowUpMessage("generate_image"),
|
||||||
.filter(Boolean)
|
}),
|
||||||
.join("\n\n"),
|
|
||||||
params.modelDisplayName,
|
params.modelDisplayName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -47,12 +96,11 @@ export function buildDirectAgentVideoGenerationAssistantText(params: {
|
|||||||
modelDisplayName?: string | null;
|
modelDisplayName?: string | null;
|
||||||
}) {
|
}) {
|
||||||
return prependImageModelBadge(
|
return prependImageModelBadge(
|
||||||
[
|
buildDirectAgentResultCard({
|
||||||
getDirectAgentToolSuccessMessage("generate_video"),
|
result: getDirectAgentToolSuccessMessage("generate_video"),
|
||||||
buildAgentToolVideoMarkdown(params.videoUrl),
|
mediaMarkdown: buildAgentToolVideoMarkdown(params.videoUrl),
|
||||||
]
|
followUp: getDirectAgentFollowUpMessage("generate_video"),
|
||||||
.filter(Boolean)
|
}),
|
||||||
.join("\n\n"),
|
|
||||||
params.modelDisplayName,
|
params.modelDisplayName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -64,16 +112,15 @@ export function buildDirectAgentImageEditAssistantText(params: {
|
|||||||
imageSize?: string;
|
imageSize?: string;
|
||||||
}) {
|
}) {
|
||||||
return prependImageModelBadge(
|
return prependImageModelBadge(
|
||||||
[
|
buildDirectAgentResultCard({
|
||||||
getDirectAgentToolSuccessMessage(params.capability),
|
result: getDirectAgentToolSuccessMessage(params.capability),
|
||||||
buildAgentToolImageMarkdown({
|
mediaMarkdown: buildAgentToolImageMarkdown({
|
||||||
originalUrl: params.originalUrl,
|
originalUrl: params.originalUrl,
|
||||||
thumbnailUrl: params.thumbnailUrl,
|
thumbnailUrl: params.thumbnailUrl,
|
||||||
imageSize: params.imageSize,
|
imageSize: params.imageSize,
|
||||||
}),
|
}),
|
||||||
]
|
followUp: getDirectAgentFollowUpMessage(params.capability),
|
||||||
.filter(Boolean)
|
}),
|
||||||
.join("\n\n"),
|
|
||||||
getDirectAgentToolLabel(params.capability),
|
getDirectAgentToolLabel(params.capability),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -87,12 +134,11 @@ export function buildDirectAgentBatchImageEditAssistantText(params: {
|
|||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
return prependImageModelBadge(
|
return prependImageModelBadge(
|
||||||
[
|
buildDirectAgentResultCard({
|
||||||
getDirectAgentToolSuccessMessage(params.capability),
|
result: getDirectAgentToolSuccessMessage(params.capability),
|
||||||
buildAgentToolImagesMarkdown(params.results),
|
mediaMarkdown: buildAgentToolImagesMarkdown(params.results),
|
||||||
]
|
followUp: getDirectAgentFollowUpMessage(params.capability),
|
||||||
.filter(Boolean)
|
}),
|
||||||
.join("\n\n"),
|
|
||||||
getDirectAgentToolLabel(params.capability),
|
getDirectAgentToolLabel(params.capability),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,6 +131,10 @@ import {
|
|||||||
resolveDirectAgentToolExecution,
|
resolveDirectAgentToolExecution,
|
||||||
shouldRunDirectAgentBatchImageEdit,
|
shouldRunDirectAgentBatchImageEdit,
|
||||||
} from "@/lib/ai/runtime/direct-agent-execution";
|
} 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 { inferAngleEditOptionsFromText } from "@/lib/ai/runtime/angle-edit-options";
|
||||||
import {
|
import {
|
||||||
hasDirectAgentLocalEditIntent,
|
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", () => {
|
test("only runs direct image edit batches for explicit safe batch requests", () => {
|
||||||
expect(
|
expect(
|
||||||
shouldRunDirectAgentBatchImageEdit({
|
shouldRunDirectAgentBatchImageEdit({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user