feat: add AI service client and implement tenant AI management dashboards

This commit is contained in:
sibarchannayak 2026-05-29 18:14:08 +05:30
parent 85ecd9dac1
commit 0a672619ea
3 changed files with 160 additions and 131 deletions

View File

@ -42,6 +42,7 @@ import { formatDate } from "@/utils/format-date";
import AuditLogs from "@/pages/tenant/AuditLogs";
import TenantSettings from "@/pages/tenant/Settings";
import TenantAIProviders from "@/pages/tenant/TenantAIProviders";
import { TenantAIDashboard } from "@/pages/tenant/TenantAIDashboard";
// import DepartmentsTable from "@/components/superadmin/DepartmentsTable";
import DesignationsTable from "@/components/superadmin/DesignationsTable";
@ -59,7 +60,8 @@ type TabType =
| "license"
| "audit-logs"
| "billing"
| "ai-providers";
| "ai-providers"
| "ai-dashboard";
const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [
{ id: "overview", label: "Overview", icon: <FileText className="w-4 h-4" /> },
@ -89,6 +91,7 @@ const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [
{ id: "modules", label: "Modules", icon: <Package className="w-4 h-4" /> },
{ id: "settings", label: "Settings", icon: <Settings className="w-4 h-4" /> },
{ id: "ai-providers", label: "AI Providers", icon: <Brain className="w-4 h-4" /> },
{ id: "ai-dashboard", label: "AI Dashboard", icon: <Brain className="w-4 h-4" /> },
{ id: "license", label: "License", icon: <FileText className="w-4 h-4" /> },
{
id: "audit-logs",
@ -381,6 +384,9 @@ const TenantDetails = (): ReactElement => {
{activeTab === "ai-providers" && id && (
<TenantAIProviders customTenantId={id} hideLayout={true} />
)}
{activeTab === "ai-dashboard" && id && (
<TenantAIDashboard customTenantId={id} hideLayout={true} />
)}
{activeTab === "license" && <LicenseTab tenant={tenant} />}
{activeTab === "audit-logs" && id && (
<AuditLogs customTenantId={id} hideLayout={true} />

View File

@ -21,7 +21,15 @@ import { formatDate } from "@/utils/format-date";
type GroupBy = "day" | "week" | "month";
export const TenantAIDashboard = (): ReactElement => {
interface TenantAIDashboardProps {
customTenantId?: string;
hideLayout?: boolean;
}
export const TenantAIDashboard = ({
customTenantId,
hideLayout = false,
}: TenantAIDashboardProps): ReactElement => {
const [groupBy, setGroupBy] = useState<GroupBy | null>(null);
const [startDate, setStartDate] = useState<string>("");
const [endDate, setEndDate] = useState<string>("");
@ -54,11 +62,14 @@ export const TenantAIDashboard = (): ReactElement => {
) => {
setIsLoading(true);
try {
const data = await aiService.getCostSummary({
group_by: group || undefined,
start_date: group && start ? `${start}T00:00:00.000Z` : undefined,
end_date: group && end ? `${end}T23:59:59.999Z` : undefined,
});
const data = await aiService.getCostSummary(
{
group_by: group || undefined,
start_date: group && start ? `${start}T00:00:00.000Z` : undefined,
end_date: group && end ? `${end}T23:59:59.999Z` : undefined,
},
customTenantId
);
setCosts(data);
} catch (err: any) {
showToast.error(
@ -73,7 +84,7 @@ export const TenantAIDashboard = (): ReactElement => {
if (!groupBy || (startDate && endDate)) {
void fetchCostSummary(groupBy, startDate, endDate);
}
}, [groupBy, startDate, endDate]);
}, [groupBy, startDate, endDate, customTenantId]);
const providerColumns: Column<any>[] = [
{
@ -170,6 +181,130 @@ export const TenantAIDashboard = (): ReactElement => {
return `${Math.round(ms)}ms`;
};
const content = (
<div className="space-y-6">
{/* Top Controls Row */}
{/* Filters */}
<div className="flex flex-col sm:flex-row items-start gap-3">
{/* Date Range */}
<div className="relative">
<input
type="text"
readOnly
value={
startDate && endDate
? `${formatDate(startDate)} - ${formatDate(endDate)}`
: "All time"
}
className="w-full sm:w-[220px] h-11 px-4 pr-10 rounded-[8px] border border-[#D1D5DB] bg-white text-[14px] font-medium text-[#334155] outline-none cursor-pointer"
/>
<Calendar className="w-4 h-4 text-[#475569] absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none" />
</div>
{/* Group By */}
<div className="w-full sm:w-[240px]">
<FilterDropdown
label="Group by"
options={[
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
]}
value={groupBy}
onChange={(val) => {
setGroupBy(val as GroupBy | null);
}}
placeholder="All"
/>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 select-none">
<GradientStatCard
icon={MessageSquare}
value={costs?.summary?.total_completions || 0}
label="Total Completions"
/>
<GradientStatCard
icon={Coins}
value={formatTokens(costs?.summary?.total_tokens || 0)}
label="Total Tokens"
/>
<GradientStatCard
icon={DollarSign}
value={`$${(costs?.summary?.total_cost || 0).toFixed(2)}`}
label="Total Cost (USD)"
/>
<GradientStatCard
icon={Timer}
value={formatLatency(costs?.summary?.avg_latency_ms || 0)}
label="Avg Latency (ms)"
/>
</div>
{/* Charts and Tables */}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* Usage by Provider Section */}
<div className="bg-white border border-slate-100 rounded-xl shadow-sm p-5 space-y-4">
<div>
<h3 className="text-base font-bold text-slate-800 select-none">
Usage by Provider
</h3>
<p className="text-xs text-slate-400 select-none">
Breakdown of requests, tokens and usage costs for each distinct
provider.
</p>
</div>
<div className="border border-slate-50 rounded-xl overflow-hidden">
<DataTable
data={costs?.by_provider || []}
columns={providerColumns}
keyExtractor={(item) =>
item.provider || Math.random().toString()
}
isLoading={isLoading}
emptyMessage="No provider metrics available for this period."
/>
</div>
</div>
{/* Cost by Model Section */}
<div className="bg-white border border-slate-100 rounded-xl shadow-sm p-5 space-y-4">
<div>
<h3 className="text-base font-bold text-slate-800 select-none">
Cost by Model
</h3>
<p className="text-xs text-slate-400 select-none">
Breakdown of requests and costs grouped at the AI model level.
</p>
</div>
<div className="border border-slate-50 rounded-xl overflow-hidden">
<DataTable
data={costs?.by_model || []}
columns={modelColumns}
keyExtractor={(item) =>
`${item.model}-${item.provider}` || Math.random().toString()
}
isLoading={isLoading}
emptyMessage="No model metrics available for this period."
/>
</div>
</div>
</div>
</div>
);
if (hideLayout) {
return content;
}
return (
<Layout
currentPage="AI Services"
@ -179,123 +314,7 @@ export const TenantAIDashboard = (): ReactElement => {
"Monitor AI completions, tokens, and billing analytics across all providers.",
}}
>
<div className="space-y-6">
{/* Top Controls Row */}
{/* Filters */}
<div className="flex flex-col sm:flex-row items-start gap-3">
{/* Date Range */}
<div className="relative">
<input
type="text"
readOnly
value={
startDate && endDate
? `${formatDate(startDate)} - ${formatDate(endDate)}`
: "All time"
}
className="w-full sm:w-[220px] h-11 px-4 pr-10 rounded-[8px] border border-[#D1D5DB] bg-white text-[14px] font-medium text-[#334155] outline-none cursor-pointer"
/>
<Calendar className="w-4 h-4 text-[#475569] absolute right-4 top-1/2 -translate-y-1/2 pointer-events-none" />
</div>
{/* Group By */}
<div className="w-full sm:w-[240px]">
<FilterDropdown
label="Group by"
options={[
{ value: "day", label: "Day" },
{ value: "week", label: "Week" },
{ value: "month", label: "Month" },
]}
value={groupBy}
onChange={(val) => {
setGroupBy(val as GroupBy | null);
}}
placeholder="All"
/>
</div>
</div>
{/* Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 select-none">
<GradientStatCard
icon={MessageSquare}
value={costs?.summary?.total_completions || 0}
label="Total Completions"
/>
<GradientStatCard
icon={Coins}
value={formatTokens(costs?.summary?.total_tokens || 0)}
label="Total Tokens"
/>
<GradientStatCard
icon={DollarSign}
value={`$${(costs?.summary?.total_cost || 0).toFixed(2)}`}
label="Total Cost (USD)"
/>
<GradientStatCard
icon={Timer}
value={formatLatency(costs?.summary?.avg_latency_ms || 0)}
label="Avg Latency (ms)"
/>
</div>
{/* Charts and Tables */}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
{/* Usage by Provider Section */}
<div className="bg-white border border-slate-100 rounded-xl shadow-sm p-5 space-y-4">
<div>
<h3 className="text-base font-bold text-slate-800 select-none">
Usage by Provider
</h3>
<p className="text-xs text-slate-400 select-none">
Breakdown of requests, tokens and usage costs for each distinct
provider.
</p>
</div>
<div className="border border-slate-50 rounded-xl overflow-hidden">
<DataTable
data={costs?.by_provider || []}
columns={providerColumns}
keyExtractor={(item) =>
item.provider || Math.random().toString()
}
isLoading={isLoading}
emptyMessage="No provider metrics available for this period."
/>
</div>
</div>
{/* Cost by Model Section */}
<div className="bg-white border border-slate-100 rounded-xl shadow-sm p-5 space-y-4">
<div>
<h3 className="text-base font-bold text-slate-800 select-none">
Cost by Model
</h3>
<p className="text-xs text-slate-400 select-none">
Breakdown of requests and costs grouped at the AI model level.
</p>
</div>
<div className="border border-slate-50 rounded-xl overflow-hidden">
<DataTable
data={costs?.by_model || []}
columns={modelColumns}
keyExtractor={(item) =>
`${item.model}-${item.provider}` || Math.random().toString()
}
isLoading={isLoading}
emptyMessage="No model metrics available for this period."
/>
</div>
</div>
</div>
</div>
{content}
</Layout>
);
};

View File

@ -84,12 +84,16 @@ class AIService {
return unwrap<AICompletion>(response);
}
async getCostSummary(params: {
group_by?: "day" | "week" | "month";
start_date?: string;
end_date?: string;
} = {}): Promise<AICostSummary> {
const response = await apiClient.get("/ai/costs", { params });
async getCostSummary(
params: {
group_by?: "day" | "week" | "month";
start_date?: string;
end_date?: string;
} = {},
tenantId?: string
): Promise<AICostSummary> {
const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
const response = await apiClient.get("/ai/costs", { params, headers });
return unwrap<AICostSummary>(response);
}