feat: implement AI service and pages for managing tenant AI providers and configurations
This commit is contained in:
parent
fd6436e389
commit
23c32409ed
38
package-lock.json
generated
38
package-lock.json
generated
@ -87,6 +87,7 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@ -921,17 +922,6 @@
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||
@ -1725,6 +1715,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
|
||||
"integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@ -1986,6 +1977,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz",
|
||||
"integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@ -2104,6 +2096,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.4.tgz",
|
||||
"integrity": "sha512-PvW0Ja7ahWpo4bRuR8YCCVv4PH8lXjzhzlBAa4bMbsumOg+GbhX8Su7fwqd+IIPrHqfPXz9HTBMApSfzP6/08A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@ -2130,6 +2123,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz",
|
||||
"integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@ -2144,6 +2138,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
|
||||
"integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
@ -2381,6 +2376,7 @@
|
||||
"integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@ -2390,6 +2386,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
|
||||
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@ -2399,6 +2396,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@ -2454,6 +2452,7 @@
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
@ -2705,6 +2704,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -2826,6 +2826,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@ -3379,6 +3380,7 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -4582,6 +4584,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -4739,6 +4742,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
@ -4768,6 +4772,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
@ -4816,6 +4821,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz",
|
||||
"integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
@ -4852,6 +4858,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -4861,6 +4868,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@ -4873,6 +4881,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
||||
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
@ -4896,6 +4905,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@ -5006,7 +5016,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-persist": {
|
||||
"version": "6.0.0",
|
||||
@ -5303,6 +5314,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -5425,6 +5437,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@ -5580,6 +5593,7 @@
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
BadgeCheck,
|
||||
GitBranch,
|
||||
Zap,
|
||||
Brain,
|
||||
} from "lucide-react";
|
||||
import { Layout } from "@/components/layout/Layout";
|
||||
import {
|
||||
@ -40,6 +41,7 @@ import type { MyModule } from "@/types/module";
|
||||
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 DepartmentsTable from "@/components/superadmin/DepartmentsTable";
|
||||
import DesignationsTable from "@/components/superadmin/DesignationsTable";
|
||||
|
||||
@ -56,7 +58,8 @@ type TabType =
|
||||
| "settings"
|
||||
| "license"
|
||||
| "audit-logs"
|
||||
| "billing";
|
||||
| "billing"
|
||||
| "ai-providers";
|
||||
|
||||
const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [
|
||||
{ id: "overview", label: "Overview", icon: <FileText className="w-4 h-4" /> },
|
||||
@ -85,6 +88,7 @@ const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [
|
||||
{ id: "suppliers", label: "Suppliers", icon: <Users 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: "ai-providers", label: "AI Providers", icon: <Brain className="w-4 h-4" /> },
|
||||
{ id: "license", label: "License", icon: <FileText className="w-4 h-4" /> },
|
||||
{
|
||||
id: "audit-logs",
|
||||
@ -374,6 +378,9 @@ const TenantDetails = (): ReactElement => {
|
||||
{activeTab === "settings" && id && (
|
||||
<TenantSettings customTenantId={id} hideLayout={true} />
|
||||
)}
|
||||
{activeTab === "ai-providers" && id && (
|
||||
<TenantAIProviders customTenantId={id} hideLayout={true} />
|
||||
)}
|
||||
{activeTab === "license" && <LicenseTab tenant={tenant} />}
|
||||
{activeTab === "audit-logs" && id && (
|
||||
<AuditLogs customTenantId={id} hideLayout={true} />
|
||||
|
||||
@ -7,7 +7,7 @@ import {
|
||||
SecondaryButton,
|
||||
FormTagInput,
|
||||
} from "@/components/shared";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
// import { ArrowLeft } from "lucide-react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
@ -29,7 +29,19 @@ const createConfigSchema = z.object({
|
||||
custom_embedding_models: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export const TenantAIProviderCreate = (): ReactElement => {
|
||||
interface TenantAIProviderCreateProps {
|
||||
customTenantId?: string;
|
||||
hideLayout?: boolean;
|
||||
onCancel?: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export const TenantAIProviderCreate = ({
|
||||
customTenantId,
|
||||
hideLayout = false,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}: TenantAIProviderCreateProps = {}): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
|
||||
@ -75,10 +87,14 @@ export const TenantAIProviderCreate = (): ReactElement => {
|
||||
custom_models: data.custom_models || [],
|
||||
default_embedding_model: data.default_embedding_model || undefined,
|
||||
custom_embedding_models: data.custom_embedding_models || [],
|
||||
} as any);
|
||||
} as any, customTenantId);
|
||||
|
||||
showToast.success("AI Provider configuration created successfully!");
|
||||
navigate("/tenant/ai/providers");
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
} else {
|
||||
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) => {
|
||||
@ -97,42 +113,26 @@ export const TenantAIProviderCreate = (): ReactElement => {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
currentPage="AI Gateway Services"
|
||||
pageHeader={{
|
||||
title: "AI Provider Configuration",
|
||||
description: "Manage and reuse prompts for different use cases.",
|
||||
action: (
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton
|
||||
onClick={() => navigate("/tenant/ai/providers")}
|
||||
className="h-10 px-5 min-w-[120px]"
|
||||
>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
disabled={isSubmitting}
|
||||
className="h-10 px-5 min-w-[120px]"
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save Configuration"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className="mb-4">
|
||||
const formContent = (
|
||||
<>
|
||||
{/* <div className="mb-4">
|
||||
<button
|
||||
onClick={() => navigate("/tenant/ai/providers")}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
} else {
|
||||
navigate("/tenant/ai/providers");
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-800 transition-colors font-medium select-none cursor-pointer"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
Back to AI Providers List
|
||||
</button>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-6 max-w-4xl select-none">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-6 ">
|
||||
{/* General Settings Section */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm p-6 space-y-4">
|
||||
<h2 className="text-base font-semibold text-slate-800 select-none border-b border-slate-100 pb-2">
|
||||
@ -370,6 +370,74 @@ export const TenantAIProviderCreate = (): ReactElement => {
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
if (hideLayout) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 pb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-800">AI Provider Configuration</h2>
|
||||
<p className="text-xs text-slate-500">Add or update API credentials and models for a tenant.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton
|
||||
onClick={() => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
} else {
|
||||
navigate("/tenant/ai/providers");
|
||||
}
|
||||
}}
|
||||
className="h-10 px-5 min-w-[120px]"
|
||||
>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
disabled={isSubmitting}
|
||||
className="h-10 px-5 min-w-[120px]"
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save Configuration"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
{formContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
currentPage="AI Gateway Services"
|
||||
breadcrumbs={[
|
||||
{ label: "AI Gateway Services", path: "/tenant/ai/providers" },
|
||||
{ label: "Configuration" },
|
||||
]}
|
||||
pageHeader={{
|
||||
title: "AI Provider Configuration",
|
||||
description: "Manage and reuse prompts for different use cases.",
|
||||
action: (
|
||||
<div className="flex items-center gap-2">
|
||||
<SecondaryButton
|
||||
onClick={() => navigate("/tenant/ai/providers")}
|
||||
className="h-10 px-5 min-w-[120px]"
|
||||
>
|
||||
Cancel
|
||||
</SecondaryButton>
|
||||
<PrimaryButton
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
disabled={isSubmitting}
|
||||
className="h-10 px-5 min-w-[120px]"
|
||||
>
|
||||
{isSubmitting ? "Saving..." : "Save Configuration"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{formContent}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -18,9 +18,19 @@ import { showToast } from "@/utils/toast";
|
||||
import { formatDate } from "@/utils/format-date";
|
||||
import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal";
|
||||
import CodeBadge from "@/components/shared/CodeBadge";
|
||||
import { TenantAIProviderCreate } from "./TenantAIProviderCreate";
|
||||
|
||||
export const TenantAIProviders = (): ReactElement => {
|
||||
interface TenantAIProvidersProps {
|
||||
customTenantId?: string;
|
||||
hideLayout?: boolean;
|
||||
}
|
||||
|
||||
export const TenantAIProviders = ({
|
||||
customTenantId,
|
||||
hideLayout = false,
|
||||
}: TenantAIProvidersProps = {}): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const [view, setView] = useState<"list" | "create">("list");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [configs, setConfigs] = useState<TenantAIConfig[]>([]);
|
||||
@ -42,7 +52,7 @@ export const TenantAIProviders = (): ReactElement => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await aiService.listConfigs();
|
||||
const data = await aiService.listConfigs(customTenantId);
|
||||
setConfigs(data || []);
|
||||
} catch (err: any) {
|
||||
const msg =
|
||||
@ -56,12 +66,12 @@ export const TenantAIProviders = (): ReactElement => {
|
||||
|
||||
useEffect(() => {
|
||||
void fetchConfigs();
|
||||
}, []);
|
||||
}, [customTenantId]);
|
||||
|
||||
const handleTestConnection = async (provider: string) => {
|
||||
setTestingProviders((prev) => ({ ...prev, [provider]: true }));
|
||||
try {
|
||||
const resp = await aiService.testConfig(provider);
|
||||
const resp = await aiService.testConfig(provider, customTenantId);
|
||||
if (resp && resp.healthy) {
|
||||
showToast.success(
|
||||
`Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms`,
|
||||
@ -80,7 +90,7 @@ export const TenantAIProviders = (): ReactElement => {
|
||||
|
||||
const handleViewConfig = async (provider: string) => {
|
||||
try {
|
||||
const cfg = await aiService.getConfig(provider);
|
||||
const cfg = await aiService.getConfig(provider, customTenantId);
|
||||
setSelectedConfig(cfg);
|
||||
setIsViewModalOpen(true);
|
||||
} catch (err: any) {
|
||||
@ -100,7 +110,7 @@ export const TenantAIProviders = (): ReactElement => {
|
||||
if (!providerToDelete) return;
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
await aiService.deleteConfig(providerToDelete);
|
||||
await aiService.deleteConfig(providerToDelete, customTenantId);
|
||||
showToast.success(`${providerToDelete} config removed successfully`);
|
||||
void fetchConfigs();
|
||||
setIsDeleteModalOpen(false);
|
||||
@ -240,6 +250,100 @@ export const TenantAIProviders = (): ReactElement => {
|
||||
[testingProviders],
|
||||
);
|
||||
|
||||
if (view === "create") {
|
||||
return (
|
||||
<TenantAIProviderCreate
|
||||
customTenantId={customTenantId}
|
||||
hideLayout={true}
|
||||
onCancel={() => setView("list")}
|
||||
onSuccess={() => {
|
||||
setView("list");
|
||||
void fetchConfigs();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const listContent = (
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Subhead Toolbar matching Screenshot filter design */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<SearchBox
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="Search Here"
|
||||
containerClassName="relative w-full sm:w-[280px]"
|
||||
/>
|
||||
|
||||
<FilterDropdown
|
||||
label="Status"
|
||||
value={statusFilter || null}
|
||||
onChange={(v) => setStatusFilter(typeof v === "string" ? v : "")}
|
||||
placeholder="All"
|
||||
options={[
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{(searchQuery || statusFilter) && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="text-xs text-slate-500 hover:text-red-500 font-medium transition-colors cursor-pointer select-none"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hideLayout && (
|
||||
<PrimaryButton
|
||||
onClick={() => setView("create")}
|
||||
className="h-10 px-4 flex items-center gap-1.5 shrink-0"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create AI Provider
|
||||
</PrimaryButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Table list */}
|
||||
<DataTable
|
||||
data={filteredConfigs}
|
||||
columns={columns}
|
||||
keyExtractor={(item) => item.id || item.provider}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
emptyMessage="No tenant AI providers configured."
|
||||
/>
|
||||
|
||||
<ViewAIProviderModal
|
||||
isOpen={isViewModalOpen}
|
||||
onClose={() => setIsViewModalOpen(false)}
|
||||
config={selectedConfig}
|
||||
/>
|
||||
|
||||
<DeleteConfirmationModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
title="Delete AI Provider"
|
||||
message="Are you sure you want to delete this AI provider configuration? This action cannot be undone."
|
||||
itemName={providerToDelete || ""}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (hideLayout) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{listContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
currentPage="AI Gateway Services"
|
||||
@ -257,65 +361,7 @@ export const TenantAIProviders = (): ReactElement => {
|
||||
),
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Subhead Toolbar matching Screenshot filter design */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<SearchBox
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder="Search Here"
|
||||
containerClassName="relative w-full sm:w-[280px]"
|
||||
/>
|
||||
|
||||
<FilterDropdown
|
||||
label="Status"
|
||||
value={statusFilter || null}
|
||||
onChange={(v) => setStatusFilter(typeof v === "string" ? v : "")}
|
||||
placeholder="All"
|
||||
options={[
|
||||
{ value: "active", label: "Active" },
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
]}
|
||||
/>
|
||||
|
||||
{(searchQuery || statusFilter) && (
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="text-xs text-slate-500 hover:text-red-500 font-medium transition-colors cursor-pointer select-none"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table list */}
|
||||
<DataTable
|
||||
data={filteredConfigs}
|
||||
columns={columns}
|
||||
keyExtractor={(item) => item.id || item.provider}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
emptyMessage="No tenant AI providers configured."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ViewAIProviderModal
|
||||
isOpen={isViewModalOpen}
|
||||
onClose={() => setIsViewModalOpen(false)}
|
||||
config={selectedConfig}
|
||||
/>
|
||||
|
||||
<DeleteConfirmationModal
|
||||
isOpen={isDeleteModalOpen}
|
||||
onClose={() => setIsDeleteModalOpen(false)}
|
||||
onConfirm={onConfirmDelete}
|
||||
title="Delete AI Provider"
|
||||
message="Are you sure you want to delete this AI provider configuration? This action cannot be undone."
|
||||
itemName={providerToDelete || ""}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
{listContent}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
@ -93,39 +93,47 @@ class AIService {
|
||||
return unwrap<AICostSummary>(response);
|
||||
}
|
||||
|
||||
async upsertConfig(payload: {
|
||||
provider: string;
|
||||
config_type: "azure" | "direct";
|
||||
api_key: string;
|
||||
display_name?: string;
|
||||
endpoint?: string;
|
||||
deployment?: string;
|
||||
api_version?: string;
|
||||
custom_models?: string[];
|
||||
default_model?: string;
|
||||
is_active?: boolean;
|
||||
}): Promise<TenantAIConfig> {
|
||||
const response = await apiClient.post("/ai/config", payload);
|
||||
async upsertConfig(
|
||||
payload: {
|
||||
provider: string;
|
||||
config_type: "azure" | "direct";
|
||||
api_key: string;
|
||||
display_name?: string;
|
||||
endpoint?: string;
|
||||
deployment?: string;
|
||||
api_version?: string;
|
||||
custom_models?: string[];
|
||||
default_model?: string;
|
||||
is_active?: boolean;
|
||||
},
|
||||
tenantId?: string
|
||||
): Promise<TenantAIConfig> {
|
||||
const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
|
||||
const response = await apiClient.post("/ai/config", payload, { headers });
|
||||
return unwrap<TenantAIConfig>(response);
|
||||
}
|
||||
|
||||
async listConfigs(): Promise<TenantAIConfig[]> {
|
||||
const response = await apiClient.get("/ai/config");
|
||||
async listConfigs(tenantId?: string): Promise<TenantAIConfig[]> {
|
||||
const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
|
||||
const response = await apiClient.get("/ai/config", { headers });
|
||||
return unwrap<TenantAIConfig[]>(response);
|
||||
}
|
||||
|
||||
async getConfig(provider: string): Promise<TenantAIConfig> {
|
||||
const response = await apiClient.get(`/ai/config/${encodeURIComponent(provider)}`);
|
||||
async getConfig(provider: string, tenantId?: string): Promise<TenantAIConfig> {
|
||||
const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
|
||||
const response = await apiClient.get(`/ai/config/${encodeURIComponent(provider)}`, { headers });
|
||||
return unwrap<TenantAIConfig>(response);
|
||||
}
|
||||
|
||||
async testConfig(provider: string): Promise<AIHealthResponse> {
|
||||
const response = await apiClient.post(`/ai/config/${encodeURIComponent(provider)}/test`, {});
|
||||
async testConfig(provider: string, tenantId?: string): Promise<AIHealthResponse> {
|
||||
const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
|
||||
const response = await apiClient.post(`/ai/config/${encodeURIComponent(provider)}/test`, {}, { headers });
|
||||
return unwrap<AIHealthResponse>(response);
|
||||
}
|
||||
|
||||
async deleteConfig(provider: string): Promise<void> {
|
||||
await apiClient.delete(`/ai/config/${encodeURIComponent(provider)}`);
|
||||
async deleteConfig(provider: string, tenantId?: string): Promise<void> {
|
||||
const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
|
||||
await apiClient.delete(`/ai/config/${encodeURIComponent(provider)}`, { headers });
|
||||
}
|
||||
|
||||
async createPrompt(payload: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user