feat: persist claw skill tag order
This commit is contained in:
parent
c789dd7a69
commit
469ae7b02d
@ -4,26 +4,140 @@ import { auth } from "@/app/(auth)/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { systemSettings } from "@/lib/db/schema";
|
||||
import {
|
||||
CANVAS_SKILL_TAG_ORDER_SETTING_KEY_PREFIX,
|
||||
CANVAS_SKILL_TAGS_CONFIG_VERSION,
|
||||
CANVAS_SKILL_TAGS_SETTING_KEY,
|
||||
type CanvasSkillTag,
|
||||
normalizeCanvasSkillTagSettings,
|
||||
} from "@/lib/canvas-skill-tags";
|
||||
|
||||
function getUserSkillTagOrderKey(userId: string) {
|
||||
return `${CANVAS_SKILL_TAG_ORDER_SETTING_KEY_PREFIX}${userId}`;
|
||||
}
|
||||
|
||||
function normalizeSkillTagOrder(value: unknown) {
|
||||
const source =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>).tagIds
|
||||
: value;
|
||||
if (!Array.isArray(source)) return [];
|
||||
|
||||
const seen = new Set<string>();
|
||||
return source
|
||||
.map((item) => String(item || "").trim())
|
||||
.filter((item) => {
|
||||
if (!item || seen.has(item)) return false;
|
||||
seen.add(item);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function applySkillTagOrder(tags: CanvasSkillTag[], order: string[]) {
|
||||
if (tags.length < 2 || order.length === 0) return tags;
|
||||
|
||||
const tagById = new Map(tags.map((tag) => [tag.id, tag]));
|
||||
const ordered = order
|
||||
.map((id) => tagById.get(id))
|
||||
.filter((tag): tag is CanvasSkillTag => Boolean(tag));
|
||||
const orderedIds = new Set(ordered.map((tag) => tag.id));
|
||||
const remaining = tags.filter((tag) => !orderedIds.has(tag.id));
|
||||
return [...ordered, ...remaining];
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth().catch(() => null);
|
||||
const userId = session?.user?.id || null;
|
||||
const [setting, userOrderSetting] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, CANVAS_SKILL_TAGS_SETTING_KEY))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0]),
|
||||
userId
|
||||
? db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, getUserSkillTagOrderKey(userId)))
|
||||
.limit(1)
|
||||
.then((rows) => rows[0])
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const settings = normalizeCanvasSkillTagSettings(setting?.value, {
|
||||
requireCurrentVersion: true,
|
||||
});
|
||||
const userOrder = normalizeSkillTagOrder(userOrderSetting?.value);
|
||||
return NextResponse.json({
|
||||
...settings,
|
||||
tags: applySkillTagOrder(settings.tags, userOrder),
|
||||
userOrder,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[CanvasSkillTags API] Get error:", error);
|
||||
return NextResponse.json(normalizeCanvasSkillTagSettings(null));
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const requestedOrder = normalizeSkillTagOrder(
|
||||
body && typeof body === "object" ? (body as Record<string, unknown>).tagIds : body
|
||||
);
|
||||
|
||||
const [setting] = await db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, CANVAS_SKILL_TAGS_SETTING_KEY))
|
||||
.limit(1);
|
||||
|
||||
const settings = normalizeCanvasSkillTagSettings(setting?.value, {
|
||||
requireCurrentVersion: true,
|
||||
});
|
||||
return NextResponse.json(settings);
|
||||
const knownIds = new Set(settings.tags.map((tag) => tag.id));
|
||||
const orderedKnownIds = requestedOrder.filter((id) => knownIds.has(id));
|
||||
const orderedSet = new Set(orderedKnownIds);
|
||||
const tagIds = [
|
||||
...orderedKnownIds,
|
||||
...settings.tags.map((tag) => tag.id).filter((id) => !orderedSet.has(id)),
|
||||
];
|
||||
const value = {
|
||||
version: 1,
|
||||
tagIds,
|
||||
};
|
||||
const key = getUserSkillTagOrderKey(session.user.id);
|
||||
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key,
|
||||
value,
|
||||
updatedBy: session.user.id,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value,
|
||||
updatedBy: session.user.id,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
userOrder: tagIds,
|
||||
tags: applySkillTagOrder(settings.tags, tagIds),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[CanvasSkillTags API] Get error:", error);
|
||||
return NextResponse.json(normalizeCanvasSkillTagSettings(null));
|
||||
console.error("[CanvasSkillTags API] Save order error:", error);
|
||||
return NextResponse.json({ error: "Failed to save order" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -133,6 +133,14 @@ function getOpenClawErrorCode(payload: unknown) {
|
||||
return typeof code === "string" ? code.trim() : "";
|
||||
}
|
||||
|
||||
function getMetadataString(
|
||||
metadata: Record<string, unknown> | undefined,
|
||||
key: string
|
||||
) {
|
||||
const value = metadata?.[key];
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth().catch(() => null);
|
||||
|
||||
@ -211,6 +219,8 @@ export async function POST(request: Request) {
|
||||
email: userProfile?.email ?? session.user.email,
|
||||
});
|
||||
const openClawDepartmentName = getOpenClawDepartmentName(userProfile?.department);
|
||||
const skillTagId = getMetadataString(parsed.metadata, "skillTagId");
|
||||
const skillTagName = getMetadataString(parsed.metadata, "skillTagName");
|
||||
const endpoint = `${config.baseUrl}/api/chat`;
|
||||
|
||||
let upstreamPayload: unknown = null;
|
||||
@ -233,6 +243,8 @@ export async function POST(request: Request) {
|
||||
department: openClawDepartmentName,
|
||||
department_name: openClawDepartmentName,
|
||||
user_department: openClawDepartmentName,
|
||||
skill_tag_id: skillTagId || undefined,
|
||||
skill_tag_name: skillTagName || undefined,
|
||||
message: upstreamMessage,
|
||||
metadata: {
|
||||
source: "ai_chatbot_canvas",
|
||||
@ -245,6 +257,8 @@ export async function POST(request: Request) {
|
||||
department: openClawDepartmentName,
|
||||
department_name: openClawDepartmentName,
|
||||
user_department: openClawDepartmentName,
|
||||
skill_tag_id: skillTagId || undefined,
|
||||
skill_tag_name: skillTagName || undefined,
|
||||
spreadsheet_files: spreadsheetMarkdownResult?.files || undefined,
|
||||
spreadsheet_markdown_truncated:
|
||||
spreadsheetMarkdownResult?.truncated || undefined,
|
||||
@ -326,6 +340,8 @@ export async function POST(request: Request) {
|
||||
videoMetadata: {
|
||||
composerMode: "clawd",
|
||||
source: "openclaw",
|
||||
skillTagId: skillTagId || null,
|
||||
skillTagName: skillTagName || null,
|
||||
},
|
||||
};
|
||||
const assistantMessage = {
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { useEffect, useRef, useState, useCallback, useMemo, type ClipboardEvent as ReactClipboardEvent, type MouseEvent as ReactMouseEvent } from "react";
|
||||
import { useEffect, useRef, useState, useCallback, useMemo, type ClipboardEvent as ReactClipboardEvent, type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { createPortal } from "react-dom";
|
||||
@ -418,6 +418,24 @@ function buildClawdSkillTagPrompt(tag: Pick<CanvasSkillTag, "name" | "prompt"> |
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function reorderCanvasSkillTags(
|
||||
tags: CanvasSkillTag[],
|
||||
draggedId: string,
|
||||
targetId: string
|
||||
) {
|
||||
if (!draggedId || !targetId || draggedId === targetId) return tags;
|
||||
const currentIndex = tags.findIndex((tag) => tag.id === draggedId);
|
||||
const targetIndex = tags.findIndex((tag) => tag.id === targetId);
|
||||
if (currentIndex < 0 || targetIndex < 0) return tags;
|
||||
|
||||
const nextTags = [...tags];
|
||||
const [draggedTag] = nextTags.splice(currentIndex, 1);
|
||||
if (!draggedTag) return tags;
|
||||
const nextTargetIndex = nextTags.findIndex((tag) => tag.id === targetId);
|
||||
nextTags.splice(nextTargetIndex < 0 ? targetIndex : nextTargetIndex, 0, draggedTag);
|
||||
return nextTags;
|
||||
}
|
||||
|
||||
type ClawdComposerSubmissionSnapshot = {
|
||||
chatId: string;
|
||||
canvasId?: string;
|
||||
@ -2166,6 +2184,10 @@ export function CanvasChatPanel({
|
||||
const [isComposerModeMenuOpen, setIsComposerModeMenuOpen] = useState(false);
|
||||
const [selectedSkillTagId, setSelectedSkillTagId] = useState<string | null>(null);
|
||||
const [areCanvasSkillTagsExpanded, setAreCanvasSkillTagsExpanded] = useState(false);
|
||||
const [draggingSkillTagId, setDraggingSkillTagId] = useState<string | null>(null);
|
||||
const pendingSelectedClawdSkillTagRef = useRef<CanvasSkillTag | null>(null);
|
||||
const draggingSkillTagIdRef = useRef<string | null>(null);
|
||||
const suppressSkillTagClickRef = useRef(false);
|
||||
const [messageViewportKey, setMessageViewportKey] = useState(0);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@ -3948,6 +3970,7 @@ export function CanvasChatPanel({
|
||||
enabled?: boolean;
|
||||
title?: string;
|
||||
tags?: CanvasSkillTag[];
|
||||
userOrder?: string[];
|
||||
}>(
|
||||
"/api/canvas-skill-tags",
|
||||
fetcher,
|
||||
@ -4136,6 +4159,42 @@ export function CanvasChatPanel({
|
||||
() => (canvasSkillTagsEnabled ? normalizeCanvasSkillTags(canvasSkillTagsData?.tags) : []),
|
||||
[canvasSkillTagsData?.tags, canvasSkillTagsEnabled]
|
||||
);
|
||||
const handleReorderCanvasSkillTag = useCallback(
|
||||
async (draggedId: string, targetId: string) => {
|
||||
const nextTags = reorderCanvasSkillTags(canvasSkillTags, draggedId, targetId);
|
||||
if (nextTags === canvasSkillTags) return;
|
||||
|
||||
const userOrder = nextTags.map((tag) => tag.id);
|
||||
const optimisticData = {
|
||||
...(canvasSkillTagsData || {}),
|
||||
enabled: canvasSkillTagsData?.enabled,
|
||||
title: canvasSkillTagsData?.title,
|
||||
tags: nextTags,
|
||||
userOrder,
|
||||
};
|
||||
void mutate("/api/canvas-skill-tags", optimisticData, false);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/canvas-skill-tags", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ tagIds: userOrder }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to save skill tag order");
|
||||
}
|
||||
void mutate("/api/canvas-skill-tags");
|
||||
toast.success("快捷标签顺序已保存");
|
||||
} catch (error) {
|
||||
console.error("[CanvasChat] 保存快捷标签顺序失败:", error);
|
||||
toast.error("快捷标签顺序保存失败");
|
||||
void mutate("/api/canvas-skill-tags");
|
||||
}
|
||||
},
|
||||
[canvasSkillTags, canvasSkillTagsData, mutate]
|
||||
);
|
||||
const selectedSkillTag = useMemo(
|
||||
() => canvasSkillTags.find((tag) => tag.id === selectedSkillTagId) || null,
|
||||
[canvasSkillTags, selectedSkillTagId]
|
||||
@ -4177,13 +4236,19 @@ export function CanvasChatPanel({
|
||||
if (!selectedSkillTagId) return;
|
||||
if (!canvasSkillTags.some((tag) => tag.id === selectedSkillTagId)) {
|
||||
setSelectedSkillTagId(null);
|
||||
pendingSelectedClawdSkillTagRef.current = null;
|
||||
}
|
||||
}, [canvasSkillTags, selectedSkillTagId]);
|
||||
|
||||
useEffect(() => {
|
||||
pendingSelectedClawdSkillTagRef.current = selectedSkillTag;
|
||||
}, [selectedSkillTag]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWorkspacePresentation) return;
|
||||
if (!selectedSkillTagId) return;
|
||||
setSelectedSkillTagId(null);
|
||||
pendingSelectedClawdSkillTagRef.current = null;
|
||||
}, [isWorkspacePresentation, selectedSkillTagId]);
|
||||
|
||||
const visualModels = useMemo(() => {
|
||||
@ -5314,6 +5379,9 @@ export function CanvasChatPanel({
|
||||
|
||||
const handleSelectCanvasSkillTag = useCallback(
|
||||
(tag: CanvasSkillTag) => {
|
||||
if (suppressSkillTagClickRef.current) {
|
||||
return;
|
||||
}
|
||||
if (!isClawdModeEnabled) {
|
||||
toast.error(`${CLAW_MODE_LABEL} 未开启`);
|
||||
return;
|
||||
@ -5331,6 +5399,7 @@ export function CanvasChatPanel({
|
||||
setSessionClawdSkillTag(targetChatSessionId, tag.id, {
|
||||
promptApplied: false,
|
||||
});
|
||||
pendingSelectedClawdSkillTagRef.current = tag;
|
||||
window.setTimeout(() => {
|
||||
setSelectedSkillTagId(tag.id);
|
||||
const editor = textareaRef.current as HTMLDivElement | null;
|
||||
@ -5348,6 +5417,49 @@ export function CanvasChatPanel({
|
||||
]
|
||||
);
|
||||
|
||||
const handleCanvasSkillTagDragStart = useCallback(
|
||||
(event: ReactDragEvent<HTMLButtonElement>, tagId: string) => {
|
||||
draggingSkillTagIdRef.current = tagId;
|
||||
setDraggingSkillTagId(tagId);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("text/plain", tagId);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCanvasSkillTagDragOver = useCallback(
|
||||
(event: ReactDragEvent<HTMLButtonElement>, targetId: string) => {
|
||||
const draggedId = draggingSkillTagIdRef.current;
|
||||
if (!draggedId || draggedId === targetId) return;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleCanvasSkillTagDrop = useCallback(
|
||||
(event: ReactDragEvent<HTMLButtonElement>, targetId: string) => {
|
||||
event.preventDefault();
|
||||
const draggedId =
|
||||
event.dataTransfer.getData("text/plain") || draggingSkillTagIdRef.current;
|
||||
draggingSkillTagIdRef.current = null;
|
||||
setDraggingSkillTagId(null);
|
||||
if (!draggedId || draggedId === targetId) return;
|
||||
|
||||
suppressSkillTagClickRef.current = true;
|
||||
window.setTimeout(() => {
|
||||
suppressSkillTagClickRef.current = false;
|
||||
}, 0);
|
||||
void handleReorderCanvasSkillTag(draggedId, targetId);
|
||||
},
|
||||
[handleReorderCanvasSkillTag]
|
||||
);
|
||||
|
||||
const handleCanvasSkillTagDragEnd = useCallback(() => {
|
||||
draggingSkillTagIdRef.current = null;
|
||||
setDraggingSkillTagId(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!effectiveAgentEnabled || typeof window === "undefined") return;
|
||||
const targetWindow = window as CanvasAgentPendingRequestWindow;
|
||||
@ -10191,13 +10303,18 @@ export function CanvasChatPanel({
|
||||
const textContent = normalizeClawdComposerText(composerInput);
|
||||
const tableFilesSnapshot = isProgrammaticClawdQuickSend ? [] : [...clawdTableFiles];
|
||||
const hasTableFiles = tableFilesSnapshot.length > 0;
|
||||
const pendingSelectedClawdSkillTag = pendingSelectedClawdSkillTagRef.current;
|
||||
const clawdSkillTagForRequest = isProgrammaticClawdQuickSend
|
||||
? activeClawdSkillTag
|
||||
: selectedSkillTag || activeClawdSkillTag;
|
||||
: selectedSkillTag || pendingSelectedClawdSkillTag || activeClawdSkillTag;
|
||||
const hasFreshClawdSkillTagSelection = Boolean(
|
||||
!isProgrammaticClawdQuickSend &&
|
||||
(selectedSkillTag || pendingSelectedClawdSkillTag)
|
||||
);
|
||||
const shouldIncludeClawdSkillTagPrompt = Boolean(
|
||||
!isProgrammaticClawdQuickSend &&
|
||||
clawdSkillTagForRequest &&
|
||||
(selectedSkillTag || !activeClawdSkillTagPromptApplied)
|
||||
(hasFreshClawdSkillTagSelection || !activeClawdSkillTagPromptApplied)
|
||||
);
|
||||
const clawdSkillTagPrompt = shouldIncludeClawdSkillTagPrompt
|
||||
? buildClawdSkillTagPrompt(clawdSkillTagForRequest)
|
||||
@ -10234,10 +10351,17 @@ export function CanvasChatPanel({
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
const optimisticBaseMessage =
|
||||
textContent ||
|
||||
(clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt
|
||||
? `快捷功能:${clawdSkillTagForRequest.name}`
|
||||
: baseRequestMessage);
|
||||
[
|
||||
clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt
|
||||
? `快捷功能:${clawdSkillTagForRequest.name}`
|
||||
: "",
|
||||
textContent ||
|
||||
(clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt
|
||||
? ""
|
||||
: baseRequestMessage),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n") || baseRequestMessage;
|
||||
const optimisticMessageText = [
|
||||
buildClawdTableDisplayText(optimisticBaseMessage, tableFilesSnapshot),
|
||||
buildClawdImageDisplayText(imageAttachmentsSnapshot),
|
||||
@ -10341,6 +10465,7 @@ export function CanvasChatPanel({
|
||||
setAttachments([]);
|
||||
setClawdTableFiles([]);
|
||||
setSelectedSkillTagId(null);
|
||||
pendingSelectedClawdSkillTagRef.current = null;
|
||||
clearSelection();
|
||||
}
|
||||
|
||||
@ -10356,6 +10481,9 @@ export function CanvasChatPanel({
|
||||
composerMode: "clawd",
|
||||
activeChatId,
|
||||
imageAttachmentCount: imageAttachmentsSnapshot.length || undefined,
|
||||
skillTagId: clawdSkillTagForRequest?.id || undefined,
|
||||
skillTagName: clawdSkillTagForRequest?.name || undefined,
|
||||
skillTagPromptIncluded: hasPendingClawdSkillTagPrompt || undefined,
|
||||
})
|
||||
);
|
||||
tableFilesSnapshot.forEach((file) => {
|
||||
@ -10430,6 +10558,7 @@ export function CanvasChatPanel({
|
||||
setAttachments(attachmentsSnapshot);
|
||||
if (clawdSkillTagForRequest && hasPendingClawdSkillTagPrompt) {
|
||||
setSelectedSkillTagId(clawdSkillTagForRequest.id);
|
||||
pendingSelectedClawdSkillTagRef.current = clawdSkillTagForRequest;
|
||||
}
|
||||
const editableDiv = textareaRef.current as HTMLDivElement | null;
|
||||
const currentEditorText = (editableDiv?.textContent || "")
|
||||
@ -12305,7 +12434,11 @@ export function CanvasChatPanel({
|
||||
)}
|
||||
<span className="truncate">{activeClawdSkillTag.name}</span>
|
||||
<span className="shrink-0 text-[10px] opacity-70">
|
||||
{activeClawdSkillTagPromptApplied ? "使用中" : "待带入"}
|
||||
{activeClawdSkillTagPromptApplied
|
||||
? "使用中"
|
||||
: isClawdSubmitting
|
||||
? "发送中"
|
||||
: "待带入"}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
@ -12604,18 +12737,30 @@ export function CanvasChatPanel({
|
||||
>
|
||||
{canvasSkillTags.map((tag) => {
|
||||
const isActive = selectedSkillTag?.id === tag.id;
|
||||
const isDragging = draggingSkillTagId === tag.id;
|
||||
const TagIcon = getCanvasSkillIconComponent(tag.iconName);
|
||||
return (
|
||||
<button
|
||||
key={tag.id}
|
||||
type="button"
|
||||
draggable={canvasSkillTags.length > 1}
|
||||
onDragStart={(event) =>
|
||||
handleCanvasSkillTagDragStart(event, tag.id)
|
||||
}
|
||||
onDragOver={(event) =>
|
||||
handleCanvasSkillTagDragOver(event, tag.id)
|
||||
}
|
||||
onDrop={(event) => handleCanvasSkillTagDrop(event, tag.id)}
|
||||
onDragEnd={handleCanvasSkillTagDragEnd}
|
||||
onClick={() => handleSelectCanvasSkillTag(tag)}
|
||||
className={cn(
|
||||
"inline-flex h-[34px] max-w-full items-center justify-center gap-1.5 rounded-full border px-3.5 text-[13px] font-medium leading-none transition-colors",
|
||||
"inline-flex h-[34px] max-w-full cursor-grab items-center justify-center gap-1.5 rounded-full border px-3.5 text-[13px] font-medium leading-none transition-colors active:cursor-grabbing",
|
||||
isActive
|
||||
? "border-[#d8dce4] bg-[#f7f8fa] text-[#1f2328] dark:border-blue-400/40 dark:bg-blue-500/15 dark:text-blue-200"
|
||||
: "border-[#e5e7eb] bg-white text-[#1f2328] hover:border-[#d6dae3] hover:bg-[#fafbfc] dark:border-white/10 dark:bg-white/5 dark:text-zinc-200 dark:hover:border-white/20 dark:hover:bg-white/10"
|
||||
: "border-[#e5e7eb] bg-white text-[#1f2328] hover:border-[#d6dae3] hover:bg-[#fafbfc] dark:border-white/10 dark:bg-white/5 dark:text-zinc-200 dark:hover:border-white/20 dark:hover:bg-white/10",
|
||||
isDragging && "scale-95 opacity-45"
|
||||
)}
|
||||
title="点击使用,拖动调整顺序"
|
||||
>
|
||||
{tag.iconUrl ? (
|
||||
<img
|
||||
@ -12939,10 +13084,19 @@ export function CanvasChatPanel({
|
||||
((shouldTypeAssistantMessage &&
|
||||
!hasCanvasRenderableMarkdown(plainText)) ||
|
||||
shouldTypeClawAssistantMessage);
|
||||
const shouldDeferAssistantMedia =
|
||||
isClawAssistantMessage &&
|
||||
(shouldAnimateAnalysis || shouldAnimatePlainText);
|
||||
const visibleImageUrls = shouldDeferAssistantMedia
|
||||
? []
|
||||
: imageUrls;
|
||||
const visibleVideoUrls = shouldDeferAssistantMedia
|
||||
? []
|
||||
: videoUrls;
|
||||
const shouldShowDoneSummary =
|
||||
!isDetailGenerationMode &&
|
||||
!isClawAssistantMessage &&
|
||||
imageUrls.length > 0 &&
|
||||
visibleImageUrls.length > 0 &&
|
||||
!plainText.includes(CANVAS_IMAGE_DONE_SUMMARY) &&
|
||||
!isLatestAssistantDuringGeneration;
|
||||
const renderPlainText = (text: string, key: string) => (
|
||||
@ -13041,7 +13195,7 @@ export function CanvasChatPanel({
|
||||
onComplete={handleTypewriterComplete}
|
||||
/>
|
||||
) : null}
|
||||
{imageUrls.length > 0 && (
|
||||
{visibleImageUrls.length > 0 && (
|
||||
<div className={cn("space-y-2", textWithoutImages ? "mt-3" : "mt-0")}>
|
||||
{mediaModelName ? (
|
||||
<div className="flex items-center gap-1.5 text-[#a4acb8]">
|
||||
@ -13057,7 +13211,7 @@ export function CanvasChatPanel({
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{imageUrls.map((url, idx) => (
|
||||
{visibleImageUrls.map((url, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={cn(
|
||||
@ -13135,9 +13289,9 @@ export function CanvasChatPanel({
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{videoUrls.length > 0 && (
|
||||
<div className={cn("space-y-2", textWithoutImages || imageUrls.length > 0 ? "mt-3" : "mt-0")}>
|
||||
{videoUrls.map((url, idx) => (
|
||||
{visibleVideoUrls.length > 0 && (
|
||||
<div className={cn("space-y-2", textWithoutImages || visibleImageUrls.length > 0 ? "mt-3" : "mt-0")}>
|
||||
{visibleVideoUrls.map((url, idx) => (
|
||||
<div
|
||||
key={`video-${idx}`}
|
||||
className="overflow-hidden rounded-2xl border border-black/10 bg-black/5 p-2 dark:border-white/10 dark:bg-white/5"
|
||||
@ -14133,7 +14287,10 @@ export function CanvasChatPanel({
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-[#f3f4f6] px-3 py-1.5 text-sm font-medium text-[#4b5563] transition-colors hover:bg-[#e9ebef] dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12"
|
||||
onClick={() => setSelectedSkillTagId(null)}
|
||||
onClick={() => {
|
||||
setSelectedSkillTagId(null);
|
||||
pendingSelectedClawdSkillTagRef.current = null;
|
||||
}}
|
||||
title="点击移除标签"
|
||||
>
|
||||
{selectedSkillTag.iconUrl ? (
|
||||
@ -14176,7 +14333,7 @@ export function CanvasChatPanel({
|
||||
)}
|
||||
<span className="truncate">{activeClawdSkillTag.name}</span>
|
||||
<span className="shrink-0 rounded-full bg-white/70 px-1.5 py-0.5 text-[10px] font-semibold text-violet-500 dark:bg-white/12 dark:text-violet-100">
|
||||
待发送
|
||||
{isClawdSubmitting ? "已带入" : "待发送"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
@ -14184,6 +14341,7 @@ export function CanvasChatPanel({
|
||||
onClick={() => {
|
||||
setSessionClawdSkillTag(activeChatId, null);
|
||||
setSelectedSkillTagId(null);
|
||||
pendingSelectedClawdSkillTagRef.current = null;
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
aria-label={`移除 ${activeClawdSkillTag.name} 快捷标签`}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export const CANVAS_SKILL_TAGS_SETTING_KEY = "canvas_skill_tags";
|
||||
export const CANVAS_SKILL_TAG_ORDER_SETTING_KEY_PREFIX = "canvas_skill_tag_order:";
|
||||
export const CANVAS_SKILL_TAGS_CONFIG_VERSION = 4;
|
||||
export const DEFAULT_CANVAS_SKILL_TAGS_TITLE = "Claw 快捷技能";
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user