fix: rename Claw mode and type responses

This commit is contained in:
134 2026-06-24 01:37:04 +08:00
parent d5fed4f2da
commit fa10115043
4 changed files with 175 additions and 27 deletions

View File

@ -12345,11 +12345,11 @@ export function AdminSettingsClient() {
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<Label htmlFor="openClawChatEnabled">
无限画布 Clawd
无限画布 Claw
</Label>
<div className="text-muted-foreground text-xs">
开启并配置接口后,右侧对话框模式菜单显示
Clawd;Token 仅保存在服务端,不会下发到前端。
Claw;Token 仅保存在服务端,不会下发到前端。
</div>
</div>
<Switch

View File

@ -33,7 +33,7 @@ type ParsedClawdChatRequest = z.infer<typeof requestSchema> & {
function buildChatTitle(message: string) {
const normalized = message.replace(/\s+/g, " ").trim();
if (!normalized) return "Clawd 对话";
if (!normalized) return "Claw 对话";
return normalized.length > 80 ? `${normalized.slice(0, 80)}...` : normalized;
}
@ -144,11 +144,11 @@ export async function POST(request: Request) {
const config = await readOpenClawConfig();
if (!config.enabled) {
return NextResponse.json({ error: "Clawd 未启用" }, { status: 403 });
return NextResponse.json({ error: "Claw 未启用" }, { status: 403 });
}
if (!config.baseUrl || !config.token) {
return NextResponse.json(
{ error: "Clawd 接口地址或 Token 未配置" },
{ error: "Claw 接口地址或 Token 未配置" },
{ status: 500 }
);
}

View File

@ -311,8 +311,10 @@ interface CanvasChatPanelProps {
type CanvasComposerMode = "agent" | "image" | "video" | "detail" | "clawd";
const CLAW_MODE_LABEL = "Claw";
function getCanvasComposerModeSessionLabel(mode: CanvasComposerMode) {
if (mode === "clawd") return "Clawd";
if (mode === "clawd") return CLAW_MODE_LABEL;
if (mode === "video") return "视频";
if (mode === "detail") return "AI详情";
if (mode === "image") return "图像";
@ -952,16 +954,125 @@ type CanvasTypewriterTextProps = {
enabled: boolean;
className?: string;
renderMarkdown?: boolean;
preserveMarkdownTables?: boolean;
onActiveChange?: (typingKey: string, active: boolean) => void;
onComplete?: (typingKey: string) => void;
};
type MarkdownTableBlockRange = {
start: number;
end: number;
};
function isLikelyMarkdownTableRow(line: string) {
const trimmed = line.trim();
if (!trimmed || !trimmed.includes("|")) return false;
const cells = trimmed
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|");
return cells.length >= 2;
}
function isMarkdownTableSeparatorRow(line: string) {
if (!isLikelyMarkdownTableRow(line)) return false;
const cells = line
.trim()
.replace(/^\|/, "")
.replace(/\|$/, "")
.split("|")
.map((cell) => cell.trim().replace(/\s+/g, ""));
return cells.length >= 2 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
}
function getMarkdownTableBlockRanges(content: string): MarkdownTableBlockRange[] {
if (!content.includes("|")) return [];
const rawLines = content.split("\n");
const lines = rawLines.map((line, index) => {
const hasNewline = index < rawLines.length - 1;
const text = hasNewline ? `${line}\n` : line;
return {
line: line.replace(/\r$/, ""),
text,
};
});
let offset = 0;
const indexedLines = lines.map((line) => {
const start = offset;
offset += line.text.length;
return {
...line,
start,
end: offset,
};
});
const ranges: MarkdownTableBlockRange[] = [];
for (let index = 0; index < indexedLines.length - 1; index += 1) {
const header = indexedLines[index];
const separator = indexedLines[index + 1];
if (
!header ||
!separator ||
!isLikelyMarkdownTableRow(header.line) ||
!isMarkdownTableSeparatorRow(separator.line)
) {
continue;
}
let endIndex = index + 2;
while (
endIndex < indexedLines.length &&
isLikelyMarkdownTableRow(indexedLines[endIndex]?.line || "")
) {
endIndex += 1;
}
ranges.push({
start: header.start,
end: indexedLines[endIndex - 1]?.end || separator.end,
});
index = endIndex - 1;
}
return ranges;
}
function advanceTypewriterCursor(
contentLength: number,
targetCursor: number,
tableRanges: MarkdownTableBlockRange[]
) {
const clampedCursor = Math.min(contentLength, targetCursor);
for (const range of tableRanges) {
if (clampedCursor >= range.start && clampedCursor < range.end) {
return Math.min(contentLength, range.end);
}
}
return clampedCursor;
}
function CanvasTypewriterCursor() {
return (
<span
aria-hidden="true"
className="ml-0.5 inline-block h-[1.05em] w-[2px] animate-pulse rounded-full bg-current align-[-0.16em]"
/>
);
}
function CanvasTypewriterText({
text,
typingKey,
enabled,
className,
renderMarkdown = false,
preserveMarkdownTables = false,
onActiveChange,
onComplete,
}: CanvasTypewriterTextProps) {
@ -980,12 +1091,15 @@ function CanvasTypewriterText({
}
let cursor = 0;
const tableRanges = preserveMarkdownTables
? getMarkdownTableBlockRanges(content)
: [];
activeRef.current = true;
onActiveChange?.(typingKey, true);
setDisplayText("");
const timer = window.setInterval(() => {
cursor += 1;
cursor = advanceTypewriterCursor(content.length, cursor + 1, tableRanges);
if (cursor >= content.length) {
setDisplayText(content);
window.clearInterval(timer);
@ -1006,13 +1120,25 @@ function CanvasTypewriterText({
onActiveChange?.(typingKey, false);
}
};
}, [content, enabled, onActiveChange, onComplete, typingKey]);
}, [content, enabled, onActiveChange, onComplete, preserveMarkdownTables, typingKey]);
const showCursor = enabled && Boolean(content) && displayText.length < content.length;
if (renderMarkdown) {
return <Response className={className}>{displayText}</Response>;
return (
<div className="w-full min-w-0">
<Response className={className}>{displayText}</Response>
{showCursor ? <CanvasTypewriterCursor /> : null}
</div>
);
}
return <div className={className}>{displayText}</div>;
return (
<div className={className}>
{displayText}
{showCursor ? <CanvasTypewriterCursor /> : null}
</div>
);
}
function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
@ -1037,7 +1163,7 @@ function CanvasAssistantTypingPlaceholder({ hint }: { hint?: string | null }) {
</div>
) : null}
<div
aria-label="Clawd 正在处理"
aria-label={`${CLAW_MODE_LABEL} 正在处理`}
className="inline-flex h-7 items-center gap-1 rounded-[14px] bg-[#f4f5f7] px-3 text-[#9aa3b2] dark:bg-white/8 dark:text-zinc-400"
>
{[0, 1, 2].map((index) => (
@ -1553,6 +1679,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 [clawdTypewriterMessageIds, setClawdTypewriterMessageIds] = useState<Record<string, boolean>>({});
const [insufficientPointsState, setInsufficientPointsState] = useState<{
balance: number;
expectedCost: number;
@ -3633,7 +3760,7 @@ export function CanvasChatPanel({
);
const activeModeLabel =
activeComposerMode === "clawd"
? "Clawd"
? CLAW_MODE_LABEL
: activeComposerMode === "video"
? "视频"
: activeComposerMode === "detail"
@ -3978,7 +4105,7 @@ export function CanvasChatPanel({
hiddenPromptRef.current = null;
autoContextRef.current = null;
if (mode === "clawd" && !isClawdModeEnabled) {
toast.error("Clawd 未开启");
toast.error(`${CLAW_MODE_LABEL} 未开启`);
return;
}
if (mode === "agent") {
@ -8991,7 +9118,7 @@ export function CanvasChatPanel({
});
const responseData = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(responseData?.error || "Clawd 请求失败");
throw new Error(responseData?.error || `${CLAW_MODE_LABEL} 请求失败`);
}
const returnedMessages = Array.isArray(responseData?.messages)
@ -8999,6 +9126,19 @@ export function CanvasChatPanel({
: [];
if (returnedMessages.length > 0) {
const returnedIds = new Set(returnedMessages.map((message) => message.id));
const returnedAssistantIds = returnedMessages
.filter((message) => message.role === "assistant")
.map((message) => message.id)
.filter(Boolean);
if (returnedAssistantIds.length > 0) {
setClawdTypewriterMessageIds((previousIds) => {
const nextIds = { ...previousIds };
returnedAssistantIds.forEach((messageId) => {
nextIds[messageId] = true;
});
return nextIds;
});
}
setMessages((previousMessages) =>
sortCanvasMessagesByCreatedAt([
...previousMessages.filter(
@ -9014,11 +9154,11 @@ export function CanvasChatPanel({
}
hasHydratedHistoryRef.current = true;
} catch (error) {
console.error("[CanvasChat] Clawd request failed:", error);
console.error("[CanvasChat] Claw request failed:", error);
setMessages((previousMessages) =>
previousMessages.filter((message) => message.id !== userMessageId)
);
toast.error(error instanceof Error ? error.message : "Clawd 请求失败");
toast.error(error instanceof Error ? error.message : `${CLAW_MODE_LABEL} 请求失败`);
setClawdTableFiles(tableFilesSnapshot);
setAttachments(attachmentsSnapshot);
const currentEditorText = (editableDiv?.textContent || "")
@ -9956,7 +10096,7 @@ export function CanvasChatPanel({
const hasEnoughAgentUploads = !hasAgentUploadSlots || attachments.length >= agentRequiredUploadCount;
const composerPlaceholder = isWorkspacePresentation
? isClawdMode
? "输入要交给 Clawd 的问题,可上传图片或表格"
? `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
: isDetailGenerationMode
? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
: isVideoWorkspace
@ -9967,7 +10107,7 @@ export function CanvasChatPanel({
? "可选补充本次生图要求,例如:高级感、暖灰色、近景特写、生成3张。"
: "可选补充本次生成要求,例如:更高级、更生活化、暖调灯光。"
: isClawdMode
? "输入要交给 Clawd 的问题,可上传图片或表格"
? `输入要交给 ${CLAW_MODE_LABEL} 的问题,可上传图片或表格`
: isDetailGenerationMode
? "可选补充:风格、情绪氛围、禁用元素、期望生成张数(如:生成8张)"
: isVideoWorkspace
@ -10289,7 +10429,7 @@ export function CanvasChatPanel({
!shouldShowImageLoading;
const workspaceEmptyTitle =
isClawdMode
? "开始 Clawd 对话"
? `开始 ${CLAW_MODE_LABEL} 对话`
: activeComposerMode === "agent" && !isPresetAgentSelection
? "开始 Auto Agent 对话"
: hasAgentUploadSlots && attachments.length < agentRequiredUploadCount
@ -10961,6 +11101,10 @@ export function CanvasChatPanel({
isLoading &&
!hasAssistantMedia &&
index === renderableVisibleMessages.length - 1;
const shouldTypeClawAssistantMessage =
message.role === "assistant" &&
!hasAssistantMedia &&
Boolean(clawdTypewriterMessageIds[message.id]);
const messageTimestamp = getMessageTimestamp(message);
const prevMessageTimestamp =
@ -11137,16 +11281,18 @@ export function CanvasChatPanel({
const analysisTypingKey = `${message.id}-${i}-analysis-typing`;
const plainTextTypingKey = `${message.id}-${i}-plain-typing`;
const shouldAnimateAnalysis =
shouldTypeAssistantMessage &&
Boolean(analysisSection?.analysis) &&
!hasCanvasRenderableMarkdown(analysisSection?.analysis || "") &&
!typedAssistantSections[analysisTypingKey];
!typedAssistantSections[analysisTypingKey] &&
((shouldTypeAssistantMessage &&
!hasCanvasRenderableMarkdown(analysisSection?.analysis || "")) ||
shouldTypeClawAssistantMessage);
const shouldAnimatePlainText =
shouldTypeAssistantMessage &&
!analysisSection &&
Boolean(plainText) &&
!hasCanvasRenderableMarkdown(plainText) &&
!typedAssistantSections[plainTextTypingKey];
!typedAssistantSections[plainTextTypingKey] &&
((shouldTypeAssistantMessage &&
!hasCanvasRenderableMarkdown(plainText)) ||
shouldTypeClawAssistantMessage);
const shouldShowDoneSummary =
!isDetailGenerationMode &&
imageUrls.length > 0 &&
@ -11216,6 +11362,7 @@ export function CanvasChatPanel({
enabled={shouldAnimateAnalysis}
className="break-words [overflow-wrap:anywhere]"
renderMarkdown
preserveMarkdownTables={shouldTypeClawAssistantMessage}
onActiveChange={handleTypewriterActiveChange}
onComplete={handleTypewriterComplete}
/>
@ -11232,6 +11379,7 @@ export function CanvasChatPanel({
enabled={shouldAnimatePlainText}
className="break-words [overflow-wrap:anywhere]"
renderMarkdown
preserveMarkdownTables={shouldTypeClawAssistantMessage}
onActiveChange={handleTypewriterActiveChange}
onComplete={handleTypewriterComplete}
/>
@ -13023,7 +13171,7 @@ export function CanvasChatPanel({
className={getComposerModeMenuItemClass("clawd")}
>
<MessageCircle className="h-5 w-5" />
<span className="flex-1">Clawd</span>
<span className="flex-1">{CLAW_MODE_LABEL}</span>
{activeComposerMode === "clawd" ? (
<Check className={cn("h-4 w-4", isVideoWorkspace ? "text-primary" : "text-[#1f2937]")} />
) : null}

View File

@ -336,7 +336,7 @@ export async function spreadsheetFileToOpenClawMarkdown(file: File) {
const extension = getSpreadsheetExtension(file.name);
if (!SUPPORTED_EXTENSIONS.has(extension)) {
throw new Error("Clawd 目前支持 .xlsx、.csv、.tsv 表格文件");
throw new Error("Claw 目前支持 .xlsx、.csv、.tsv 表格文件");
}
if (extension === "csv") {