mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-03 19:20:07 +02:00
feat(agents): add search box to skill picker dialog (#2269)
Filters available skills by name + description (case-insensitive) as the user types. Auto-focuses on open and clears the query on close. Shows a distinct "no match" empty state vs. the existing "all assigned" one. Closes #2266 Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { FileText } from "lucide-react";
|
||||
import { FileText, Search } from "lucide-react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import type { Agent } from "@multica/core/types";
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@multica/ui/components/ui/dialog";
|
||||
import { Input } from "@multica/ui/components/ui/input";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
/**
|
||||
@@ -46,11 +47,27 @@ export function SkillAddDialog({
|
||||
const qc = useQueryClient();
|
||||
const { data: workspaceSkills = [] } = useQuery(skillListOptions(wsId));
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const agentSkillIds = new Set(agent.skills.map((s) => s.id));
|
||||
const availableSkills = workspaceSkills.filter(
|
||||
(s) => !agentSkillIds.has(s.id),
|
||||
);
|
||||
const trimmedQuery = query.trim().toLowerCase();
|
||||
const filteredSkills = trimmedQuery
|
||||
? availableSkills.filter((s) => {
|
||||
const name = s.name.toLowerCase();
|
||||
const description = s.description?.toLowerCase() ?? "";
|
||||
return (
|
||||
name.includes(trimmedQuery) || description.includes(trimmedQuery)
|
||||
);
|
||||
})
|
||||
: availableSkills;
|
||||
|
||||
const handleOpenChange = (v: boolean) => {
|
||||
if (!v) setQuery("");
|
||||
onOpenChange(v);
|
||||
};
|
||||
|
||||
const handleAdd = async (skillId: string) => {
|
||||
setSaving(true);
|
||||
@@ -58,7 +75,7 @@ export function SkillAddDialog({
|
||||
const newIds = [...agent.skills.map((s) => s.id), skillId];
|
||||
await api.setAgentSkills(agent.id, { skill_ids: newIds });
|
||||
qc.invalidateQueries({ queryKey: workspaceKeys.agents(wsId) });
|
||||
onOpenChange(false);
|
||||
handleOpenChange(false);
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : t(($) => $.tab_body.skills.add_failed_toast));
|
||||
} finally {
|
||||
@@ -66,8 +83,11 @@ export function SkillAddDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const showSearch = availableSkills.length > 0;
|
||||
const noMatch = showSearch && filteredSkills.length === 0;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-sm">{t(($) => $.tab_body.skills.add_dialog_title)}</DialogTitle>
|
||||
@@ -75,8 +95,21 @@ export function SkillAddDialog({
|
||||
{t(($) => $.tab_body.skills.add_dialog_description)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{showSearch && (
|
||||
<div className="relative">
|
||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
autoFocus
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder={t(($) => $.tab_body.skills.add_dialog_search_placeholder)}
|
||||
aria-label={t(($) => $.tab_body.skills.add_dialog_search_placeholder)}
|
||||
className="pl-7"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="max-h-64 space-y-1 overflow-y-auto">
|
||||
{availableSkills.map((skill) => (
|
||||
{filteredSkills.map((skill) => (
|
||||
<button
|
||||
key={skill.id}
|
||||
onClick={() => handleAdd(skill.id)}
|
||||
@@ -99,9 +132,14 @@ export function SkillAddDialog({
|
||||
{t(($) => $.tab_body.skills.add_dialog_empty)}
|
||||
</p>
|
||||
)}
|
||||
{noMatch && (
|
||||
<p className="py-6 text-center text-xs text-muted-foreground">
|
||||
{t(($) => $.tab_body.skills.add_dialog_no_match)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||
<Button variant="ghost" onClick={() => handleOpenChange(false)}>
|
||||
{t(($) => $.tab_body.skills.add_dialog_cancel)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -259,7 +259,9 @@
|
||||
"remove_failed_toast": "Failed to remove skill",
|
||||
"add_dialog_title": "Add skill",
|
||||
"add_dialog_description": "Select a workspace skill to assign to this agent.",
|
||||
"add_dialog_search_placeholder": "Search skills",
|
||||
"add_dialog_empty": "All workspace skills are already assigned.",
|
||||
"add_dialog_no_match": "No skills match your search.",
|
||||
"add_dialog_cancel": "Cancel",
|
||||
"add_failed_toast": "Failed to add skill"
|
||||
},
|
||||
|
||||
@@ -255,7 +255,9 @@
|
||||
"remove_failed_toast": "移除 skill 失败",
|
||||
"add_dialog_title": "添加 skill",
|
||||
"add_dialog_description": "选择一个工作区 skill 分配给该智能体。",
|
||||
"add_dialog_search_placeholder": "搜索 skill",
|
||||
"add_dialog_empty": "全部工作区 skill 都已分配。",
|
||||
"add_dialog_no_match": "没有匹配的 skill。",
|
||||
"add_dialog_cancel": "取消",
|
||||
"add_failed_toast": "添加 skill 失败"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user