feat(skills): UI fuer AI-Skill-Definitionen — Modul 7/16 des UI-Backlogs
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
Zweites Modul via Phase-Q-Manifest-Architektur: Registrierung komplett ueber das automation-Plugin-Manifest (page_route /skills + menu_item, Sparkles-Icon) — routes/index.tsx und Sidebar.tsx unangetastet. Der Komponenten-Map-Generator wired den lazy import (39 Komponenten). Backend existierte vollstaendig im automation-Plugin (skill_routes.py: list mit is_active/category-Filtern, create, get, patch, delete; automation:read/write/delete), Frontend hatte 0% Abdeckung. - api/skills.ts: TanStack-Hooks (useSkills mit Filtern, useSkill, create/update/delete mit Cache-Invalidierung) - pages/Skills.tsx: Filter-Tabs (alle/aktiv/inaktiv), Skill-Karten (Name, Aktiv/Inaktiv-Badge, Kategorie, Beschreibung, Tool-Count), Create/Edit-Dialog (Name, Beschreibung, Instructions-Textarea, Kategorie, Tool-IDs als Komma-Liste, Aktiv-Toggle), Delete mit Confirm — Create/Edit hinter automation:write, Delete hinter automation:delete - i18n skills.* + nav.skills de/en Hinweis: Skills sind Orchestrierungs-Metadaten, KEINE Berechtigungsquelle — erlaubte Tools verweisen auf Tool-IDs, fuer die Agent und User bereits berechtigt sein muessen (Backend-Docstring). Verifikation: Vitest 10/10 (Filter-Tabs, Badges inkl. Inaktiv, Tool-Count, Create-Flow, Edit prefilled, Delete mit+ohne Confirm, separates Write/Delete-Gating) · tsc exit 0 · production build exit 0 · Backend-Regressionen (route-order, m5-miniapps, n4-scope) 28/28 · compileall sauber · Cross-Plugin-Checker 0 · Manifest-Check: page_route + menu_item korrekt.
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Skills API client — AI skill definitions (orchestration metadata).
|
||||
*
|
||||
* Backend: /api/v1/skills (list, create, get, update, delete). Skills are
|
||||
* NOT a permission source — they reference tool IDs the agent/user must
|
||||
* already be permitted to use.
|
||||
* Permissions: automation:read (list/get) / automation:write (create,
|
||||
* update) / automation:delete (delete).
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiGet, apiPost, apiPatch, apiDelete } from '@/api/client';
|
||||
|
||||
export interface SkillDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
allowed_tool_ids: string[];
|
||||
context_policy: Record<string, unknown> | null;
|
||||
category: string;
|
||||
is_active: boolean;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface SkillListResponse {
|
||||
items: SkillDefinition[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SkillCreatePayload {
|
||||
name: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
allowed_tool_ids?: string[];
|
||||
context_policy?: Record<string, unknown> | null;
|
||||
category?: string;
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export type SkillUpdatePayload = Partial<SkillCreatePayload>;
|
||||
|
||||
export interface SkillListParams {
|
||||
isActive?: boolean;
|
||||
category?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// ─── Query hooks ─────────────────────────────────────────────
|
||||
|
||||
export function useSkills(params: SkillListParams = {}) {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.isActive !== undefined) searchParams.set('is_active', String(params.isActive));
|
||||
if (params.category) searchParams.set('category', params.category);
|
||||
if (params.limit) searchParams.set('limit', String(params.limit));
|
||||
if (params.offset) searchParams.set('offset', String(params.offset));
|
||||
const qs = searchParams.toString();
|
||||
|
||||
return useQuery<SkillListResponse>({
|
||||
queryKey: ['skills', params.isActive, params.category, params.limit, params.offset],
|
||||
queryFn: () => apiGet<SkillListResponse>(`/skills${qs ? `?${qs}` : ''}`),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSkill(skillId: string | null) {
|
||||
return useQuery<SkillDefinition>({
|
||||
queryKey: ['skills', skillId],
|
||||
queryFn: () => apiGet<SkillDefinition>(`/skills/${skillId}`),
|
||||
enabled: !!skillId,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Mutation hooks ──────────────────────────────────────────
|
||||
|
||||
function useInvalidateSkills() {
|
||||
const qc = useQueryClient();
|
||||
return () => {
|
||||
qc.invalidateQueries({ queryKey: ['skills'] });
|
||||
};
|
||||
}
|
||||
|
||||
export function useCreateSkill() {
|
||||
const invalidate = useInvalidateSkills();
|
||||
return useMutation<SkillDefinition, Error, SkillCreatePayload>({
|
||||
mutationFn: (data) => apiPost<SkillDefinition>('/skills/', data),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateSkill() {
|
||||
const invalidate = useInvalidateSkills();
|
||||
return useMutation<
|
||||
SkillDefinition,
|
||||
Error,
|
||||
{ skillId: string; payload: SkillUpdatePayload }
|
||||
>({
|
||||
mutationFn: ({ skillId, payload }) =>
|
||||
apiPatch<SkillDefinition>(`/skills/${skillId}`, payload),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteSkill() {
|
||||
const invalidate = useInvalidateSkills();
|
||||
return useMutation<{ status: string }, Error, string>({
|
||||
mutationFn: (skillId) => apiDelete<{ status: string }>(`/skills/${skillId}`),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user