feat: build local skills registry

This commit is contained in:
2026-06-05 09:30:00 +02:00
parent f0a948aec9
commit 370aeec2a0
18 changed files with 1334 additions and 16 deletions

View File

@@ -0,0 +1,184 @@
"use client";
import { useMemo } from "react";
import { useQuery } from "convex/react";
import type { Id } from "@/convex/_generated/dataModel";
import { api } from "@/convex/_generated/api";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Globe } from "lucide-react";
type UsedSkill = {
name: string;
purpose?: string;
category?: string;
source?: string;
version?: string;
};
type LeadContext = {
_id: Id<"leads">;
companyName?: string;
websiteDomain?: string;
websiteUrl?: string;
city?: string;
niche?: string;
};
type SkillAwareAudit = {
_id: Id<"audits">;
slug: string;
checkedDomain: string;
status: "draft" | "approved" | "published" | "deactivated";
checkedPages: string[];
createdAt?: number;
updatedAt?: number;
usedSkills?: UsedSkill[];
internalSummary?: string | null;
};
type AuditDetailResult = {
audit: SkillAwareAudit;
lead: LeadContext | null;
} | null;
const statusText: Record<string, string> = {
draft: "Entwurf",
approved: "Freigegeben",
published: "Veröffentlicht",
deactivated: "Deaktiviert",
};
function getStatusLabel(status: SkillAwareAudit["status"]) {
return statusText[status] ?? "Unbekannt";
}
function leadSummary(lead: LeadContext | null | undefined) {
if (!lead) {
return "Kein Lead-Kontext gespeichert";
}
const detail = [lead.city, lead.niche].filter(Boolean).join(" • ");
let leadDomain = lead.websiteDomain ?? "—";
if (!leadDomain && lead.websiteUrl) {
try {
leadDomain = new URL(lead.websiteUrl).hostname;
} catch {
leadDomain = lead.websiteUrl;
}
}
return (
<>
<p className="font-medium">{lead.companyName ?? "Lead ohne Name"}</p>
<p className="text-sm text-muted-foreground">{detail || "Kein Kontext textlich"}</p>
<p className="mt-1 inline-flex items-center gap-1 text-sm text-muted-foreground">
<Globe className="size-3.5" />
{leadDomain}
</p>
</>
);
}
export function AuditDetail({ id }: { id: string | Id<"audits"> }) {
const result = useQuery(api.audits.getDetail, {
id: id as Id<"audits">,
}) as AuditDetailResult | undefined;
const audit = result?.audit;
const lead = result?.lead;
const usedSkills = useMemo(() => audit?.usedSkills ?? [], [audit]);
if (result === null) {
return (
<Card>
<CardHeader>
<CardTitle>Audit nicht gefunden</CardTitle>
<CardDescription>
Der gewünschte Audit-Datensatz konnte nicht geladen werden.
</CardDescription>
</CardHeader>
</Card>
);
}
if (audit === undefined) {
return (
<Card>
<CardHeader>
<CardTitle>Audit wird geladen...</CardTitle>
</CardHeader>
</Card>
);
}
return (
<div className="grid gap-4">
<Card>
<CardHeader>
<CardDescription>Audit-Detail</CardDescription>
<CardTitle className="text-xl">#{audit.slug}</CardTitle>
<p className="inline-flex max-w-full items-center gap-1 truncate text-sm text-muted-foreground">
<Globe className="size-3.5" />
{audit.checkedDomain}
</p>
</CardHeader>
<CardContent className="grid gap-3 sm:grid-cols-2">
<div>
<p className="text-sm text-muted-foreground">Status</p>
<p>
<Badge variant="secondary">{getStatusLabel(audit.status)}</Badge>
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Seitenanzahl</p>
<p>{audit.checkedPages.length}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Lead-Kontext</p>
<div className="text-sm">{leadSummary(lead)}</div>
</div>
{audit.internalSummary ? (
<div>
<p className="text-sm text-muted-foreground">Interne Notiz</p>
<p className="text-sm text-muted-foreground">{audit.internalSummary}</p>
</div>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Verwendete Skills</CardTitle>
<CardDescription>Skills, die an diesem Audit beteiligt wurden.</CardDescription>
</CardHeader>
<CardContent>
{usedSkills.length === 0 ? (
<p className="text-sm text-muted-foreground">Keine Skills gespeichert</p>
) : (
<ul className="grid gap-2">
{usedSkills.map((skill, index) => (
<li
className="rounded-md border p-2 text-sm"
key={`${skill.name}-${index}`}
>
<p className="font-medium">{skill.name}</p>
<p className="text-sm text-muted-foreground">
{skill.purpose ?? "Keine Zweckbeschreibung"}
</p>
<p className="mt-1 inline-flex flex-wrap items-center gap-1">
{skill.category ? <Badge variant="outline">{skill.category}</Badge> : null}
{skill.version ? <Badge variant="outline">{skill.version}</Badge> : null}
{skill.source ? <span className="text-xs text-muted-foreground">{skill.source}</span> : null}
</p>
</li>
))}
</ul>
)}
</CardContent>
</Card>
</div>
);
}