Spaces:
Paused
Paused
File size: 4,240 Bytes
0b9dc2e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | import { FileX, PlusCircle, Search, SearchX, Trash } from 'lucide-react';
import { useState } from 'react';
import type { Skill } from '@/api';
import { AddSkillDialog } from '@/components/dialog/AddSkillDialog.tsx';
import { DeleteDialog } from '@/components/dialog/DeleteDialog.tsx';
import { PanelEmpty } from '@/components/panel/PanelEmpty';
import { Button } from '@/components/ui/button';
import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group';
import { Item, ItemActions, ItemContent, ItemDescription, ItemTitle } from '@/components/ui/item';
import { useTranslation } from '@/i18n/useI18n.ts';
interface SkillPanelProps {
/** The skills equipped in the workspace. */
skills: Skill[];
/** Whether the skill list is still loading. */
loading?: boolean;
/**
* Add a skill to the workspace.
*
* @param skillPath - Path of the skill to add.
*/
onAdd: (skillPath: string) => Promise<void>;
/**
* Remove a skill by name.
*
* @param name - The skill name to remove.
*/
onRemove: (name: string) => Promise<void>;
}
/**
* Pure content body for the Skill dock panel: a search box, the list
* of equipped skills, and an "Add Skill" action. Holds only local UI
* state (search text, delete confirmation target); all data arrives
* via props so it owns no data fetching.
*
* Renders without its own header/border — the surrounding `Panel`
* chrome (from `PanelDock`) provides those.
*
* @param skills - The skills to list.
* @param loading - Whether the list is loading.
* @param onAdd - Add-skill callback.
* @param onRemove - Remove-skill callback.
* @returns The skill panel body.
*/
export function SkillPanel({ skills, loading = false, onAdd, onRemove }: SkillPanelProps) {
const { t } = useTranslation();
const [search, setSearch] = useState('');
const [deleteOpen, setDeleteOpen] = useState(false);
const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
const filtered = search
? skills.filter((s) => s.name.toLowerCase().includes(search.toLowerCase()))
: skills;
return (
<div className="flex flex-col flex-1 min-h-0 gap-y-2">
<span className="text-muted-foreground text-sm">{t('panel.skill.description')}</span>
<InputGroup>
<InputGroupInput
placeholder={t('panel.skill.searchPlaceholder')}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<InputGroupAddon align="inline-end">
<Search />
</InputGroupAddon>
</InputGroup>
{loading ? (
<div className="flex flex-1 items-center justify-center">
<p className="text-muted-foreground text-sm">{t('panel.loading')}</p>
</div>
) : filtered.length === 0 ? (
<PanelEmpty
icon={search ? SearchX : FileX}
title={search ? t('panel.search.emptyTitle') : t('panel.skill.emptyTitle')}
description={
search
? t('panel.search.emptyDescription', { query: search })
: t('panel.skill.emptyDescription')
}
/>
) : (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto gap-y-2">
{filtered.map((skill) => (
<Item key={skill.name} variant="outline">
<ItemContent>
<ItemTitle>{skill.name}</ItemTitle>
<ItemDescription>{skill.description}</ItemDescription>
</ItemContent>
<ItemActions>
<Button
variant="outline"
size="icon-sm"
onClick={() => {
setDeleteTarget(skill.name);
setDeleteOpen(true);
}}
>
<Trash />
</Button>
</ItemActions>
</Item>
))}
</div>
)}
<AddSkillDialog onAdd={onAdd}>
<Button variant="default">
<PlusCircle />
{t('panel.skill.add')}
</Button>
</AddSkillDialog>
<DeleteDialog
open={deleteOpen}
onOpenChange={setDeleteOpen}
title={t('common.deleteTitle', {
entity: t('dialog-mcp-delete.skillEntity'),
name: deleteTarget ?? '',
})}
description={t('dialog-mcp-delete.skillDescription')}
onConfirm={async () => {
if (deleteTarget) await onRemove(deleteTarget);
}}
/>
</div>
);
}
|