Phase 3: Skills & Context — skill system, personal context, context layering
Backend: - Skill model + migration (with FK on chats.skill_id) - Personal + general skill CRUD services with access isolation - Admin skill CRUD endpoints (POST/GET/PATCH/DELETE /admin/skills) - User skill CRUD endpoints (POST/GET/PATCH/DELETE /skills/) - Personal context GET/PUT at /users/me/context - Extended context assembly: primary + personal context + skill prompt - Chat creation/update now accepts skill_id with validation Frontend: - Skill selector dropdown in chat header (grouped: general + personal) - Reusable skill editor form component - Admin skills management page (/admin/skills) - Personal skills page (/skills) - Personal context editor page (/profile/context) - Updated sidebar: Skills, My Context nav items + admin skills link - English + Russian translations for all skill/context UI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
114
frontend/src/pages/admin/skills.tsx
Normal file
114
frontend/src/pages/admin/skills.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import {
|
||||
getGeneralSkills,
|
||||
createGeneralSkill,
|
||||
updateGeneralSkill,
|
||||
deleteGeneralSkill,
|
||||
type Skill,
|
||||
} from "@/api/skills";
|
||||
import { SkillEditor } from "@/components/admin/skill-editor";
|
||||
import { Plus, Pencil, Trash2, Sparkles } from "lucide-react";
|
||||
|
||||
export function AdminSkillsPage() {
|
||||
const { t } = useTranslation();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const queryClient = useQueryClient();
|
||||
const [editing, setEditing] = useState<Skill | null>(null);
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
if (user?.role !== "admin") return <Navigate to="/" replace />;
|
||||
|
||||
const { data: skills = [] } = useQuery({
|
||||
queryKey: ["admin-skills"],
|
||||
queryFn: getGeneralSkills,
|
||||
});
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: createGeneralSkill,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin-skills"] });
|
||||
setCreating(false);
|
||||
},
|
||||
});
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ id, ...data }: { id: string } & Record<string, string>) =>
|
||||
updateGeneralSkill(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["admin-skills"] });
|
||||
setEditing(null);
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: deleteGeneralSkill,
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["admin-skills"] }),
|
||||
});
|
||||
|
||||
if (creating || editing) {
|
||||
return (
|
||||
<div className="max-w-2xl space-y-4">
|
||||
<h1 className="text-2xl font-semibold">
|
||||
{creating ? t("skills.create_general") : t("skills.edit_general")}
|
||||
</h1>
|
||||
<SkillEditor
|
||||
skill={editing}
|
||||
onSave={(data) =>
|
||||
editing
|
||||
? updateMut.mutate({ id: editing.id, ...data })
|
||||
: createMut.mutate(data)
|
||||
}
|
||||
onCancel={() => { setCreating(false); setEditing(null); }}
|
||||
loading={createMut.isPending || updateMut.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold">{t("skills.general_skills")}</h1>
|
||||
<button
|
||||
onClick={() => setCreating(true)}
|
||||
className="inline-flex h-9 items-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4" /> {t("common.create")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{skills.length === 0 && (
|
||||
<p className="text-muted-foreground py-8 text-center">{t("skills.no_skills")}</p>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{skills.map((skill) => (
|
||||
<div key={skill.id} className="flex items-center gap-3 rounded-lg border bg-card p-4">
|
||||
<Sparkles className="h-5 w-5 text-primary shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium">{skill.name}</p>
|
||||
{skill.description && (
|
||||
<p className="text-sm text-muted-foreground truncate">{skill.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<button onClick={() => setEditing(skill)} className="rounded p-2 hover:bg-accent transition-colors">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteMut.mutate(skill.id)}
|
||||
className="rounded p-2 hover:bg-destructive/10 hover:text-destructive transition-colors"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user