Vitron-Front/app/routes/admin.ai-health.tsx
Arda Samadi b3189db773 admin: Slice 3c AI-health dashboard UI
- useAiHealth hook + AiHealthOverview type (60s refetch).
- /admin/ai-health: per-pipeline sections (try-on, metadata, instagram,
  apgs) with status pills, throughput KPIs, and recent-failure lists.
- sidebar "سلامت AI" nav link.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 14:57:42 +03:30

183 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Loader2, Sparkles, FileText, Instagram, Bot } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { useAiHealth, type AiHealthOverview } from "~/requestHandler/use-admin-hooks";
import { KpiCard } from "~/components/admin/KpiCard";
import { AdminPageTitle, faNum, faDate } from "~/components/admin/DataTable";
const STATUS_LABEL: Record<string, string> = {
pending: "در انتظار",
processing: "در حال پردازش",
success: "موفق",
ready: "آماده",
failed: "ناموفق",
};
const STATUS_TONE: Record<string, string> = {
pending: "bg-yellow-500/10 text-yellow-600",
processing: "bg-VITROWN_BLUE/10 text-VITROWN_BLUE",
success: "bg-GREEN/10 text-GREEN",
ready: "bg-GREEN/10 text-GREEN",
failed: "bg-RED/10 text-RED",
};
function StatusPills({ byStatus }: { byStatus: Record<string, number> }) {
const entries = Object.entries(byStatus);
if (entries.length === 0) return <p className="text-GRAY text-[12px]">دادهای نیست</p>;
return (
<div className="flex flex-wrap gap-2">
{entries.map(([k, v]) => (
<span
key={k}
className={`text-[12px] font-semibold px-2.5 py-1 rounded-full ${
STATUS_TONE[k] || "bg-WHITE3 text-BLACK2"
}`}
>
{STATUS_LABEL[k] || k}: {faNum(v)}
</span>
))}
</div>
);
}
function Failures({
rows,
dateKey,
}: {
rows: AiHealthOverview["tryon"]["recentFailures"];
dateKey: "createdAt" | "updatedAt";
}) {
if (rows.length === 0)
return <p className="text-GREEN text-[12px] mt-2">خطای اخیری ثبت نشده </p>;
return (
<div className="mt-3 flex flex-col gap-1.5">
<p className="text-[12px] font-semibold text-RED">خطاهای اخیر</p>
{rows.map((r) => (
<div key={r.id} className="rounded-xl border border-RED/20 bg-RED/5 p-2.5">
<div className="flex items-center justify-between gap-2">
<span className="text-GRAY text-[10.5px] tabular-nums" dir="ltr">
{r.id.slice(0, 8)}
</span>
<span className="text-GRAY text-[10.5px] whitespace-nowrap">
{faDate(r[dateKey])}
</span>
</div>
<p className="text-[11.5px] text-BLACK2 mt-1 break-words" dir="ltr">
{r.error || "—"}
</p>
</div>
))}
</div>
);
}
function Section({
title,
icon: Icon,
children,
}: {
title: string;
icon: LucideIcon;
children: React.ReactNode;
}) {
return (
<section className="rounded-2xl border border-WHITE3 bg-WHITE p-4 sm:p-5">
<div className="flex items-center gap-2 mb-4">
<div className="h-9 w-9 rounded-xl bg-WHITE2 grid place-items-center">
<Icon size={18} />
</div>
<h2 className="font-extrabold text-[15px]">{title}</h2>
</div>
{children}
</section>
);
}
export default function AdminAiHealth() {
const { data, isLoading, isError } = useAiHealth();
if (isLoading)
return (
<div className="flex min-h-[40vh] items-center justify-center">
<Loader2 className="h-7 w-7 animate-spin text-GRAY" />
</div>
);
if (isError || !data)
return <p className="text-RED text-[14px]">خطا در بارگذاری وضعیت سرویسهای هوش مصنوعی</p>;
return (
<div>
<AdminPageTitle>سلامت سرویسهای هوش مصنوعی</AdminPageTitle>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Section title="پرو مجازی" icon={Sparkles}>
<div className="grid grid-cols-3 gap-3">
<KpiCard label="کل" value={faNum(data.tryon.total)} />
<KpiCard label="۲۴ ساعت" value={faNum(data.tryon.last24h)} />
<KpiCard label="۷ روز" value={faNum(data.tryon.last7d)} />
</div>
<div className="mt-3">
<StatusPills byStatus={data.tryon.byStatus} />
</div>
<Failures rows={data.tryon.recentFailures} dateKey="createdAt" />
</Section>
<Section title="متادیتای محصولات" icon={FileText}>
<div className="grid grid-cols-3 gap-3">
<KpiCard label="کل" value={faNum(data.metadata.total)} />
<KpiCard label="۲۴ ساعت" value={faNum(data.metadata.last24h)} />
<KpiCard label="۷ روز" value={faNum(data.metadata.last7d)} />
</div>
<div className="mt-3">
<StatusPills byStatus={data.metadata.byStatus} />
</div>
<Failures rows={data.metadata.recentFailures} dateKey="updatedAt" />
</Section>
<Section title="ایمپورت اینستاگرام" icon={Instagram}>
<div className="grid grid-cols-3 gap-3">
<KpiCard
label="اکانت‌ها"
value={faNum(data.instagram.accountsTotal)}
sub={`${faNum(data.instagram.accountsActive)} فعال`}
/>
<KpiCard
label="در صف AI"
value={faNum(data.instagram.postsPendingAi)}
accent={data.instagram.postsPendingAi > 0 ? "red" : "default"}
/>
<KpiCard label="تبدیل‌شده" value={faNum(data.instagram.postsConverted)} accent="green" />
</div>
<p className="text-[12px] text-GRAY mt-3">
ایمپورت ۷ روز اخیر: <b className="tabular-nums text-BLACK2">{faNum(data.instagram.importedLast7d)}</b>
</p>
{data.instagram.recentImports.length > 0 && (
<div className="mt-2 flex flex-col gap-1">
{data.instagram.recentImports.map((im, i) => (
<div key={i} className="flex items-center justify-between text-[12px] border-b border-WHITE3 py-1.5 last:border-0">
<span className="tabular-nums" dir="ltr">@{im.account}</span>
<span className="text-GRAY">
{faNum(im.importedCount)} {faDate(im.date)}
</span>
</div>
))}
</div>
)}
</Section>
<Section title="تولید محصول با هوش مصنوعی" icon={Bot}>
<div className="grid grid-cols-3 gap-3">
<KpiCard label="کل" value={faNum(data.apgs.total)} />
<KpiCard label="موفق" value={faNum(data.apgs.success)} accent="green" />
<KpiCard
label="ناموفق"
value={faNum(data.apgs.failed)}
accent={data.apgs.failed > 0 ? "red" : "default"}
/>
</div>
<Failures rows={data.apgs.recentFailures} dateKey="createdAt" />
</Section>
</div>
</div>
);
}