feat: add AI service client and implement tenant AI management dashboards
This commit is contained in:
parent
85ecd9dac1
commit
0a672619ea
@ -42,6 +42,7 @@ import { formatDate } from "@/utils/format-date";
|
|||||||
import AuditLogs from "@/pages/tenant/AuditLogs";
|
import AuditLogs from "@/pages/tenant/AuditLogs";
|
||||||
import TenantSettings from "@/pages/tenant/Settings";
|
import TenantSettings from "@/pages/tenant/Settings";
|
||||||
import TenantAIProviders from "@/pages/tenant/TenantAIProviders";
|
import TenantAIProviders from "@/pages/tenant/TenantAIProviders";
|
||||||
|
import { TenantAIDashboard } from "@/pages/tenant/TenantAIDashboard";
|
||||||
// import DepartmentsTable from "@/components/superadmin/DepartmentsTable";
|
// import DepartmentsTable from "@/components/superadmin/DepartmentsTable";
|
||||||
import DesignationsTable from "@/components/superadmin/DesignationsTable";
|
import DesignationsTable from "@/components/superadmin/DesignationsTable";
|
||||||
|
|
||||||
@ -59,7 +60,8 @@ type TabType =
|
|||||||
| "license"
|
| "license"
|
||||||
| "audit-logs"
|
| "audit-logs"
|
||||||
| "billing"
|
| "billing"
|
||||||
| "ai-providers";
|
| "ai-providers"
|
||||||
|
| "ai-dashboard";
|
||||||
|
|
||||||
const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [
|
const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [
|
||||||
{ id: "overview", label: "Overview", icon: <FileText className="w-4 h-4" /> },
|
{ 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: "modules", label: "Modules", icon: <Package className="w-4 h-4" /> },
|
||||||
{ id: "settings", label: "Settings", icon: <Settings 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-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: "license", label: "License", icon: <FileText className="w-4 h-4" /> },
|
||||||
{
|
{
|
||||||
id: "audit-logs",
|
id: "audit-logs",
|
||||||
@ -381,6 +384,9 @@ const TenantDetails = (): ReactElement => {
|
|||||||
{activeTab === "ai-providers" && id && (
|
{activeTab === "ai-providers" && id && (
|
||||||
<TenantAIProviders customTenantId={id} hideLayout={true} />
|
<TenantAIProviders customTenantId={id} hideLayout={true} />
|
||||||
)}
|
)}
|
||||||
|
{activeTab === "ai-dashboard" && id && (
|
||||||
|
<TenantAIDashboard customTenantId={id} hideLayout={true} />
|
||||||
|
)}
|
||||||
{activeTab === "license" && <LicenseTab tenant={tenant} />}
|
{activeTab === "license" && <LicenseTab tenant={tenant} />}
|
||||||
{activeTab === "audit-logs" && id && (
|
{activeTab === "audit-logs" && id && (
|
||||||
<AuditLogs customTenantId={id} hideLayout={true} />
|
<AuditLogs customTenantId={id} hideLayout={true} />
|
||||||
|
|||||||
@ -21,7 +21,15 @@ import { formatDate } from "@/utils/format-date";
|
|||||||
|
|
||||||
type GroupBy = "day" | "week" | "month";
|
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 [groupBy, setGroupBy] = useState<GroupBy | null>(null);
|
||||||
const [startDate, setStartDate] = useState<string>("");
|
const [startDate, setStartDate] = useState<string>("");
|
||||||
const [endDate, setEndDate] = useState<string>("");
|
const [endDate, setEndDate] = useState<string>("");
|
||||||
@ -54,11 +62,14 @@ export const TenantAIDashboard = (): ReactElement => {
|
|||||||
) => {
|
) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await aiService.getCostSummary({
|
const data = await aiService.getCostSummary(
|
||||||
group_by: group || undefined,
|
{
|
||||||
start_date: group && start ? `${start}T00:00:00.000Z` : undefined,
|
group_by: group || undefined,
|
||||||
end_date: group && end ? `${end}T23:59:59.999Z` : undefined,
|
start_date: group && start ? `${start}T00:00:00.000Z` : undefined,
|
||||||
});
|
end_date: group && end ? `${end}T23:59:59.999Z` : undefined,
|
||||||
|
},
|
||||||
|
customTenantId
|
||||||
|
);
|
||||||
setCosts(data);
|
setCosts(data);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
showToast.error(
|
showToast.error(
|
||||||
@ -73,7 +84,7 @@ export const TenantAIDashboard = (): ReactElement => {
|
|||||||
if (!groupBy || (startDate && endDate)) {
|
if (!groupBy || (startDate && endDate)) {
|
||||||
void fetchCostSummary(groupBy, startDate, endDate);
|
void fetchCostSummary(groupBy, startDate, endDate);
|
||||||
}
|
}
|
||||||
}, [groupBy, startDate, endDate]);
|
}, [groupBy, startDate, endDate, customTenantId]);
|
||||||
|
|
||||||
const providerColumns: Column<any>[] = [
|
const providerColumns: Column<any>[] = [
|
||||||
{
|
{
|
||||||
@ -170,6 +181,130 @@ export const TenantAIDashboard = (): ReactElement => {
|
|||||||
return `${Math.round(ms)}ms`;
|
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 (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
currentPage="AI Services"
|
currentPage="AI Services"
|
||||||
@ -179,123 +314,7 @@ export const TenantAIDashboard = (): ReactElement => {
|
|||||||
"Monitor AI completions, tokens, and billing analytics across all providers.",
|
"Monitor AI completions, tokens, and billing analytics across all providers.",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
{content}
|
||||||
{/* 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>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -84,12 +84,16 @@ class AIService {
|
|||||||
return unwrap<AICompletion>(response);
|
return unwrap<AICompletion>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCostSummary(params: {
|
async getCostSummary(
|
||||||
group_by?: "day" | "week" | "month";
|
params: {
|
||||||
start_date?: string;
|
group_by?: "day" | "week" | "month";
|
||||||
end_date?: string;
|
start_date?: string;
|
||||||
} = {}): Promise<AICostSummary> {
|
end_date?: string;
|
||||||
const response = await apiClient.get("/ai/costs", { params });
|
} = {},
|
||||||
|
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);
|
return unwrap<AICostSummary>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user