feat: implement AI service and pages for managing tenant AI providers and configurations

This commit is contained in:
sibarchannayak 2026-05-25 18:02:58 +05:30
parent fd6436e389
commit 23c32409ed
5 changed files with 275 additions and 132 deletions

38
package-lock.json generated
View File

@ -87,6 +87,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@ -921,17 +922,6 @@
"@floating-ui/utils": "^0.2.11" "@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": { "node_modules/@floating-ui/utils": {
"version": "0.2.11", "version": "0.2.11",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", "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", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.4.tgz",
"integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==", "integrity": "sha512-3i/DG89TFY/b34T5P+j35UcjYuB5d3+9K8u6qID+iUqNPiza015HPIZLuPfE5elNwVdV3EXIoPo0LLeBLgXXAg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
@ -1986,6 +1977,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.4.tgz",
"integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==", "integrity": "sha512-X+5plTKhOioNcQ4KsAFJJSb/3+zR8Xhdpow4HzXtoV1KcbdDey1fhZdpsfkbrzCL0s6/wAgwZuAchCK7HujurQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "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", "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-3.20.4.tgz",
"integrity": "sha512-PvW0Ja7ahWpo4bRuR8YCCVv4PH8lXjzhzlBAa4bMbsumOg+GbhX8Su7fwqd+IIPrHqfPXz9HTBMApSfzP6/08A==", "integrity": "sha512-PvW0Ja7ahWpo4bRuR8YCCVv4PH8lXjzhzlBAa4bMbsumOg+GbhX8Su7fwqd+IIPrHqfPXz9HTBMApSfzP6/08A==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
@ -2130,6 +2123,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.4.tgz",
"integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==", "integrity": "sha512-8p6hVT65DjuQjtEdlH6ewX9SOJHlVQAOee3sWIJQmeJNRnZNvqPIBLleebUqDiljNTpxBv6s6QWkSTKgf3btwg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
@ -2144,6 +2138,7 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.4.tgz",
"integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==", "integrity": "sha512-rCHYSBToilBEuI6PtjziHDdRkABH/XqwJ7dG4Amn/SD3yGiZKYCiEApQlTUS2zZeo8DsLeuqqqB4vEOeD4OEPg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"prosemirror-changeset": "^2.3.0", "prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1", "prosemirror-collab": "^1.3.1",
@ -2381,6 +2376,7 @@
"integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@ -2390,6 +2386,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz",
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@ -2399,6 +2396,7 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -2454,6 +2452,7 @@
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/scope-manager": "8.54.0",
"@typescript-eslint/types": "8.54.0", "@typescript-eslint/types": "8.54.0",
@ -2705,6 +2704,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -2826,6 +2826,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@ -3379,6 +3380,7 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -4582,6 +4584,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -4739,6 +4742,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"orderedmap": "^2.0.0" "orderedmap": "^2.0.0"
} }
@ -4768,6 +4772,7 @@
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"prosemirror-model": "^1.0.0", "prosemirror-model": "^1.0.0",
"prosemirror-transform": "^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", "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.7.tgz",
"integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==", "integrity": "sha512-jUwKNCEIGiqdvhlS91/2QAg21e4dfU5bH2iwmSDQeosXJgKF7smG0YSplOWK0cjSNgIqXe7VXqo7EIfUFJdt3w==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"prosemirror-model": "^1.20.0", "prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0", "prosemirror-state": "^1.0.0",
@ -4852,6 +4858,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -4861,6 +4868,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -4873,6 +4881,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@ -4896,6 +4905,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@ -5006,7 +5016,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-persist": { "node_modules/redux-persist": {
"version": "6.0.0", "version": "6.0.0",
@ -5303,6 +5314,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -5425,6 +5437,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.27.0", "esbuild": "^0.27.0",
"fdir": "^6.5.0", "fdir": "^6.5.0",
@ -5580,6 +5593,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -15,6 +15,7 @@ import {
BadgeCheck, BadgeCheck,
GitBranch, GitBranch,
Zap, Zap,
Brain,
} from "lucide-react"; } from "lucide-react";
import { Layout } from "@/components/layout/Layout"; import { Layout } from "@/components/layout/Layout";
import { import {
@ -40,6 +41,7 @@ import type { MyModule } from "@/types/module";
import { formatDate } from "@/utils/format-date"; 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 DepartmentsTable from "@/components/superadmin/DepartmentsTable"; // import DepartmentsTable from "@/components/superadmin/DepartmentsTable";
import DesignationsTable from "@/components/superadmin/DesignationsTable"; import DesignationsTable from "@/components/superadmin/DesignationsTable";
@ -56,7 +58,8 @@ type TabType =
| "settings" | "settings"
| "license" | "license"
| "audit-logs" | "audit-logs"
| "billing"; | "billing"
| "ai-providers";
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" /> },
@ -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: "suppliers", label: "Suppliers", icon: <Users className="w-4 h-4" /> },
{ 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: "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",
@ -374,6 +378,9 @@ const TenantDetails = (): ReactElement => {
{activeTab === "settings" && id && ( {activeTab === "settings" && id && (
<TenantSettings customTenantId={id} hideLayout={true} /> <TenantSettings customTenantId={id} hideLayout={true} />
)} )}
{activeTab === "ai-providers" && id && (
<TenantAIProviders 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} />

View File

@ -7,7 +7,7 @@ import {
SecondaryButton, SecondaryButton,
FormTagInput, FormTagInput,
} from "@/components/shared"; } from "@/components/shared";
import { ArrowLeft } from "lucide-react"; // import { ArrowLeft } from "lucide-react";
import { useForm, Controller } from "react-hook-form"; import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
@ -29,7 +29,19 @@ const createConfigSchema = z.object({
custom_embedding_models: z.array(z.string()).default([]), 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 navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
@ -75,10 +87,14 @@ export const TenantAIProviderCreate = (): ReactElement => {
custom_models: data.custom_models || [], custom_models: data.custom_models || [],
default_embedding_model: data.default_embedding_model || undefined, default_embedding_model: data.default_embedding_model || undefined,
custom_embedding_models: data.custom_embedding_models || [], custom_embedding_models: data.custom_embedding_models || [],
} as any); } as any, customTenantId);
showToast.success("AI Provider configuration created successfully!"); showToast.success("AI Provider configuration created successfully!");
if (onSuccess) {
onSuccess();
} else {
navigate("/tenant/ai/providers"); navigate("/tenant/ai/providers");
}
} catch (err: any) { } catch (err: any) {
if (err?.response?.data?.details && Array.isArray(err.response.data.details)) { if (err?.response?.data?.details && Array.isArray(err.response.data.details)) {
err.response.data.details.forEach((detail: any) => { err.response.data.details.forEach((detail: any) => {
@ -97,42 +113,26 @@ export const TenantAIProviderCreate = (): ReactElement => {
} }
}; };
return ( const formContent = (
<Layout <>
currentPage="AI Gateway Services" {/* <div className="mb-4">
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">
<button <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" 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" /> <ArrowLeft className="w-3.5 h-3.5" />
Back to AI Providers List Back to AI Providers List
</button> </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 */} {/* General Settings Section */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm p-6 space-y-4"> <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"> <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>
</div> </div>
</form> </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> </Layout>
); );
}; };

View File

@ -18,9 +18,19 @@ import { showToast } from "@/utils/toast";
import { formatDate } from "@/utils/format-date"; import { formatDate } from "@/utils/format-date";
import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal"; import { ViewAIProviderModal } from "@/components/tenant/ViewAIProviderModal";
import CodeBadge from "@/components/shared/CodeBadge"; 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 navigate = useNavigate();
const [view, setView] = useState<"list" | "create">("list");
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [configs, setConfigs] = useState<TenantAIConfig[]>([]); const [configs, setConfigs] = useState<TenantAIConfig[]>([]);
@ -42,7 +52,7 @@ export const TenantAIProviders = (): ReactElement => {
setIsLoading(true); setIsLoading(true);
setError(null); setError(null);
try { try {
const data = await aiService.listConfigs(); const data = await aiService.listConfigs(customTenantId);
setConfigs(data || []); setConfigs(data || []);
} catch (err: any) { } catch (err: any) {
const msg = const msg =
@ -56,12 +66,12 @@ export const TenantAIProviders = (): ReactElement => {
useEffect(() => { useEffect(() => {
void fetchConfigs(); void fetchConfigs();
}, []); }, [customTenantId]);
const handleTestConnection = async (provider: string) => { const handleTestConnection = async (provider: string) => {
setTestingProviders((prev) => ({ ...prev, [provider]: true })); setTestingProviders((prev) => ({ ...prev, [provider]: true }));
try { try {
const resp = await aiService.testConfig(provider); const resp = await aiService.testConfig(provider, customTenantId);
if (resp && resp.healthy) { if (resp && resp.healthy) {
showToast.success( showToast.success(
`Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms`, `Connection healthy for ${provider}! Latency: ${resp.latency_ms || "N/A"} ms`,
@ -80,7 +90,7 @@ export const TenantAIProviders = (): ReactElement => {
const handleViewConfig = async (provider: string) => { const handleViewConfig = async (provider: string) => {
try { try {
const cfg = await aiService.getConfig(provider); const cfg = await aiService.getConfig(provider, customTenantId);
setSelectedConfig(cfg); setSelectedConfig(cfg);
setIsViewModalOpen(true); setIsViewModalOpen(true);
} catch (err: any) { } catch (err: any) {
@ -100,7 +110,7 @@ export const TenantAIProviders = (): ReactElement => {
if (!providerToDelete) return; if (!providerToDelete) return;
setIsDeleting(true); setIsDeleting(true);
try { try {
await aiService.deleteConfig(providerToDelete); await aiService.deleteConfig(providerToDelete, customTenantId);
showToast.success(`${providerToDelete} config removed successfully`); showToast.success(`${providerToDelete} config removed successfully`);
void fetchConfigs(); void fetchConfigs();
setIsDeleteModalOpen(false); setIsDeleteModalOpen(false);
@ -240,23 +250,21 @@ export const TenantAIProviders = (): ReactElement => {
[testingProviders], [testingProviders],
); );
if (view === "create") {
return ( return (
<Layout <TenantAIProviderCreate
currentPage="AI Gateway Services" customTenantId={customTenantId}
pageHeader={{ hideLayout={true}
title: "Tenant AI Providers List", onCancel={() => setView("list")}
description: "Manage tenant API keys and models for AI integrations.", onSuccess={() => {
action: ( setView("list");
<PrimaryButton void fetchConfigs();
onClick={() => navigate("/tenant/ai/providers/create")}
className="h-10 px-4 flex items-center gap-1.5"
>
<Plus className="w-4 h-4" />
Create AI Provider
</PrimaryButton>
),
}} }}
> />
);
}
const listContent = (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{/* Subhead Toolbar matching Screenshot filter design */} {/* 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-col sm:flex-row sm:items-center justify-between gap-4">
@ -288,6 +296,16 @@ export const TenantAIProviders = (): ReactElement => {
</button> </button>
)} )}
</div> </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> </div>
{/* Table list */} {/* Table list */}
@ -299,7 +317,6 @@ export const TenantAIProviders = (): ReactElement => {
error={error} error={error}
emptyMessage="No tenant AI providers configured." emptyMessage="No tenant AI providers configured."
/> />
</div>
<ViewAIProviderModal <ViewAIProviderModal
isOpen={isViewModalOpen} isOpen={isViewModalOpen}
@ -316,6 +333,35 @@ export const TenantAIProviders = (): ReactElement => {
itemName={providerToDelete || ""} itemName={providerToDelete || ""}
isLoading={isDeleting} isLoading={isDeleting}
/> />
</div>
);
if (hideLayout) {
return (
<div className="space-y-4">
{listContent}
</div>
);
}
return (
<Layout
currentPage="AI Gateway Services"
pageHeader={{
title: "Tenant AI Providers List",
description: "Manage tenant API keys and models for AI integrations.",
action: (
<PrimaryButton
onClick={() => navigate("/tenant/ai/providers/create")}
className="h-10 px-4 flex items-center gap-1.5"
>
<Plus className="w-4 h-4" />
Create AI Provider
</PrimaryButton>
),
}}
>
{listContent}
</Layout> </Layout>
); );
}; };

View File

@ -93,7 +93,8 @@ class AIService {
return unwrap<AICostSummary>(response); return unwrap<AICostSummary>(response);
} }
async upsertConfig(payload: { async upsertConfig(
payload: {
provider: string; provider: string;
config_type: "azure" | "direct"; config_type: "azure" | "direct";
api_key: string; api_key: string;
@ -104,28 +105,35 @@ class AIService {
custom_models?: string[]; custom_models?: string[];
default_model?: string; default_model?: string;
is_active?: boolean; is_active?: boolean;
}): Promise<TenantAIConfig> { },
const response = await apiClient.post("/ai/config", payload); 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); return unwrap<TenantAIConfig>(response);
} }
async listConfigs(): Promise<TenantAIConfig[]> { async listConfigs(tenantId?: string): Promise<TenantAIConfig[]> {
const response = await apiClient.get("/ai/config"); const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
const response = await apiClient.get("/ai/config", { headers });
return unwrap<TenantAIConfig[]>(response); return unwrap<TenantAIConfig[]>(response);
} }
async getConfig(provider: string): Promise<TenantAIConfig> { async getConfig(provider: string, tenantId?: string): Promise<TenantAIConfig> {
const response = await apiClient.get(`/ai/config/${encodeURIComponent(provider)}`); const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
const response = await apiClient.get(`/ai/config/${encodeURIComponent(provider)}`, { headers });
return unwrap<TenantAIConfig>(response); return unwrap<TenantAIConfig>(response);
} }
async testConfig(provider: string): Promise<AIHealthResponse> { async testConfig(provider: string, tenantId?: string): Promise<AIHealthResponse> {
const response = await apiClient.post(`/ai/config/${encodeURIComponent(provider)}/test`, {}); const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
const response = await apiClient.post(`/ai/config/${encodeURIComponent(provider)}/test`, {}, { headers });
return unwrap<AIHealthResponse>(response); return unwrap<AIHealthResponse>(response);
} }
async deleteConfig(provider: string): Promise<void> { async deleteConfig(provider: string, tenantId?: string): Promise<void> {
await apiClient.delete(`/ai/config/${encodeURIComponent(provider)}`); const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
await apiClient.delete(`/ai/config/${encodeURIComponent(provider)}`, { headers });
} }
async createPrompt(payload: { async createPrompt(payload: {