fix: rename Claw mode and type responses
This commit is contained in:
parent
d5fed4f2da
commit
fa10115043
@ -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
|
||||
|
||||
@ -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 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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") {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user