feat: add Claw skill search

This commit is contained in:
134 2026-08-04 13:49:10 +08:00 committed by root
parent d467694aa7
commit 233c7b3308

View File

@ -25,7 +25,7 @@ import {
type UiSurfaceSnapshot,
} from "@/lib/ui-surface-protocol";
import useSWR, { useSWRConfig } from "swr";
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, Paperclip, Copy, Square, GripVertical } from "lucide-react";
import { Loader2, ImagePlus, X, Plus, History, LayoutGrid, ArrowRightToLine, RefreshCw, Globe, ArrowUp, Video, Sparkles, Check, ChevronDown, ChevronUp, AlertCircle, CheckCircle2, Brain, ThumbsUp, ThumbsDown, Eye, Trash2, Download, MessageCircle, Clapperboard, Bot, FileSpreadsheet, Paperclip, Copy, Square, Search, GripVertical } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Collapsible,
@ -2656,6 +2656,7 @@ export function CanvasChatPanel({
const [isComposerModeMenuOpen, setIsComposerModeMenuOpen] = useState(false);
const [selectedSkillTagId, setSelectedSkillTagId] = useState<string | null>(null);
const [areCanvasSkillTagsExpanded, setAreCanvasSkillTagsExpanded] = useState(false);
const [canvasSkillTagSearchQuery, setCanvasSkillTagSearchQuery] = useState("");
const [draggingSkillTagId, setDraggingSkillTagId] = useState<string | null>(null);
const [canvasSkillTagOrderOverride, setCanvasSkillTagOrderOverride] = useState<string[] | null>(
null
@ -4817,6 +4818,25 @@ export function CanvasChatPanel({
: baseCanvasSkillTags,
[baseCanvasSkillTags, canvasSkillTagOrderOverride]
);
const normalizedCanvasSkillTagSearchQuery = canvasSkillTagSearchQuery
.trim()
.toLocaleLowerCase();
const filteredCanvasSkillTags = useMemo(() => {
if (!normalizedCanvasSkillTagSearchQuery) {
return canvasSkillTags;
}
const keywords = normalizedCanvasSkillTagSearchQuery.split(/\s+/).filter(Boolean);
return canvasSkillTags.filter((tag) => {
const searchableText = [tag.name, tag.prompt, tag.placeholder]
.map((value) => String(value || "").trim())
.filter(Boolean)
.join("\n")
.toLocaleLowerCase();
return keywords.every((keyword) => searchableText.includes(keyword));
});
}, [canvasSkillTags, normalizedCanvasSkillTagSearchQuery]);
const isCanvasSkillTagSearching = normalizedCanvasSkillTagSearchQuery.length > 0;
useEffect(() => {
latestCanvasSkillTagsRef.current = canvasSkillTags;
}, [canvasSkillTags]);
@ -4869,7 +4889,8 @@ export function CanvasChatPanel({
() => canvasSkillTags.find((tag) => tag.id === selectedSkillTagId) || null,
[canvasSkillTags, selectedSkillTagId]
);
const shouldShowCanvasSkillTagsToggle = canvasSkillTags.length > 4;
const shouldShowCanvasSkillTagsToggle =
!isCanvasSkillTagSearching && canvasSkillTags.length > 4;
const activeChatSession = useMemo(
() => chatSessions.find((session) => session.id === activeChatId) || null,
[activeChatId, chatSessions]
@ -6169,6 +6190,7 @@ export function CanvasChatPanel({
promptApplied: false,
});
pendingSelectedClawdSkillTagRef.current = tag;
setCanvasSkillTagSearchQuery("");
window.setTimeout(() => {
setSelectedSkillTagId(tag.id);
const editor = textareaRef.current as HTMLDivElement | null;
@ -14382,15 +14404,49 @@ export function CanvasChatPanel({
<h3 className="text-[14px] font-semibold leading-none text-[#1f2328] dark:text-zinc-100">
{canvasSkillTagsTitle}
</h3>
{canvasSkillTags.length > 4 ? (
<div className="mx-auto mt-5 flex h-9 w-full max-w-[360px] items-center gap-2 rounded-lg border border-[#e5e7eb] bg-[#fafbfc] px-3 text-left transition-colors focus-within:border-[#aeb6c5] focus-within:bg-white dark:border-white/10 dark:bg-white/5 dark:focus-within:border-white/25 dark:focus-within:bg-white/[0.07]">
<Search className="h-4 w-4 shrink-0 text-[#9299a6] dark:text-zinc-500" />
<input
type="text"
value={canvasSkillTagSearchQuery}
onChange={(event) =>
setCanvasSkillTagSearchQuery(event.target.value)
}
onKeyDown={(event) => {
if (event.key === "Escape") {
setCanvasSkillTagSearchQuery("");
}
}}
placeholder="搜索快捷技能"
aria-label="搜索快捷技能"
autoComplete="off"
enterKeyHint="search"
spellCheck={false}
className="h-full min-w-0 flex-1 bg-transparent text-[13px] text-[#1f2328] outline-none placeholder:text-[#9aa1ad] dark:text-zinc-100 dark:placeholder:text-zinc-500"
/>
{canvasSkillTagSearchQuery ? (
<button
type="button"
onClick={() => setCanvasSkillTagSearchQuery("")}
className="inline-flex h-6 w-6 shrink-0 items-center justify-center text-[#9299a6] transition-colors hover:text-[#1f2328] dark:text-zinc-500 dark:hover:text-zinc-100"
title="清空搜索"
aria-label="清空快捷技能搜索"
>
<X className="h-3.5 w-3.5" />
</button>
) : null}
</div>
) : null}
<div
className={cn(
"mt-4 flex flex-wrap justify-center gap-1.5 transition-[max-height] duration-150 ease-out",
areCanvasSkillTagsExpanded
isCanvasSkillTagSearching || areCanvasSkillTagsExpanded
? "max-h-none overflow-visible"
: "max-h-[76px] overflow-hidden"
)}
>
{canvasSkillTags.map((tag) => {
{filteredCanvasSkillTags.map((tag) => {
const isActive = selectedSkillTag?.id === tag.id;
const isDragging = draggingSkillTagId === tag.id;
const TagIcon = getCanvasSkillIconComponent(tag.iconName);
@ -14398,7 +14454,7 @@ export function CanvasChatPanel({
<button
key={tag.id}
type="button"
draggable={canvasSkillTags.length > 1}
draggable={!isCanvasSkillTagSearching && canvasSkillTags.length > 1}
onDragStart={(event) =>
handleCanvasSkillTagDragStart(event, tag.id)
}
@ -14412,13 +14468,20 @@ export function CanvasChatPanel({
onDragEnd={handleCanvasSkillTagDragEnd}
onClick={() => handleSelectCanvasSkillTag(tag)}
className={cn(
"inline-flex h-8 max-w-full cursor-grab items-center justify-center gap-1.5 rounded-full border px-3 text-[12px] font-medium leading-none transition-colors duration-150 active:cursor-grabbing",
"inline-flex h-8 max-w-full items-center justify-center gap-1.5 rounded-full border px-3 text-[12px] font-medium leading-none transition-colors duration-150",
isCanvasSkillTagSearching
? "cursor-pointer"
: "cursor-grab 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",
isDragging && "scale-95 opacity-45"
)}
title="点击使用,拖动调整顺序"
title={
isCanvasSkillTagSearching
? "点击使用快捷技能"
: "点击使用,拖动调整顺序"
}
>
{tag.iconUrl ? (
<img
@ -14440,6 +14503,11 @@ export function CanvasChatPanel({
</button>
);
})}
{isCanvasSkillTagSearching && filteredCanvasSkillTags.length === 0 ? (
<p className="w-full py-2 text-center text-[13px] text-[#9299a6] dark:text-zinc-500">
未找到匹配的快捷技能
</p>
) : null}
</div>
{shouldShowCanvasSkillTagsToggle ? (
<button