From 1d207d2dcbdb62424270628f24ed00d75584f1d8 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Mon, 4 May 2026 19:01:08 +0530 Subject: [PATCH] feat: implement AI usage dashboard and provider management features for tenant administration --- src/components/layout/Sidebar.tsx | 18 +- src/components/tenant/ViewAIProviderModal.tsx | 140 ++ src/pages/tenant/AIGateway.tsx | 1808 ++++++++--------- src/pages/tenant/TenantAIDashboard.tsx | 342 ++++ src/pages/tenant/TenantAIProviderCreate.tsx | 377 ++++ src/pages/tenant/TenantAIProviders.tsx | 307 +++ src/routes/tenant-admin-routes.tsx | 29 +- src/services/ai-service.ts | 5 + 8 files changed, 2113 insertions(+), 913 deletions(-) create mode 100644 src/components/tenant/ViewAIProviderModal.tsx create mode 100644 src/pages/tenant/TenantAIDashboard.tsx create mode 100644 src/pages/tenant/TenantAIProviderCreate.tsx create mode 100644 src/pages/tenant/TenantAIProviders.tsx diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index d525177..d73d086 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -197,15 +197,25 @@ const tenantAdminPlatformServiceMenu: MenuItem[] = [ requiredPermission: { resource: "ai" }, }, { - label: "Tenant Config", - path: "/tenant/ai/config", + label: "Tenant AI Providers", + path: "/tenant/ai/providers", requiredPermission: { resource: "ai" }, }, { - label: "Knowledge (RAG)", - path: "/tenant/ai/knowledge", + label: "AI Usage & Cost Dashboard", + path: "/tenant/ai/dashboard", requiredPermission: { resource: "ai" }, }, + // { + // label: "Tenant Config", + // path: "/tenant/ai/config", + // requiredPermission: { resource: "ai" }, + // }, + // { + // label: "Knowledge (RAG)", + // path: "/tenant/ai/knowledge", + // requiredPermission: { resource: "ai" }, + // }, ], requiredPermission: { resource: "ai" }, }, diff --git a/src/components/tenant/ViewAIProviderModal.tsx b/src/components/tenant/ViewAIProviderModal.tsx new file mode 100644 index 0000000..644cbaa --- /dev/null +++ b/src/components/tenant/ViewAIProviderModal.tsx @@ -0,0 +1,140 @@ +import type { ReactElement } from "react"; +import { Modal } from "@/components/shared"; +import type { TenantAIConfig } from "@/types/ai"; + +interface ViewAIProviderModalProps { + isOpen: boolean; + onClose: () => void; + config: TenantAIConfig | null; +} + +export const ViewAIProviderModal = ({ + isOpen, + onClose, + config, +}: ViewAIProviderModalProps): ReactElement => { + if (!config) return <>; + + const parseArray = (val: any): string[] => { + if (!val) return []; + if (Array.isArray(val)) return val; + if (typeof val === "string") { + return val.split(",").map(s => s.trim()).filter(Boolean); + } + return []; + }; + + return ( + +
+
+ + Provider + + + {config.provider} + +
+ +
+ + Display Name + + + {config.display_name || "—"} + +
+ +
+ + Config Type + + + {config.config_type || "direct"} + +
+ +
+ + Default Model + + + {config.default_model || "—"} + +
+ +
+ + Default Embedding Model + + + {(config as any).default_embedding_model || "—"} + +
+ + {config.endpoint && ( +
+ + Endpoint URL + + + {config.endpoint} + +
+ )} + +
+ + Custom Models + + {parseArray(config.custom_models).length > 0 ? ( +
+ {parseArray(config.custom_models).map((m: any, idx: any) => ( + + {m} + + ))} +
+ ) : ( + + )} +
+ +
+ + Custom Embedding Models + + {parseArray((config as any).custom_embedding_models).length > 0 ? ( +
+ {parseArray((config as any).custom_embedding_models).map((m: any, idx: any) => ( + + {m} + + ))} +
+ ) : ( + + )} +
+
+ + {/*
+ + Close + +
*/} +
+ ); +}; diff --git a/src/pages/tenant/AIGateway.tsx b/src/pages/tenant/AIGateway.tsx index 10e9195..b314e30 100644 --- a/src/pages/tenant/AIGateway.tsx +++ b/src/pages/tenant/AIGateway.tsx @@ -1,952 +1,952 @@ -import { useEffect, useMemo, useState, type ReactElement } from "react"; -import { Link, useLocation } from "react-router-dom"; -import { Brain, Layers, PlayCircle, Settings2 } from "lucide-react"; -import { Layout } from "@/components/layout/Layout"; -import { - DataTable, - type Column, - FormField, - FormSelect, - FormTextArea, - PrimaryButton, - SecondaryButton, - StatusBadge, -} from "@/components/shared"; -import { aiService } from "@/services/ai-service"; -import type { - AICompletion, - AICostSummary, - AIProviderInfo, - AIPrompt, - KnowledgeCollection, - TenantAIConfig, -} from "@/types/ai"; -import { showToast } from "@/utils/toast"; -import { cn } from "@/lib/utils"; +// import { useEffect, useMemo, useState, type ReactElement } from "react"; +// import { Link, useLocation } from "react-router-dom"; +// import { Brain, Layers, PlayCircle, Settings2 } from "lucide-react"; +// import { Layout } from "@/components/layout/Layout"; +// import { +// DataTable, +// type Column, +// FormField, +// FormSelect, +// FormTextArea, +// PrimaryButton, +// SecondaryButton, +// StatusBadge, +// } from "@/components/shared"; +// import { aiService } from "@/services/ai-service"; +// import type { +// AICompletion, +// AICostSummary, +// AIProviderInfo, +// AIPrompt, +// KnowledgeCollection, +// TenantAIConfig, +// } from "@/types/ai"; +// import { showToast } from "@/utils/toast"; +// import { cn } from "@/lib/utils"; -type TabKey = "gateway" | "config" | "prompts" | "knowledge"; +// type TabKey = "gateway" | "config" | "prompts" | "knowledge"; -const getTabFromPath = (pathname: string): TabKey => { - if (pathname.endsWith("/ai/config")) return "config"; - if (pathname.endsWith("/ai/prompts")) return "prompts"; - if (pathname.endsWith("/ai/knowledge")) return "knowledge"; - return "gateway"; -}; +// const getTabFromPath = (pathname: string): TabKey => { +// if (pathname.endsWith("/ai/config")) return "config"; +// if (pathname.endsWith("/ai/prompts")) return "prompts"; +// if (pathname.endsWith("/ai/knowledge")) return "knowledge"; +// return "gateway"; +// }; -const tabPath: Record = { - gateway: "/tenant/ai", - config: "/tenant/ai/config", - prompts: "/tenant/ai/prompts", - knowledge: "/tenant/ai/knowledge", -}; +// const tabPath: Record = { +// gateway: "/tenant/ai", +// config: "/tenant/ai/config", +// prompts: "/tenant/ai/prompts", +// knowledge: "/tenant/ai/knowledge", +// }; -const tabMeta: Record = { - gateway: { - label: "Completion Playground", - icon: PlayCircle, - subtitle: "Run completions, monitor health, and review costs.", - }, - config: { - label: "Tenant AI Provider Management", - icon: Settings2, - subtitle: "Configure provider keys/endpoints and test connectivity.", - }, - prompts: { - label: "Prompt Management & Testing", - icon: Layers, - subtitle: "Create prompts, activate them, and test outputs safely.", - }, - knowledge: { - label: "RAG Knowledge Workspace", - icon: Brain, - subtitle: "Create collections, ingest documents, and run context search.", - }, -}; +// const tabMeta: Record = { +// gateway: { +// label: "Completion Playground", +// icon: PlayCircle, +// subtitle: "Run completions, monitor health, and review costs.", +// }, +// config: { +// label: "Tenant AI Provider Management", +// icon: Settings2, +// subtitle: "Configure provider keys/endpoints and test connectivity.", +// }, +// prompts: { +// label: "Prompt Management & Testing", +// icon: Layers, +// subtitle: "Create prompts, activate them, and test outputs safely.", +// }, +// knowledge: { +// label: "RAG Knowledge Workspace", +// icon: Brain, +// subtitle: "Create collections, ingest documents, and run context search.", +// }, +// }; -const Card = ({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) => ( -
-
-

{title}

- {description &&

{description}

} -
- {children} -
-); +// const Card = ({ title, description, children }: { title: string; description?: string; children: React.ReactNode }) => ( +//
+//
+//

{title}

+// {description &&

{description}

} +//
+// {children} +//
+// ); -const StatTile = ({ label, value, hint }: { label: string; value: string; hint?: string }) => ( -
-

{label}

-

{value}

- {hint &&

{hint}

} -
-); +// const StatTile = ({ label, value, hint }: { label: string; value: string; hint?: string }) => ( +//
+//

{label}

+//

{value}

+// {hint &&

{hint}

} +//
+// ); -const AIGateway = (): ReactElement => { - const location = useLocation(); - const activeTab = getTabFromPath(location.pathname); - const ActiveIcon = tabMeta[activeTab].icon; +// const AIGateway = (): ReactElement => { +// const location = useLocation(); +// const activeTab = getTabFromPath(location.pathname); +// const ActiveIcon = tabMeta[activeTab].icon; - const [isLoading, setIsLoading] = useState(false); - const [providers, setProviders] = useState([]); - const [models, setModels] = useState>([]); - const [completions, setCompletions] = useState([]); - const [gatewayHealthy, setGatewayHealthy] = useState(true); - const [configs, setConfigs] = useState([]); - const [prompts, setPrompts] = useState([]); - const [collections, setCollections] = useState([]); - const [costSummary, setCostSummary] = useState(null); +// const [isLoading, setIsLoading] = useState(false); +// const [providers, setProviders] = useState([]); +// const [models, setModels] = useState>([]); +// const [completions, setCompletions] = useState([]); +// const [gatewayHealthy, setGatewayHealthy] = useState(true); +// const [configs, setConfigs] = useState([]); +// const [prompts, setPrompts] = useState([]); +// const [collections, setCollections] = useState([]); +// const [costSummary, setCostSummary] = useState(null); - const [completionForm, setCompletionForm] = useState({ - provider: "", - model: "", - message: "Define FDA 21 CFR Part 11 in one paragraph.", - temperature: "0.3", - max_tokens: "250", - }); - const [playgroundResult, setPlaygroundResult] = useState(""); - const [playgroundMeta, setPlaygroundMeta] = useState<{ provider?: string; model?: string; latency?: number }>({}); - const [creatingCompletion, setCreatingCompletion] = useState(false); - const [costFilter, setCostFilter] = useState<{ group_by: "day" | "week" | "month" }>({ group_by: "day" }); +// const [completionForm, setCompletionForm] = useState({ +// provider: "", +// model: "", +// message: "Define FDA 21 CFR Part 11 in one paragraph.", +// temperature: "0.3", +// max_tokens: "250", +// }); +// const [playgroundResult, setPlaygroundResult] = useState(""); +// const [playgroundMeta, setPlaygroundMeta] = useState<{ provider?: string; model?: string; latency?: number }>({}); +// const [creatingCompletion, setCreatingCompletion] = useState(false); +// const [costFilter, setCostFilter] = useState<{ group_by: "day" | "week" | "month" }>({ group_by: "day" }); - const [configForm, setConfigForm] = useState({ - provider: "", - config_type: "direct", - api_key: "", - endpoint: "", - default_model: "", - custom_models: "", - }); - const [savingConfig, setSavingConfig] = useState(false); +// const [configForm, setConfigForm] = useState({ +// provider: "", +// config_type: "direct", +// api_key: "", +// endpoint: "", +// default_model: "", +// custom_models: "", +// }); +// const [savingConfig, setSavingConfig] = useState(false); - const [promptForm, setPromptForm] = useState({ - name: "", - use_case: "quality_review", - user_template: "Analyze this topic: {{topic}}", - system_message: "You are a quality compliance assistant.", - provider: "", - model: "", - temperature: "0.2", - max_tokens: "800", - }); - const [creatingPrompt, setCreatingPrompt] = useState(false); - const [executePromptId, setExecutePromptId] = useState(""); - const [executeVariables, setExecuteVariables] = useState('{"topic":"CAPA risk assessment"}'); - const [executeResult, setExecuteResult] = useState(""); - const [executeMeta, setExecuteMeta] = useState<{ provider?: string; model?: string }>({}); +// const [promptForm, setPromptForm] = useState({ +// name: "", +// use_case: "quality_review", +// user_template: "Analyze this topic: {{topic}}", +// system_message: "You are a quality compliance assistant.", +// provider: "", +// model: "", +// temperature: "0.2", +// max_tokens: "800", +// }); +// const [creatingPrompt, setCreatingPrompt] = useState(false); +// const [executePromptId, setExecutePromptId] = useState(""); +// const [executeVariables, setExecuteVariables] = useState('{"topic":"CAPA risk assessment"}'); +// const [executeResult, setExecuteResult] = useState(""); +// const [executeMeta, setExecuteMeta] = useState<{ provider?: string; model?: string }>({}); - const [collectionForm, setCollectionForm] = useState({ name: "", description: "" }); - const [creatingCollection, setCreatingCollection] = useState(false); - const [uploadCollectionId, setUploadCollectionId] = useState(""); - const [uploadFile, setUploadFile] = useState(null); - const [uploadingDoc, setUploadingDoc] = useState(false); - const [searchQuery, setSearchQuery] = useState("Summarize the regulatory controls from uploaded SOP."); - const [searchCollectionId, setSearchCollectionId] = useState(""); - const [searchResult, setSearchResult] = useState(""); +// const [collectionForm, setCollectionForm] = useState({ name: "", description: "" }); +// const [creatingCollection, setCreatingCollection] = useState(false); +// const [uploadCollectionId, setUploadCollectionId] = useState(""); +// const [uploadFile, setUploadFile] = useState(null); +// const [uploadingDoc, setUploadingDoc] = useState(false); +// const [searchQuery, setSearchQuery] = useState("Summarize the regulatory controls from uploaded SOP."); +// const [searchCollectionId, setSearchCollectionId] = useState(""); +// const [searchResult, setSearchResult] = useState(""); - const providerOptions = useMemo( - () => providers.map((p) => ({ value: p.name, label: p.displayName || p.name })), - [providers], - ); - const modelOptions = useMemo( - () => - models.map((m) => ({ - value: m.id, - label: `${m.id} (${m.provider})${m.isDefault ? " • default" : ""}`, - })), - [models], - ); - const collectionOptions = useMemo( - () => collections.map((c) => ({ value: c.id, label: c.name })), - [collections], - ); +// const providerOptions = useMemo( +// () => providers.map((p) => ({ value: p.name, label: p.displayName || p.name })), +// [providers], +// ); +// const modelOptions = useMemo( +// () => +// models.map((m) => ({ +// value: m.id, +// label: `${m.id} (${m.provider})${m.isDefault ? " • default" : ""}`, +// })), +// [models], +// ); +// const collectionOptions = useMemo( +// () => collections.map((c) => ({ value: c.id, label: c.name })), +// [collections], +// ); - const loadGatewayData = async (): Promise => { - setIsLoading(true); - try { - const [providerData, modelData, healthData, completionData, costs] = await Promise.all([ - aiService.getProviders(), - aiService.getModels(), - aiService.getGatewayHealth(), - aiService.listCompletions({ page: 1, limit: 10 }), - aiService.getCostSummary(costFilter), - ]); - setProviders(providerData); - setModels(modelData); - setGatewayHealthy(Boolean(healthData?.healthy)); - setCompletions(completionData.data || []); - setCostSummary(costs); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to load AI gateway data"); - } finally { - setIsLoading(false); - } - }; +// const loadGatewayData = async (): Promise => { +// setIsLoading(true); +// try { +// const [providerData, modelData, healthData, completionData, costs] = await Promise.all([ +// aiService.getProviders(), +// aiService.getModels(), +// aiService.getGatewayHealth(), +// aiService.listCompletions({ page: 1, limit: 10 }), +// aiService.getCostSummary(costFilter), +// ]); +// setProviders(providerData); +// setModels(modelData); +// setGatewayHealthy(Boolean(healthData?.healthy)); +// setCompletions(completionData.data || []); +// setCostSummary(costs); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to load AI gateway data"); +// } finally { +// setIsLoading(false); +// } +// }; - const loadConfigData = async (): Promise => { - setIsLoading(true); - try { - const [providerData, configData] = await Promise.all([aiService.getProviders(), aiService.listConfigs()]); - setProviders(providerData); - setConfigs(configData); - setConfigForm((prev) => ({ ...prev, provider: prev.provider || providerData[0]?.name || "" })); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to load config data"); - } finally { - setIsLoading(false); - } - }; +// const loadConfigData = async (): Promise => { +// setIsLoading(true); +// try { +// const [providerData, configData] = await Promise.all([aiService.getProviders(), aiService.listConfigs()]); +// setProviders(providerData); +// setConfigs(configData); +// setConfigForm((prev) => ({ ...prev, provider: prev.provider || providerData[0]?.name || "" })); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to load config data"); +// } finally { +// setIsLoading(false); +// } +// }; - const loadPromptData = async (): Promise => { - setIsLoading(true); - try { - const [providerData, modelData, promptData] = await Promise.all([ - aiService.getProviders(), - aiService.getModels(), - aiService.listPrompts({ page: 1, limit: 20 }), - ]); - setProviders(providerData); - setModels(modelData); - setPrompts(promptData.data || []); - setPromptForm((prev) => ({ ...prev, provider: prev.provider || providerData[0]?.name || "" })); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to load prompts"); - } finally { - setIsLoading(false); - } - }; +// const loadPromptData = async (): Promise => { +// setIsLoading(true); +// try { +// const [providerData, modelData, promptData] = await Promise.all([ +// aiService.getProviders(), +// aiService.getModels(), +// aiService.listPrompts({ page: 1, limit: 20 }), +// ]); +// setProviders(providerData); +// setModels(modelData); +// setPrompts(promptData.data || []); +// setPromptForm((prev) => ({ ...prev, provider: prev.provider || providerData[0]?.name || "" })); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to load prompts"); +// } finally { +// setIsLoading(false); +// } +// }; - const loadKnowledgeData = async (): Promise => { - setIsLoading(true); - try { - const [providerData, collectionData] = await Promise.all([aiService.getProviders(), aiService.listCollections({ page: 1, limit: 20 })]); - setProviders(providerData); - setCollections(collectionData.data || []); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to load knowledge data"); - } finally { - setIsLoading(false); - } - }; +// const loadKnowledgeData = async (): Promise => { +// setIsLoading(true); +// try { +// const [providerData, collectionData] = await Promise.all([aiService.getProviders(), aiService.listCollections({ page: 1, limit: 20 })]); +// setProviders(providerData); +// setCollections(collectionData.data || []); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to load knowledge data"); +// } finally { +// setIsLoading(false); +// } +// }; - useEffect(() => { - if (activeTab === "gateway") void loadGatewayData(); - if (activeTab === "config") void loadConfigData(); - if (activeTab === "prompts") void loadPromptData(); - if (activeTab === "knowledge") void loadKnowledgeData(); - }, [activeTab, costFilter.group_by]); +// useEffect(() => { +// if (activeTab === "gateway") void loadGatewayData(); +// if (activeTab === "config") void loadConfigData(); +// if (activeTab === "prompts") void loadPromptData(); +// if (activeTab === "knowledge") void loadKnowledgeData(); +// }, [activeTab, costFilter.group_by]); - const handleRunPlayground = async (): Promise => { - setCreatingCompletion(true); - try { - const result = await aiService.playground({ - messages: [{ role: "user", content: completionForm.message }], - provider: completionForm.provider || undefined, - model: completionForm.model || undefined, - temperature: Number(completionForm.temperature), - max_tokens: Number(completionForm.max_tokens), - }); - setPlaygroundResult(result.content || result.response || ""); - setPlaygroundMeta({ provider: result.provider, model: result.model, latency: result.latency_ms }); - showToast.success(`Response from ${result.provider || "provider"}`); - await loadGatewayData(); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to run completion"); - } finally { - setCreatingCompletion(false); - } - }; +// const handleRunPlayground = async (): Promise => { +// setCreatingCompletion(true); +// try { +// const result = await aiService.playground({ +// messages: [{ role: "user", content: completionForm.message }], +// provider: completionForm.provider || undefined, +// model: completionForm.model || undefined, +// temperature: Number(completionForm.temperature), +// max_tokens: Number(completionForm.max_tokens), +// }); +// setPlaygroundResult(result.content || result.response || ""); +// setPlaygroundMeta({ provider: result.provider, model: result.model, latency: result.latency_ms }); +// showToast.success(`Response from ${result.provider || "provider"}`); +// await loadGatewayData(); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to run completion"); +// } finally { +// setCreatingCompletion(false); +// } +// }; - const handleSaveConfig = async (): Promise => { - setSavingConfig(true); - try { - await aiService.upsertConfig({ - provider: configForm.provider, - config_type: configForm.config_type as "direct" | "azure", - api_key: configForm.api_key, - endpoint: configForm.endpoint || undefined, - default_model: configForm.default_model || undefined, - custom_models: configForm.custom_models - ? configForm.custom_models.split(",").map((m) => m.trim()).filter(Boolean) - : undefined, - }); - showToast.success("Provider configuration saved"); - setConfigForm((prev) => ({ ...prev, api_key: "" })); - await loadConfigData(); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to save config"); - } finally { - setSavingConfig(false); - } - }; +// const handleSaveConfig = async (): Promise => { +// setSavingConfig(true); +// try { +// await aiService.upsertConfig({ +// provider: configForm.provider, +// config_type: configForm.config_type as "direct" | "azure", +// api_key: configForm.api_key, +// endpoint: configForm.endpoint || undefined, +// default_model: configForm.default_model || undefined, +// custom_models: configForm.custom_models +// ? configForm.custom_models.split(",").map((m) => m.trim()).filter(Boolean) +// : undefined, +// }); +// showToast.success("Provider configuration saved"); +// setConfigForm((prev) => ({ ...prev, api_key: "" })); +// await loadConfigData(); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to save config"); +// } finally { +// setSavingConfig(false); +// } +// }; - const handleCreatePrompt = async (): Promise => { - setCreatingPrompt(true); - try { - await aiService.createPrompt({ - name: promptForm.name, - use_case: promptForm.use_case, - user_template: promptForm.user_template, - system_message: promptForm.system_message || undefined, - provider: promptForm.provider || undefined, - model: promptForm.model || undefined, - temperature: Number(promptForm.temperature), - max_tokens: Number(promptForm.max_tokens), - variables: [{ name: "topic", type: "string", required: true }], - }); - showToast.success("Prompt created"); - setPromptForm((prev) => ({ ...prev, name: "" })); - await loadPromptData(); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to create prompt"); - } finally { - setCreatingPrompt(false); - } - }; +// const handleCreatePrompt = async (): Promise => { +// setCreatingPrompt(true); +// try { +// await aiService.createPrompt({ +// name: promptForm.name, +// use_case: promptForm.use_case, +// user_template: promptForm.user_template, +// system_message: promptForm.system_message || undefined, +// provider: promptForm.provider || undefined, +// model: promptForm.model || undefined, +// temperature: Number(promptForm.temperature), +// max_tokens: Number(promptForm.max_tokens), +// variables: [{ name: "topic", type: "string", required: true }], +// }); +// showToast.success("Prompt created"); +// setPromptForm((prev) => ({ ...prev, name: "" })); +// await loadPromptData(); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to create prompt"); +// } finally { +// setCreatingPrompt(false); +// } +// }; - const handleExecutePrompt = async (): Promise => { - if (!executePromptId) { - showToast.error("Select a prompt to test"); - return; - } - try { - const variables = JSON.parse(executeVariables); - const result = await aiService.testPrompt(executePromptId, { variables }); - setExecuteResult(result.content || result.response || ""); - setExecuteMeta({ provider: result.provider, model: result.model }); - showToast.success("Prompt tested successfully"); - } catch (error: any) { - showToast.error(error?.message || error?.response?.data?.error?.message || "Failed to execute prompt"); - } - }; +// const handleExecutePrompt = async (): Promise => { +// if (!executePromptId) { +// showToast.error("Select a prompt to test"); +// return; +// } +// try { +// const variables = JSON.parse(executeVariables); +// const result = await aiService.testPrompt(executePromptId, { variables }); +// setExecuteResult(result.content || result.response || ""); +// setExecuteMeta({ provider: result.provider, model: result.model }); +// showToast.success("Prompt tested successfully"); +// } catch (error: any) { +// showToast.error(error?.message || error?.response?.data?.error?.message || "Failed to execute prompt"); +// } +// }; - const handleCreateCollection = async (): Promise => { - setCreatingCollection(true); - try { - await aiService.createCollection({ - name: collectionForm.name, - description: collectionForm.description || undefined, - }); - setCollectionForm({ name: "", description: "" }); - showToast.success("Collection created"); - await loadKnowledgeData(); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to create collection"); - } finally { - setCreatingCollection(false); - } - }; +// const handleCreateCollection = async (): Promise => { +// setCreatingCollection(true); +// try { +// await aiService.createCollection({ +// name: collectionForm.name, +// description: collectionForm.description || undefined, +// }); +// setCollectionForm({ name: "", description: "" }); +// showToast.success("Collection created"); +// await loadKnowledgeData(); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to create collection"); +// } finally { +// setCreatingCollection(false); +// } +// }; - const handleUploadDoc = async (): Promise => { - if (!uploadCollectionId || !uploadFile) { - showToast.error("Select collection and file"); - return; - } - setUploadingDoc(true); - try { - await aiService.uploadKnowledgeDocument({ collectionId: uploadCollectionId, file: uploadFile }); - showToast.success("Document uploaded and queued for ingestion"); - setUploadFile(null); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "Failed to upload document"); - } finally { - setUploadingDoc(false); - } - }; +// const handleUploadDoc = async (): Promise => { +// if (!uploadCollectionId || !uploadFile) { +// showToast.error("Select collection and file"); +// return; +// } +// setUploadingDoc(true); +// try { +// await aiService.uploadKnowledgeDocument({ collectionId: uploadCollectionId, file: uploadFile }); +// showToast.success("Document uploaded and queued for ingestion"); +// setUploadFile(null); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "Failed to upload document"); +// } finally { +// setUploadingDoc(false); +// } +// }; - const handleSearchRag = async (): Promise => { - try { - const result = await aiService.searchKnowledgeWithContext({ - query: searchQuery, - collectionId: searchCollectionId || undefined, - topK: 5, - minScore: 0.7, - }); - setSearchResult(result.context || JSON.stringify(result.matches || [], null, 2)); - } catch (error: any) { - showToast.error(error?.response?.data?.error?.message || "RAG search failed"); - } - }; +// const handleSearchRag = async (): Promise => { +// try { +// const result = await aiService.searchKnowledgeWithContext({ +// query: searchQuery, +// collectionId: searchCollectionId || undefined, +// topK: 5, +// minScore: 0.7, +// }); +// setSearchResult(result.context || JSON.stringify(result.matches || [], null, 2)); +// } catch (error: any) { +// showToast.error(error?.response?.data?.error?.message || "RAG search failed"); +// } +// }; - const providerColumns: Column[] = [ - { key: "name", label: "Provider", render: (row) => row.displayName || row.name }, - { - key: "status", - label: "Status", - render: (row) => ( - - {row.isEnabled ? "Enabled" : "Disabled"} - - ), - }, - { key: "defaultModel", label: "Default Model", render: (row) => row.defaultModel || "-" }, - ]; +// const providerColumns: Column[] = [ +// { key: "name", label: "Provider", render: (row) => row.displayName || row.name }, +// { +// key: "status", +// label: "Status", +// render: (row) => ( +// +// {row.isEnabled ? "Enabled" : "Disabled"} +// +// ), +// }, +// { key: "defaultModel", label: "Default Model", render: (row) => row.defaultModel || "-" }, +// ]; - const completionColumns: Column[] = [ - { key: "provider", label: "Provider", render: (row) => row.provider || "-" }, - { key: "model", label: "Model", render: (row) => row.model || "-" }, - { key: "tokens", label: "Tokens", align: "right", render: (row) => String(row.usage?.total_tokens ?? 0) }, - { - key: "fallback", - label: "Fallback", - render: (row) => ( - - {row.fallbackUsed ? "Used" : "No"} - - ), - }, - { key: "latency", label: "Latency", align: "right", render: (row) => `${row.latency_ms ?? 0} ms` }, - ]; +// const completionColumns: Column[] = [ +// { key: "provider", label: "Provider", render: (row) => row.provider || "-" }, +// { key: "model", label: "Model", render: (row) => row.model || "-" }, +// { key: "tokens", label: "Tokens", align: "right", render: (row) => String(row.usage?.total_tokens ?? 0) }, +// { +// key: "fallback", +// label: "Fallback", +// render: (row) => ( +// +// {row.fallbackUsed ? "Used" : "No"} +// +// ), +// }, +// { key: "latency", label: "Latency", align: "right", render: (row) => `${row.latency_ms ?? 0} ms` }, +// ]; - const providerCostColumns: Column<{ provider: string; completions: number; tokens: number; cost: number }>[] = [ - { key: "provider", label: "Provider" }, - { key: "completions", label: "Completions", align: "right" }, - { key: "tokens", label: "Tokens", align: "right" }, - { key: "cost", label: "Cost (USD)", align: "right", render: (row) => `$${(row.cost || 0).toFixed(4)}` }, - ]; +// const providerCostColumns: Column<{ provider: string; completions: number; tokens: number; cost: number }>[] = [ +// { key: "provider", label: "Provider" }, +// { key: "completions", label: "Completions", align: "right" }, +// { key: "tokens", label: "Tokens", align: "right" }, +// { key: "cost", label: "Cost (USD)", align: "right", render: (row) => `$${(row.cost || 0).toFixed(4)}` }, +// ]; - const configColumns: Column[] = [ - { key: "provider", label: "Provider" }, - { key: "config_type", label: "Config Type" }, - { key: "default_model", label: "Default Model", render: (row) => row.default_model || "-" }, - { - key: "state", - label: "State", - render: (row) => ( - - {row.is_active ? "Active" : "Inactive"} - - ), - }, - { - key: "actions", - label: "Actions", - align: "right", - render: (row) => ( -
- - -
- ), - }, - ]; +// const configColumns: Column[] = [ +// { key: "provider", label: "Provider" }, +// { key: "config_type", label: "Config Type" }, +// { key: "default_model", label: "Default Model", render: (row) => row.default_model || "-" }, +// { +// key: "state", +// label: "State", +// render: (row) => ( +// +// {row.is_active ? "Active" : "Inactive"} +// +// ), +// }, +// { +// key: "actions", +// label: "Actions", +// align: "right", +// render: (row) => ( +//
+// +// +//
+// ), +// }, +// ]; - const promptColumns: Column[] = [ - { key: "name", label: "Prompt Name" }, - { key: "useCase", label: "Use Case" }, - { key: "provider", label: "Provider", render: (row) => row.defaultParameters?.provider || "-" }, - { key: "model", label: "Model", render: (row) => row.defaultParameters?.model || "-" }, - { - key: "status", - label: "Status", - render: (row) => ( - - {row.status || "draft"} - - ), - }, - { - key: "activate", - label: "Action", - align: "right", - render: (row) => ( - - ), - }, - ]; +// const promptColumns: Column[] = [ +// { key: "name", label: "Prompt Name" }, +// { key: "useCase", label: "Use Case" }, +// { key: "provider", label: "Provider", render: (row) => row.defaultParameters?.provider || "-" }, +// { key: "model", label: "Model", render: (row) => row.defaultParameters?.model || "-" }, +// { +// key: "status", +// label: "Status", +// render: (row) => ( +// +// {row.status || "draft"} +// +// ), +// }, +// { +// key: "activate", +// label: "Action", +// align: "right", +// render: (row) => ( +// +// ), +// }, +// ]; - const collectionColumns: Column[] = [ - { key: "name", label: "Collection" }, - { key: "description", label: "Description", render: (row) => row.description || "-" }, - { - key: "status", - label: "Status", - render: (row) => ( - - {row.status || "active"} - - ), - }, - ]; +// const collectionColumns: Column[] = [ +// { key: "name", label: "Collection" }, +// { key: "description", label: "Description", render: (row) => row.description || "-" }, +// { +// key: "status", +// label: "Status", +// render: (row) => ( +// +// {row.status || "active"} +// +// ), +// }, +// ]; - return ( - -
-
-
-
-
- -
-
-

{tabMeta[activeTab].label}

-

{tabMeta[activeTab].subtitle}

-
-
-
- {(Object.keys(tabPath) as TabKey[]).map((tab) => { - const Icon = tabMeta[tab].icon; - return ( - - - {tabMeta[tab].label} - - ); - })} -
-
-
+// return ( +// +//
+//
+//
+//
+//
+// +//
+//
+//

{tabMeta[activeTab].label}

+//

{tabMeta[activeTab].subtitle}

+//
+//
+//
+// {(Object.keys(tabPath) as TabKey[]).map((tab) => { +// const Icon = tabMeta[tab].icon; +// return ( +// +// +// {tabMeta[tab].label} +// +// ); +// })} +//
+//
+//
- {activeTab === "gateway" && ( - <> -
- - - - -
+// {activeTab === "gateway" && ( +// <> +//
+// +// +// +// +//
-
- -
- setCompletionForm((prev) => ({ ...prev, provider: value }))} - options={providerOptions} - placeholder="Auto" - /> - setCompletionForm((prev) => ({ ...prev, model: value }))} - options={modelOptions} - placeholder="Provider default" - /> -
- setCompletionForm((prev) => ({ ...prev, message: e.target.value }))} - /> -
- setCompletionForm((prev) => ({ ...prev, temperature: e.target.value }))} - /> - setCompletionForm((prev) => ({ ...prev, max_tokens: e.target.value }))} - /> -
-
- - {creatingCompletion ? "Running..." : "Run Playground"} - - void loadGatewayData()}>Refresh Data -
-
+//
+// +//
+// setCompletionForm((prev) => ({ ...prev, provider: value }))} +// options={providerOptions} +// placeholder="Auto" +// /> +// setCompletionForm((prev) => ({ ...prev, model: value }))} +// options={modelOptions} +// placeholder="Provider default" +// /> +//
+// setCompletionForm((prev) => ({ ...prev, message: e.target.value }))} +// /> +//
+// setCompletionForm((prev) => ({ ...prev, temperature: e.target.value }))} +// /> +// setCompletionForm((prev) => ({ ...prev, max_tokens: e.target.value }))} +// /> +//
+//
+// +// {creatingCompletion ? "Running..." : "Run Playground"} +// +// void loadGatewayData()}>Refresh Data +//
+//
-
- -
- {playgroundMeta.provider || "Provider: -"} - {playgroundMeta.model || "Model: -"} - - Latency: {playgroundMeta.latency ? `${playgroundMeta.latency} ms` : "-"} - -
-
-

{playgroundResult || "No output yet."}

-
-
+//
+// +//
+// {playgroundMeta.provider || "Provider: -"} +// {playgroundMeta.model || "Model: -"} +// +// Latency: {playgroundMeta.latency ? `${playgroundMeta.latency} ms` : "-"} +// +//
+//
+//

{playgroundResult || "No output yet."}

+//
+//
- -
- setCostFilter({ group_by: value as "day" | "week" | "month" })} - options={[ - { value: "day", label: "Daily" }, - { value: "week", label: "Weekly" }, - { value: "month", label: "Monthly" }, - ]} - /> - - - -
- `${item.provider}-${item.completions}`} - emptyMessage="No cost data available" - isLoading={isLoading} - /> -
-
-
+// +//
+// setCostFilter({ group_by: value as "day" | "week" | "month" })} +// options={[ +// { value: "day", label: "Daily" }, +// { value: "week", label: "Weekly" }, +// { value: "month", label: "Monthly" }, +// ]} +// /> +// +// +// +//
+// `${item.provider}-${item.completions}`} +// emptyMessage="No cost data available" +// isLoading={isLoading} +// /> +//
+//
+//
- -
- item.name} - emptyMessage="No providers available" - isLoading={isLoading} - /> - item.id} - emptyMessage="No completions yet" - isLoading={isLoading} - /> -
-
- - )} +// +//
+// item.name} +// emptyMessage="No providers available" +// isLoading={isLoading} +// /> +// item.id} +// emptyMessage="No completions yet" +// isLoading={isLoading} +// /> +//
+//
+// +// )} - {activeTab === "config" && ( -
- - setConfigForm((prev) => ({ ...prev, provider: value }))} - options={providerOptions} - /> - setConfigForm((prev) => ({ ...prev, config_type: value }))} - options={[ - { value: "direct", label: "Direct Endpoint" }, - { value: "azure", label: "Azure" }, - ]} - /> - setConfigForm((prev) => ({ ...prev, api_key: e.target.value }))} - required - /> - setConfigForm((prev) => ({ ...prev, endpoint: e.target.value }))} - placeholder="http://host.docker.internal:11434/v1" - /> - setConfigForm((prev) => ({ ...prev, default_model: e.target.value }))} - /> - setConfigForm((prev) => ({ ...prev, custom_models: e.target.value }))} - placeholder="mistral-small3.2:24b,llama3.2" - /> -
- - {savingConfig ? "Saving..." : "Save Configuration"} - - void loadConfigData()}>Reload -
-
+// {activeTab === "config" && ( +//
+// +// setConfigForm((prev) => ({ ...prev, provider: value }))} +// options={providerOptions} +// /> +// setConfigForm((prev) => ({ ...prev, config_type: value }))} +// options={[ +// { value: "direct", label: "Direct Endpoint" }, +// { value: "azure", label: "Azure" }, +// ]} +// /> +// setConfigForm((prev) => ({ ...prev, api_key: e.target.value }))} +// required +// /> +// setConfigForm((prev) => ({ ...prev, endpoint: e.target.value }))} +// placeholder="http://host.docker.internal:11434/v1" +// /> +// setConfigForm((prev) => ({ ...prev, default_model: e.target.value }))} +// /> +// setConfigForm((prev) => ({ ...prev, custom_models: e.target.value }))} +// placeholder="mistral-small3.2:24b,llama3.2" +// /> +//
+// +// {savingConfig ? "Saving..." : "Save Configuration"} +// +// void loadConfigData()}>Reload +//
+//
-
- - item.id} - emptyMessage="No tenant provider configs found" - isLoading={isLoading} - /> - - -
- Example endpoint: - - http://host.docker.internal:11434/v1 - -
-
-
-
- )} +//
+// +// item.id} +// emptyMessage="No tenant provider configs found" +// isLoading={isLoading} +// /> +// +// +//
+// Example endpoint: +// +// http://host.docker.internal:11434/v1 +// +//
+//
+//
+//
+// )} - {activeTab === "prompts" && ( -
-
- - setPromptForm((prev) => ({ ...prev, name: e.target.value }))} - required - /> - setPromptForm((prev) => ({ ...prev, use_case: e.target.value }))} - required - /> - setPromptForm((prev) => ({ ...prev, system_message: e.target.value }))} - /> - setPromptForm((prev) => ({ ...prev, user_template: e.target.value }))} - /> -
- setPromptForm((prev) => ({ ...prev, provider: value }))} - options={providerOptions} - placeholder="Auto" - /> - setPromptForm((prev) => ({ ...prev, model: value }))} - options={modelOptions} - placeholder="Provider default" - /> -
-
- setPromptForm((prev) => ({ ...prev, temperature: e.target.value }))} - /> - setPromptForm((prev) => ({ ...prev, max_tokens: e.target.value }))} - /> -
-
- - {creatingPrompt ? "Creating..." : "Create Prompt"} - - void loadPromptData()}>Refresh -
-
+// {activeTab === "prompts" && ( +//
+//
+// +// setPromptForm((prev) => ({ ...prev, name: e.target.value }))} +// required +// /> +// setPromptForm((prev) => ({ ...prev, use_case: e.target.value }))} +// required +// /> +// setPromptForm((prev) => ({ ...prev, system_message: e.target.value }))} +// /> +// setPromptForm((prev) => ({ ...prev, user_template: e.target.value }))} +// /> +//
+// setPromptForm((prev) => ({ ...prev, provider: value }))} +// options={providerOptions} +// placeholder="Auto" +// /> +// setPromptForm((prev) => ({ ...prev, model: value }))} +// options={modelOptions} +// placeholder="Provider default" +// /> +//
+//
+// setPromptForm((prev) => ({ ...prev, temperature: e.target.value }))} +// /> +// setPromptForm((prev) => ({ ...prev, max_tokens: e.target.value }))} +// /> +//
+//
+// +// {creatingPrompt ? "Creating..." : "Create Prompt"} +// +// void loadPromptData()}>Refresh +//
+//
- - ({ - value: p.id, - label: `${p.name} (${p.status || "draft"})`, - }))} - /> - setExecuteVariables(e.target.value)} - /> -
- Run Prompt Test -
-
- {executeMeta.provider || "Provider: -"} - {executeMeta.model || "Model: -"} -
-
-

{executeResult || "No test output yet."}

-
-
-
+// +// ({ +// value: p.id, +// label: `${p.name} (${p.status || "draft"})`, +// }))} +// /> +// setExecuteVariables(e.target.value)} +// /> +//
+// Run Prompt Test +//
+//
+// {executeMeta.provider || "Provider: -"} +// {executeMeta.model || "Model: -"} +//
+//
+//

{executeResult || "No test output yet."}

+//
+//
+//
- - item.id} - emptyMessage="No prompts found" - isLoading={isLoading} - /> - -
- )} +// +// item.id} +// emptyMessage="No prompts found" +// isLoading={isLoading} +// /> +// +//
+// )} - {activeTab === "knowledge" && ( -
-
- - setCollectionForm((prev) => ({ ...prev, name: e.target.value }))} - required - /> - setCollectionForm((prev) => ({ ...prev, description: e.target.value }))} - /> - - {creatingCollection ? "Creating..." : "Create Collection"} - - +// {activeTab === "knowledge" && ( +//
+//
+// +// setCollectionForm((prev) => ({ ...prev, name: e.target.value }))} +// required +// /> +// setCollectionForm((prev) => ({ ...prev, description: e.target.value }))} +// /> +// +// {creatingCollection ? "Creating..." : "Create Collection"} +// +// - - -
- - setUploadFile(e.target.files?.[0] || null)} - className="h-10 w-full px-3.5 py-1 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm" - /> -
- - {uploadingDoc ? "Uploading..." : "Upload & Ingest"} - -
+// +// +//
+// +// setUploadFile(e.target.files?.[0] || null)} +// className="h-10 w-full px-3.5 py-1 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm" +// /> +//
+// +// {uploadingDoc ? "Uploading..." : "Upload & Ingest"} +// +//
- - - setSearchQuery(e.target.value)} - /> -
- Search Context - void loadKnowledgeData()}>Refresh -
-
-
+// +// +// setSearchQuery(e.target.value)} +// /> +//
+// Search Context +// void loadKnowledgeData()}>Refresh +//
+//
+//
- -
-

{searchResult || "No context result yet."}

-
-
+// +//
+//

{searchResult || "No context result yet."}

+//
+//
- - item.id} - emptyMessage="No collections available" - isLoading={isLoading} - /> - -
- )} -
- - ); -}; +// +// item.id} +// emptyMessage="No collections available" +// isLoading={isLoading} +// /> +// +//
+// )} +//
+//
+// ); +// }; -export default AIGateway; +// export default AIGateway; diff --git a/src/pages/tenant/TenantAIDashboard.tsx b/src/pages/tenant/TenantAIDashboard.tsx new file mode 100644 index 0000000..d6f4feb --- /dev/null +++ b/src/pages/tenant/TenantAIDashboard.tsx @@ -0,0 +1,342 @@ +import { useEffect, useState, type ReactElement } from "react"; +import { Layout } from "@/components/layout/Layout"; +import { DataTable, type Column, FilterDropdown } from "@/components/shared"; +import { aiService } from "@/services/ai-service"; +import type { AICostSummary } from "@/types/ai"; +import { showToast } from "@/utils/toast"; +import { + Calendar, + // RefreshCw, + MessageSquare, + Coins, + DollarSign, + Timer, +} from "lucide-react"; +import { formatDate } from "@/utils/format-date"; + +type GroupBy = "day" | "week" | "month"; + +export const TenantAIDashboard = (): ReactElement => { + const [groupBy, setGroupBy] = useState(null); + const [startDate, setStartDate] = useState(""); + const [endDate, setEndDate] = useState(""); + const [isLoading, setIsLoading] = useState(true); + const [costs, setCosts] = useState(null); + + useEffect(() => { + if (!groupBy) { + setStartDate(""); + setEndDate(""); + return; + } + const now = new Date(); + const start = new Date(); + if (groupBy === "day") { + start.setDate(now.getDate() - 1); + } else if (groupBy === "week") { + start.setDate(now.getDate() - 7); + } else if (groupBy === "month") { + start.setDate(now.getDate() - 30); + } + setStartDate(start.toISOString().split("T")[0]); + setEndDate(now.toISOString().split("T")[0]); + }, [groupBy]); + + const fetchCostSummary = async ( + group: GroupBy | null = groupBy, + start: string = startDate, + end: string = endDate, + ) => { + 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, + }); + setCosts(data); + } catch (err: any) { + showToast.error( + err?.response?.data?.message || "Failed to fetch cost summary", + ); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (!groupBy || (startDate && endDate)) { + void fetchCostSummary(groupBy, startDate, endDate); + } + }, [groupBy, startDate, endDate]); + + const providerColumns: Column[] = [ + { + key: "provider", + label: "Provider", + render: (row) => ( + + {row.provider} + + ), + }, + { + key: "completions", + label: "Requests", + align: "right", + render: (row) => ( + {row.completions} + ), + }, + { + key: "tokens", + label: "Tokens", + align: "right", + render: (row) => ( + {row.tokens} + ), + }, + { + key: "cost", + label: "Cost (USD)", + align: "right", + render: (row) => ( + + ${(row.cost || 0).toFixed(6)} + + ), + }, + ]; + + const modelColumns: Column[] = [ + { + key: "model", + label: "Model", + render: (row) => ( +
+ + {row.model} + + + {row.provider} + +
+ ), + }, + { + key: "completions", + label: "Requests", + align: "right", + render: (row) => ( + {row.completions} + ), + }, + { + key: "tokens", + label: "Tokens", + align: "right", + render: (row) => ( + {row.tokens} + ), + }, + { + key: "cost", + label: "Cost (USD)", + align: "right", + render: (row) => ( + + ${(row.cost || 0).toFixed(6)} + + ), + }, + ]; + + // Formatting helpers + const formatTokens = (t: number) => { + if (!t) return "0"; + if (t >= 1000000) return `${(t / 1000000).toFixed(2)}M`; + if (t >= 1000) return `${(t / 1000).toFixed(1)}K`; + return t.toString(); + }; + + const formatLatency = (ms: number) => { + if (!ms) return "0ms"; + if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`; + return `${Math.round(ms)}ms`; + }; + + return ( + +
+ {/* Top Controls Row */} + + {/* Filters */} +
+ {/* Date Range */} +
+ + +
+ + {/* Group By */} +
+ { + setGroupBy(val as GroupBy | null); + }} + placeholder="All" + /> +
+
+ + {/* Stats Row */} +
+ {/* Card 1 */} +
+
+
+ +
+ +
+

+ {costs?.summary?.total_completions || 0} +

+

+ Total Completions +

+
+
+
+ + {/* Card 2 */} +
+
+
+ +
+ +
+

+ {formatTokens(costs?.summary?.total_tokens || 0)} +

+

+ Total Tokens +

+
+
+
+ + {/* Card 3 */} +
+
+
+ +
+ +
+

+ ${(costs?.summary?.total_cost || 0).toFixed(2)} +

+

+ Total Cost (USD) +

+
+
+
+ + {/* Card 4 */} +
+
+
+ +
+ +
+

+ {formatLatency(costs?.summary?.avg_latency_ms || 0)} +

+

+ Avg Latency (ms) +

+
+
+
+
+ + {/* Charts and Tables */} +
+ {/* Usage by Provider Section */} +
+
+

+ Usage by Provider +

+

+ Breakdown of requests, tokens and usage costs for each distinct + provider. +

+
+ +
+ + item.provider || Math.random().toString() + } + isLoading={isLoading} + emptyMessage="No provider metrics available for this period." + /> +
+
+ + {/* Cost by Model Section */} +
+
+

+ Cost by Model +

+

+ Breakdown of requests and costs grouped at the AI model level. +

+
+ +
+ + `${item.model}-${item.provider}` || Math.random().toString() + } + isLoading={isLoading} + emptyMessage="No model metrics available for this period." + /> +
+
+
+
+
+ ); +}; + +export default TenantAIDashboard; diff --git a/src/pages/tenant/TenantAIProviderCreate.tsx b/src/pages/tenant/TenantAIProviderCreate.tsx new file mode 100644 index 0000000..afdc732 --- /dev/null +++ b/src/pages/tenant/TenantAIProviderCreate.tsx @@ -0,0 +1,377 @@ +import { type ReactElement, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Layout } from "@/components/layout/Layout"; +import { + FormField, + PrimaryButton, + SecondaryButton, + FormTagInput, +} from "@/components/shared"; +import { ArrowLeft } from "lucide-react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { aiService } from "@/services/ai-service"; +import { showToast } from "@/utils/toast"; + +const createConfigSchema = z.object({ + provider: z.string().min(1, "Provider vendor is required"), + display_name: z.string().optional(), + config_type: z.enum(["direct", "azure"]), + is_active: z.boolean().default(true), + api_key: z.string().min(1, "API Key is required"), + endpoint: z.string().url().max(500).optional(), + deployment: z.string().optional(), + api_version: z.string().optional(), + default_model: z.string().min(1, "Default model is required"), + custom_models: z.array(z.string()).default([]), + default_embedding_model: z.string().optional(), + custom_embedding_models: z.array(z.string()).default([]), +}); + +export const TenantAIProviderCreate = (): ReactElement => { + const navigate = useNavigate(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const { + control, + handleSubmit, + watch, + setError, + formState: { errors }, + } = useForm({ + resolver: zodResolver(createConfigSchema), + defaultValues: { + provider: "", + display_name: "", + config_type: "direct", + is_active: true, + api_key: "", + endpoint: "", + deployment: "", + api_version: "", + default_model: "", + custom_models: [], + default_embedding_model: "", + custom_embedding_models: [], + }, + }); + + const selectedConfigType = watch("config_type"); + + const onFormSubmit = async (data: any) => { + setIsSubmitting(true); + try { + await aiService.upsertConfig({ + provider: data.provider, + display_name: data.display_name || undefined, + config_type: data.config_type, + is_active: data.is_active, + api_key: data.api_key, + endpoint: data.endpoint || undefined, + deployment: data.deployment || undefined, + api_version: data.api_version || undefined, + default_model: data.default_model, + custom_models: data.custom_models || [], + default_embedding_model: data.default_embedding_model || undefined, + custom_embedding_models: data.custom_embedding_models || [], + } as any); + + showToast.success("AI Provider configuration created successfully!"); + navigate("/tenant/ai/providers"); + } catch (err: any) { + if (err?.response?.data?.details && Array.isArray(err.response.data.details)) { + err.response.data.details.forEach((detail: any) => { + if (detail.path) { + setError(detail.path, { type: "server", message: detail.message }); + } + }); + } + const msg = + err?.response?.data?.message || + err?.response?.data?.error || + "Failed to save AI Provider configuration."; + showToast.error(typeof msg === "string" ? msg : "Validation failed"); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + navigate("/tenant/ai/providers")} + className="h-10 px-5 min-w-[120px]" + > + Cancel + + + {isSubmitting ? "Saving..." : "Save Configuration"} + + + ), + }} + > +
+ +
+ +
+ {/* General Settings Section */} +
+

+ General Settings +

+
+ ( + + )} + /> + + ( + + )} + /> +
+ +
+
+ + Config Type * + + + Select between Direct and Azure API configurations. + +
+ + ( +
+ + +
+ )} + /> +
+ +
+
+ + Enable Configuration + + + Activate this configuration to allow processing completions via this provider. + +
+ + ( +
field.onChange(!field.value)} + className={`relative w-11 h-6 flex items-center rounded-full transition-colors cursor-pointer select-none ${ + field.value ? "bg-[#112868]" : "bg-slate-200" + }`} + > +
+
+ )} + /> +
+
+ + {/* Connection Credentials Section */} +
+

+ Connection Credentials +

+
+ ( + + )} + /> + + ( + + )} + /> + + {selectedConfigType === "azure" && ( + <> + ( + + )} + /> + + ( + + )} + /> + + )} +
+
+ + {/* Models */} +
+

+ Models +

+
+ ( + + )} + /> + + ( + + )} + /> +
+ +
+ ( + + )} + /> + + ( + + )} + /> +
+
+ + + ); +}; + +export default TenantAIProviderCreate; diff --git a/src/pages/tenant/TenantAIProviders.tsx b/src/pages/tenant/TenantAIProviders.tsx new file mode 100644 index 0000000..cbb5290 --- /dev/null +++ b/src/pages/tenant/TenantAIProviders.tsx @@ -0,0 +1,307 @@ +import { useEffect, useMemo, useState, type ReactElement } from "react"; +import { useNavigate } from "react-router-dom"; +import { Layout } from "@/components/layout/Layout"; +import { + DataTable, + type Column, + SearchBox, + FilterDropdown, + ActionDropdown, + PrimaryButton, +} from "@/components/shared"; +import { Plus, Play, Loader2, Eye, Trash2 } from "lucide-react"; +import { aiService } from "@/services/ai-service"; +import type { TenantAIConfig } from "@/types/ai"; +import { showToast } from "@/utils/toast"; +import { formatDate } from "@/utils/format-date"; +import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal"; + +export const TenantAIProviders = (): ReactElement => { + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [configs, setConfigs] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState(""); + + const [testingProviders, setTestingProviders] = useState>({}); + const [selectedConfig, setSelectedConfig] = useState(null); + const [isViewModalOpen, setIsViewModalOpen] = useState(false); + + const fetchConfigs = async () => { + setIsLoading(true); + setError(null); + try { + const data = await aiService.listConfigs(); + setConfigs(data || []); + } catch (err: any) { + const msg = err?.response?.data?.error?.message || "Failed to load configs"; + setError(msg); + showToast.error(msg); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + void fetchConfigs(); + }, []); + + const handleTestConnection = async (provider: string) => { + setTestingProviders((prev) => ({ ...prev, [provider]: true })); + try { + const resp = await aiService.testConfig(provider); + if (resp && resp.healthy) { + showToast.success( + `Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms` + ); + } else { + showToast.error(`Connection test failed for ${provider}.`); + } + } catch (err: any) { + const msg = err?.response?.data?.error?.message || "Failed to test connection."; + showToast.error(msg); + } finally { + setTestingProviders((prev) => ({ ...prev, [provider]: false })); + } + }; + + const handleViewConfig = async (provider: string) => { + try { + const cfg = await aiService.getConfig(provider); + setSelectedConfig(cfg); + setIsViewModalOpen(true); + } catch (err: any) { + const msg = err?.response?.data?.error?.message || "Failed to fetch AI Provider config details."; + showToast.error(msg); + } + }; + + const handleDeleteConfig = async (provider: string) => { + if (!window.confirm(`Are you sure you want to delete the AI provider configuration for ${provider}?`)) { + return; + } + try { + await aiService.deleteConfig(provider); + showToast.success(`${provider} config removed successfully`); + void fetchConfigs(); + } catch (err: any) { + const msg = err?.response?.data?.error?.message || "Failed to delete AI Provider configuration."; + showToast.error(msg); + } + }; + + const clearFilters = () => { + setSearchQuery(""); + setStatusFilter(""); + }; + + const filteredConfigs = useMemo(() => { + return configs.filter((cfg) => { + const searchMatches = + !searchQuery.trim() || + (cfg.provider || "").toLowerCase().includes(searchQuery.toLowerCase()) || + (cfg.display_name || "").toLowerCase().includes(searchQuery.toLowerCase()) || + (cfg.default_model || "").toLowerCase().includes(searchQuery.toLowerCase()); + + const statusMatches = + !statusFilter || + (statusFilter === "active" ? cfg.is_active : !cfg.is_active); + + return searchMatches && statusMatches; + }); + }, [configs, searchQuery, statusFilter]); + + const columns: Column[] = useMemo( + () => [ + { + key: "provider", + label: "Provider", + render: (row) => ( +
+

+ {row.display_name || row.provider} +

+ {row.endpoint && ( +

+ {row.endpoint} +

+ )} +
+ ), + }, + { + key: "config_type", + label: "Config Type", + render: (row) => { + const type = row.config_type || "direct"; + return ( + + {type} + + ); + }, + }, + { + key: "default_model", + label: "Default Model", + render: (row) => ( + + {row.default_model || "—"} + + ), + }, + { + key: "is_active", + label: "Status", + render: (row) => { + const active = row.is_active; + return ( + + + {active ? "Active" : "Disabled"} + + ); + }, + }, + { + key: "last_verified_at", + label: "Last Verified", + render: (row) => ( + + {row.last_verified_at ? formatDate(row.last_verified_at) : "Never"} + + ), + }, + { + key: "actions", + label: "Actions", + align: "right", + render: (row) => { + const isTesting = testingProviders[row.provider] || false; + return ( +
+ + + , + label: "View Config", + onClick: () => handleViewConfig(row.provider), + }, + { + icon: , + label: "Delete Config", + onClick: () => handleDeleteConfig(row.provider), + }, + ]} + /> +
+ ); + }, + }, + ], + [testingProviders] + ); + + return ( + navigate("/tenant/ai/providers/create")} + className="h-10 px-4 flex items-center gap-1.5" + > + + Create AI Provider + + ), + }} + > +
+ {/* Subhead Toolbar matching Screenshot filter design */} +
+
+ + + setStatusFilter(typeof v === "string" ? v : "")} + placeholder="All" + options={[ + { value: "active", label: "Active" }, + { value: "disabled", label: "Disabled" }, + ]} + /> + + {(searchQuery || statusFilter) && ( + + )} +
+
+ + {/* Table list */} +
+ item.id || item.provider} + isLoading={isLoading} + error={error} + emptyMessage="No tenant AI providers configured." + /> +
+
+ + setIsViewModalOpen(false)} + config={selectedConfig} + /> +
+ ); +}; + +export default TenantAIProviders; diff --git a/src/routes/tenant-admin-routes.tsx b/src/routes/tenant-admin-routes.tsx index 2453dc9..11251ee 100644 --- a/src/routes/tenant-admin-routes.tsx +++ b/src/routes/tenant-admin-routes.tsx @@ -38,7 +38,7 @@ const FileView = lazy(() => import("@/pages/tenant/FileView")); const StorageDashboard = lazy(() => import("@/pages/tenant/StorageDashboard")); const SmtpSettings = lazy(() => import("@/pages/tenant/SmtpSettings")); const FailedEmails = lazy(() => import("@/pages/tenant/FailedEmails")); -const AIGateway = lazy(() => import("@/pages/tenant/AIGateway")); +// const AIGateway = lazy(() => import("@/pages/tenant/AIGateway")); const CompletionHistory = lazy( () => import("@/pages/tenant/CompletionHistory"), ); @@ -51,6 +51,13 @@ const PromptTestCases = lazy(() => import("@/pages/tenant/PromptTestCases")); const PromptTestCaseCreate = lazy( () => import("@/pages/tenant/PromptTestCaseCreate"), ); +const TenantAIProviders = lazy( + () => import("@/pages/tenant/TenantAIProviders"), +); +const TenantAIProviderCreate = lazy( + () => import("@/pages/tenant/TenantAIProviderCreate"), +); +const TenantAIDashboard = lazy(() => import("@/pages/tenant/TenantAIDashboard")); // Loading fallback component const RouteLoader = (): ReactElement => ( @@ -197,9 +204,17 @@ export const tenantAdminRoutes: RouteConfig[] = [ path: "/tenant/ai/completions/:completionId", element: , }, + // { + // path: "/tenant/ai/config", + // element: , + // }, { - path: "/tenant/ai/config", - element: , + path: "/tenant/ai/providers", + element: , + }, + { + path: "/tenant/ai/providers/create", + element: , }, { path: "/tenant/ai/prompts", @@ -226,7 +241,11 @@ export const tenantAdminRoutes: RouteConfig[] = [ element: , }, { - path: "/tenant/ai/knowledge", - element: , + path: "/tenant/ai/dashboard", + element: , }, + // { + // path: "/tenant/ai/knowledge", + // element: , + // }, ]; diff --git a/src/services/ai-service.ts b/src/services/ai-service.ts index f6651c9..f8cfaa5 100644 --- a/src/services/ai-service.ts +++ b/src/services/ai-service.ts @@ -114,6 +114,11 @@ class AIService { return unwrap(response); } + async getConfig(provider: string): Promise { + const response = await apiClient.get(`/ai/config/${encodeURIComponent(provider)}`); + return unwrap(response); + } + async testConfig(provider: string): Promise { const response = await apiClient.post(`/ai/config/${encodeURIComponent(provider)}/test`, {}); return unwrap(response);