diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/configuration/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/configuration/page.tsx
new file mode 100644
index 00000000..701ec125
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/configuration/page.tsx
@@ -0,0 +1,205 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useParams } from 'next/navigation';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Button, FormLayout, Text, TextField, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { useEditStatus } from '../../context';
+
+const FetchAIModelConfig: any = graphql(`
+ query AIModelConfig($filters: AIModelFilter) {
+ aiModels(filters: $filters) {
+ id
+ inputSchema
+ outputSchema
+ metadata
+ }
+ }
+`);
+
+const UpdateAIModelConfigMutation: any = graphql(`
+ mutation updateAIModelConfig($input: UpdateAIModelInput!) {
+ updateAiModel(input: $input) {
+ success
+ data {
+ id
+ inputSchema
+ outputSchema
+ metadata
+ }
+ }
+ }
+`);
+
+export default function ConfigurationPage() {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+
+ const { setStatus } = useEditStatus();
+
+ const ConfigData: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_AIModelConfig_${params.id}`],
+ () =>
+ GraphQL(
+ FetchAIModelConfig,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: parseInt(params.id),
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ }
+ );
+
+ const model = ConfigData.data?.aiModels[0];
+
+ const { mutate, isLoading: updateLoading } = useMutation(
+ (data: any) =>
+ GraphQL(
+ UpdateAIModelConfigMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ input: {
+ id: parseInt(params.id),
+ ...data,
+ },
+ }
+ ),
+ {
+ onSuccess: () => {
+ toast('Configuration updated successfully');
+ setStatus('saved');
+ ConfigData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ setStatus('unsaved');
+ },
+ }
+ );
+
+ const [formData, setFormData] = useState({
+ inputSchema: '{}',
+ outputSchema: '{}',
+ metadata: '{}',
+ });
+
+ useEffect(() => {
+ if (model) {
+ setFormData({
+ inputSchema: JSON.stringify(model.inputSchema || {}, null, 2),
+ outputSchema: JSON.stringify(model.outputSchema || {}, null, 2),
+ metadata: JSON.stringify(model.metadata || {}, null, 2),
+ });
+ }
+ }, [model]);
+
+ const handleInputChange = (field: string, value: string) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ setStatus('unsaved');
+ };
+
+ const handleSave = () => {
+ setStatus('saving');
+ try {
+ const updateData = {
+ inputSchema: JSON.parse(formData.inputSchema),
+ outputSchema: JSON.parse(formData.outputSchema),
+ metadata: JSON.parse(formData.metadata),
+ };
+ mutate(updateData);
+ } catch (error) {
+ toast('Invalid JSON format. Please check your input.');
+ setStatus('unsaved');
+ }
+ };
+
+ if (ConfigData.isLoading) {
+ return
Loading...
;
+ }
+
+ return (
+
+
+
+ Input Schema
+
+
+ Define the expected input format and parameters for the model. Use
+ JSON format.
+
+
+ handleInputChange('inputSchema', value)}
+ multiline={10}
+ helpText="Example: { 'prompt': 'string', 'max_tokens': 'number' }"
+ monospaced
+ />
+
+
+
+
+
+ Output Schema
+
+
+ Define the expected output format from the model. Use JSON format.
+
+
+ handleInputChange('outputSchema', value)}
+ multiline={10}
+ helpText="Example: { 'text': 'string', 'tokens_used': 'number' }"
+ monospaced
+ />
+
+
+
+
+
+ Additional Metadata
+
+
+ Store additional information about the model such as training data,
+ limitations, use cases, etc.
+
+
+ handleInputChange('metadata', value)}
+ multiline={10}
+ helpText="Example: { 'training_data': 'Description', 'limitations': ['List', 'of', 'limitations'] }"
+ monospaced
+ />
+
+
+
+
+
+ Save Configuration
+
+
+
+ );
+}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx
new file mode 100644
index 00000000..de4d1a0d
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx
@@ -0,0 +1,466 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useParams } from 'next/navigation';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import {
+ Combobox,
+ FormLayout,
+ Select,
+ Text,
+ TextField,
+ toast,
+} from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { useEditStatus } from '../../context';
+
+const tagsListQueryDoc: any = graphql(`
+ query TagsList {
+ tags {
+ id
+ value
+ }
+ }
+`);
+
+const sectorsListQueryDoc: any = graphql(`
+ query AIModelSectorsList {
+ sectors {
+ id
+ name
+ }
+ }
+`);
+
+const geographiesListQueryDoc: any = graphql(`
+ query AIModelGeographiesList {
+ geographies {
+ id
+ name
+ }
+ }
+`);
+
+const FetchAIModelDetails: any = graphql(`
+ query AIModelDetails($filters: AIModelFilter) {
+ aiModels(filters: $filters) {
+ id
+ name
+ displayName
+ description
+ version
+ modelType
+ provider
+ providerModelId
+ tags {
+ id
+ value
+ }
+ sectors {
+ id
+ name
+ }
+ geographies {
+ id
+ name
+ }
+ supportedLanguages
+ supportsStreaming
+ maxTokens
+ }
+ }
+`);
+
+const UpdateAIModelMutation: any = graphql(`
+ mutation updateAIModelDetails($input: UpdateAIModelInput!) {
+ updateAiModel(input: $input) {
+ success
+ data {
+ id
+ name
+ displayName
+ description
+ }
+ }
+ }
+`);
+
+export default function AIModelDetailsPage() {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+
+ const { setStatus } = useEditStatus();
+
+ const [formData, setFormData] = useState({
+ name: '',
+ displayName: '',
+ description: '',
+ version: '',
+ modelType: 'TEXT_GENERATION',
+ provider: 'CUSTOM',
+ providerModelId: '',
+ tags: [] as Array<{ label: string; value: string }>,
+ sectors: [] as Array<{ label: string; value: string }>,
+ geographies: [] as Array<{ label: string; value: string }>,
+ supportedLanguages: '',
+ supportsStreaming: false,
+ maxTokens: 0,
+ });
+
+ const [isTagsListUpdated, setIsTagsListUpdated] = useState(false);
+
+ const getTagsList: {
+ data: any;
+ isLoading: boolean;
+ error: any;
+ refetch: any;
+ } = useQuery([`tags_list_query`], () =>
+ GraphQL(
+ tagsListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {} as any
+ )
+ );
+
+ const getSectorsList: { data: any; isLoading: boolean; error: any } =
+ useQuery([`sectors_list_query`], () =>
+ GraphQL(
+ sectorsListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {} as any
+ )
+ );
+
+ const getGeographiesList: { data: any; isLoading: boolean; error: any } =
+ useQuery([`geographies_list_query`], () =>
+ GraphQL(
+ geographiesListQueryDoc,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {} as any
+ )
+ );
+
+ const AIModelData: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_AIModelDetails_${params.id}`],
+ () =>
+ GraphQL(
+ FetchAIModelDetails,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: parseInt(params.id),
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ }
+ );
+
+ const model = AIModelData.data?.aiModels[0];
+
+ const { mutate } = useMutation(
+ (data: any) =>
+ GraphQL(
+ UpdateAIModelMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ input: {
+ id: parseInt(params.id),
+ ...data,
+ },
+ }
+ ),
+ {
+ onSuccess: () => {
+ toast('AI Model updated successfully');
+ setStatus('saved');
+ if (isTagsListUpdated) {
+ getTagsList.refetch();
+ setIsTagsListUpdated(false);
+ }
+ AIModelData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ setStatus('unsaved');
+ },
+ }
+ );
+
+ useEffect(() => {
+ if (model) {
+ setFormData({
+ name: model.name || '',
+ displayName: model.displayName || '',
+ description: model.description || '',
+ version: model.version || '',
+ modelType: model.modelType || 'TEXT_GENERATION',
+ provider: model.provider || 'CUSTOM',
+ providerModelId: model.providerModelId || '',
+ tags: model.tags?.map((tag: any) => ({
+ label: tag.value,
+ value: tag.id,
+ })) || [],
+ sectors: model.sectors?.map((sector: any) => ({
+ label: sector.name,
+ value: sector.id,
+ })) || [],
+ geographies: model.geographies?.map((geography: any) => ({
+ label: geography.name,
+ value: geography.id,
+ })) || [],
+ supportedLanguages: model.supportedLanguages?.join(', ') || '',
+ supportsStreaming: model.supportsStreaming || false,
+ maxTokens: model.maxTokens || 0,
+ });
+ }
+ }, [model]);
+
+ const handleInputChange = (field: string, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ setStatus('unsaved');
+ };
+
+ const handleSave = (overrideData?: any) => {
+ setStatus('saving');
+ const dataToUse = overrideData || formData;
+ const updateData: any = {
+ name: dataToUse.name,
+ displayName: dataToUse.displayName,
+ description: dataToUse.description,
+ modelType: dataToUse.modelType,
+ provider: dataToUse.provider,
+ version: dataToUse.version,
+ providerModelId: dataToUse.providerModelId,
+ tags: dataToUse.tags.map((item: any) => item.label),
+ sectors: dataToUse.sectors.map((item: any) => item.label),
+ geographies: dataToUse.geographies.map((item: any) => item.label),
+ supportedLanguages: dataToUse.supportedLanguages
+ .split(',')
+ .map((l: string) => l.trim())
+ .filter(Boolean),
+ supportsStreaming: dataToUse.supportsStreaming,
+ maxTokens: parseInt(dataToUse.maxTokens.toString()) || 0,
+ };
+ mutate(updateData);
+ };
+
+ const modelTypeOptions = [
+ { label: 'Translation', value: 'TRANSLATION' },
+ { label: 'Text Generation', value: 'TEXT_GENERATION' },
+ { label: 'Summarization', value: 'SUMMARIZATION' },
+ { label: 'Question Answering', value: 'QUESTION_ANSWERING' },
+ { label: 'Sentiment Analysis', value: 'SENTIMENT_ANALYSIS' },
+ { label: 'Text Classification', value: 'TEXT_CLASSIFICATION' },
+ { label: 'Named Entity Recognition', value: 'NAMED_ENTITY_RECOGNITION' },
+ { label: 'Text to Speech', value: 'TEXT_TO_SPEECH' },
+ { label: 'Speech to Text', value: 'SPEECH_TO_TEXT' },
+ { label: 'Other', value: 'OTHER' },
+ ];
+
+ const providerOptions = [
+ { label: 'OpenAI', value: 'OPENAI' },
+ { label: 'Llama (Ollama)', value: 'LLAMA_OLLAMA' },
+ { label: 'Llama (Together AI)', value: 'LLAMA_TOGETHER' },
+ { label: 'Llama (Replicate)', value: 'LLAMA_REPLICATE' },
+ { label: 'Llama (Custom)', value: 'LLAMA_CUSTOM' },
+ { label: 'Custom API', value: 'CUSTOM' },
+ ];
+
+ if (AIModelData.isLoading) {
+ return Loading...
;
+ }
+
+ return (
+
+
+
+ Basic Information
+
+
+ handleInputChange('name', value)}
+ onBlur={() => handleSave()}
+ helpText="Unique identifier for the model (e.g., gpt-4-turbo)"
+ required
+ />
+ handleInputChange('displayName', value)}
+ onBlur={() => handleSave()}
+ helpText="Human-readable name for the model"
+ required
+ />
+ handleInputChange('description', value)}
+ onBlur={() => handleSave()}
+ multiline={4}
+ helpText="Describe the model's capabilities and use cases"
+ required
+ />
+ handleInputChange('version', value)}
+ onBlur={() => handleSave()}
+ helpText="Model version (e.g., 1.0, v2.5)"
+ />
+
+
+
+
+
+ Model Configuration
+
+
+ {
+ handleInputChange('modelType', value);
+ handleSave();
+ }}
+ required
+ />
+ {
+ handleInputChange('provider', value);
+ handleSave();
+ }}
+ required
+ />
+ handleInputChange('providerModelId', value)}
+ onBlur={() => handleSave()}
+ helpText="Provider's model identifier (e.g., gpt-4, claude-3-opus)"
+ />
+
+
+
+
+
+ Capabilities
+
+
+ ({
+ label: item.value,
+ value: item.id,
+ })) || []
+ }
+ key={`tags-${getTagsList.data?.tags?.length || 0}`}
+ label="Tags *"
+ creatable
+ selectedValue={formData.tags}
+ onChange={(value) => {
+ setIsTagsListUpdated(true);
+ handleInputChange('tags', value);
+ handleSave({ ...formData, tags: value });
+ }}
+ />
+ ({
+ label: item.name,
+ value: item.id,
+ })) || []
+ }
+ key={`sectors-${getSectorsList.data?.sectors?.length || 0}`}
+ label="Sectors"
+ selectedValue={formData.sectors}
+ onChange={(value) => {
+ handleInputChange('sectors', value);
+ handleSave({ ...formData, sectors: value });
+ }}
+ />
+ ({
+ label: item.name,
+ value: item.id,
+ })) || []
+ }
+ key={`geographies-${getGeographiesList.data?.geographies?.length || 0}`}
+ label="Geographies"
+ selectedValue={formData.geographies}
+ onChange={(value) => {
+ handleInputChange('geographies', value);
+ handleSave({ ...formData, geographies: value });
+ }}
+ />
+ handleInputChange('supportedLanguages', value)}
+ onBlur={() => handleSave()}
+ helpText="Comma-separated language codes (e.g., en, es, fr)"
+ />
+ handleInputChange('maxTokens', value)}
+ onBlur={() => handleSave()}
+ helpText="Maximum number of tokens the model can process"
+ />
+
+ {
+ handleInputChange('supportsStreaming', e.target.checked);
+ handleSave();
+ }}
+ className="h-4 w-4"
+ />
+ Supports Streaming
+
+
+
+
+
+ );
+}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/endpoints/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/endpoints/page.tsx
new file mode 100644
index 00000000..c2d90c26
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/endpoints/page.tsx
@@ -0,0 +1,433 @@
+'use client';
+
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { useParams } from 'next/navigation';
+import {
+ Button,
+ DataTable,
+ Dialog,
+ FormLayout,
+ Icon,
+ IconButton,
+ Select,
+ Text,
+ TextField,
+ toast,
+} from 'opub-ui';
+import { useState } from 'react';
+
+import { Icons } from '@/components/icons';
+import { GraphQL } from '@/lib/api';
+
+const FetchModelEndpoints: any = graphql(`
+ query ModelEndpoints($filters: AIModelFilter) {
+ aiModels(filters: $filters) {
+ id
+ endpoints {
+ id
+ url
+ httpMethod
+ authType
+ authHeaderName
+ isPrimary
+ isActive
+ timeoutSeconds
+ maxRetries
+ }
+ }
+ }
+`);
+
+const CreateEndpointMutation: any = graphql(`
+ mutation createEndpoint($input: CreateModelEndpointInput!) {
+ createModelEndpoint(input: $input) {
+ success
+ }
+ }
+`);
+
+const UpdateEndpointMutation: any = graphql(`
+ mutation updateEndpoint($input: UpdateModelEndpointInput!) {
+ updateModelEndpoint(input: $input) {
+ success
+ }
+ }
+`);
+
+const DeleteEndpointMutation: any = graphql(`
+ mutation deleteEndpoint($endpointId: Int!) {
+ deleteModelEndpoint(endpointId: $endpointId) {
+ success
+ }
+ }
+`);
+
+export default function EndpointsPage() {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [editingEndpoint, setEditingEndpoint] = useState(null);
+
+ const EndpointsData: { data: any; isLoading: boolean; refetch: any } =
+ useQuery(
+ [`fetch_ModelEndpoints_${params.id}`],
+ () =>
+ GraphQL(
+ FetchModelEndpoints,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: parseInt(params.id),
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ }
+ );
+
+ const endpoints = EndpointsData.data?.aiModels[0]?.endpoints || [];
+
+ const { mutate: createEndpoint, isLoading: createLoading } = useMutation(
+ (data: any) =>
+ GraphQL(
+ CreateEndpointMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ input: {
+ modelId: parseInt(params.id),
+ ...data,
+ },
+ }
+ ),
+ {
+ onSuccess: () => {
+ toast('Endpoint created successfully');
+ setIsModalOpen(false);
+ EndpointsData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const { mutate: updateEndpoint, isLoading: updateLoading } = useMutation(
+ (data: any) =>
+ GraphQL(
+ UpdateEndpointMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ input: data,
+ }
+ ),
+ {
+ onSuccess: () => {
+ toast('Endpoint updated successfully');
+ setIsModalOpen(false);
+ setEditingEndpoint(null);
+ EndpointsData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const { mutate: deleteEndpoint } = useMutation(
+ (id: number) =>
+ GraphQL(
+ DeleteEndpointMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ { endpointId: id }
+ ),
+ {
+ onSuccess: () => {
+ toast('Endpoint deleted successfully');
+ EndpointsData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const [formData, setFormData] = useState({
+ url: '',
+ httpMethod: 'POST',
+ authType: 'BEARER',
+ authHeaderName: 'Authorization',
+ isPrimary: false,
+ isActive: true,
+ timeoutSeconds: 30,
+ maxRetries: 3,
+ });
+
+ const handleOpenModal = (endpoint?: any) => {
+ if (endpoint) {
+ setEditingEndpoint(endpoint);
+ setFormData({
+ url: endpoint.url,
+ httpMethod: endpoint.httpMethod,
+ authType: endpoint.authType,
+ authHeaderName: endpoint.authHeaderName,
+ isPrimary: endpoint.isPrimary,
+ isActive: endpoint.isActive,
+ timeoutSeconds: endpoint.timeoutSeconds,
+ maxRetries: endpoint.maxRetries,
+ });
+ } else {
+ setEditingEndpoint(null);
+ setFormData({
+ url: '',
+ httpMethod: 'POST',
+ authType: 'BEARER',
+ authHeaderName: 'Authorization',
+ isPrimary: false,
+ isActive: true,
+ timeoutSeconds: 30,
+ maxRetries: 3,
+ });
+ }
+ setIsModalOpen(true);
+ };
+
+ const handleSave = () => {
+ if (editingEndpoint) {
+ updateEndpoint({
+ id: editingEndpoint.id,
+ ...formData,
+ });
+ } else {
+ createEndpoint(formData);
+ }
+ };
+
+ const httpMethodOptions = [
+ { label: 'GET', value: 'GET' },
+ { label: 'POST', value: 'POST' },
+ { label: 'PUT', value: 'PUT' },
+ ];
+
+ const authTypeOptions = [
+ { label: 'Bearer Token', value: 'BEARER' },
+ { label: 'API Key', value: 'API_KEY' },
+ { label: 'Basic Auth', value: 'BASIC' },
+ { label: 'OAuth 2.0', value: 'OAUTH2' },
+ { label: 'Custom Headers', value: 'CUSTOM' },
+ { label: 'No Authentication', value: 'NONE' },
+ ];
+
+ if (EndpointsData.isLoading) {
+ return Loading...
;
+ }
+
+ const endpointsColumns = [
+ {
+ accessorKey: 'url',
+ header: 'URL',
+ cell: ({ row }: any) => (
+
+ {row.original.url}
+
+ ),
+ },
+ {
+ accessorKey: 'httpMethod',
+ header: 'Method',
+ cell: ({ row }: any) => (
+
+ {row.original.httpMethod}
+
+ ),
+ },
+ {
+ accessorKey: 'authType',
+ header: 'Auth Type',
+ cell: ({ row }: any) => (
+ {row.original.authType}
+ ),
+ },
+ {
+ accessorKey: 'isPrimary',
+ header: 'Primary',
+ cell: ({ row }: any) =>
+ row.original.isPrimary ? (
+
+ ) : null,
+ },
+ {
+ accessorKey: 'isActive',
+ header: 'Active',
+ cell: ({ row }: any) =>
+ row.original.isActive ? (
+
+ ) : (
+
+ ),
+ },
+ {
+ accessorKey: 'actions',
+ header: 'Actions',
+ cell: ({ row }: any) => (
+
+ handleOpenModal(row.original)}
+ >
+ Edit
+
+ deleteEndpoint(row.original.id)}
+ >
+ Delete
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ API Endpoints
+
+ handleOpenModal()}>Add Endpoint
+
+
+ {endpoints.length > 0 ? (
+
+ ) : (
+
+
+
+ No endpoints configured yet
+
+ handleOpenModal()} className="mt-4">
+ Add Your First Endpoint
+
+
+ )}
+
+
+ {isModalOpen && (
+
+
+ setFormData({ ...formData, url: value })}
+ placeholder="https://api.example.com/v1/chat/completions"
+ required
+ />
+
+ setFormData({ ...formData, httpMethod: value })
+ }
+ />
+ setFormData({ ...formData, authType: value })}
+ />
+
+ setFormData({ ...formData, authHeaderName: value })
+ }
+ helpText="Header name for authentication (e.g., Authorization, X-API-Key)"
+ />
+
+ setFormData({ ...formData, timeoutSeconds: parseInt(value) || 30 })
+ }
+ />
+
+ setFormData({ ...formData, maxRetries: parseInt(value) || 3 })
+ }
+ />
+
+
+
+ setFormData({ ...formData, isPrimary: e.target.checked })
+ }
+ className="h-4 w-4"
+ />
+ Primary Endpoint
+
+
+
+ setFormData({ ...formData, isActive: e.target.checked })
+ }
+ className="h-4 w-4"
+ />
+ Active
+
+
+
+ setIsModalOpen(false)} kind="secondary">
+ Cancel
+
+
+ {editingEndpoint ? 'Update' : 'Create'}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/page.tsx
new file mode 100644
index 00000000..8998e2d5
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/page.tsx
@@ -0,0 +1,12 @@
+import { redirect } from 'next/navigation';
+
+export default function AIModelEditPage({
+ params,
+}: {
+ params: { entityType: string; entitySlug: string; id: string };
+}) {
+ // Redirect to the details page by default
+ redirect(
+ `/dashboard/${params.entityType}/${params.entitySlug}/aimodels/edit/${params.id}/details`
+ );
+}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx
new file mode 100644
index 00000000..9a9dd10f
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx
@@ -0,0 +1,318 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { useParams, useRouter } from 'next/navigation';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Button, FormLayout, Select, Text, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import { useEditStatus } from '../../context';
+
+const FetchAIModelStatus: any = graphql(`
+ query AIModelStatus($filters: AIModelFilter) {
+ aiModels(filters: $filters) {
+ id
+ name
+ displayName
+ description
+ status
+ isPublic
+ isActive
+ endpoints {
+ id
+ }
+ }
+ }
+`);
+
+const UpdateAIModelStatusMutation: any = graphql(`
+ mutation updateAIModelStatus($input: UpdateAIModelInput!) {
+ updateAiModel(input: $input) {
+ success
+ data {
+ id
+ status
+ isPublic
+ isActive
+ }
+ }
+ }
+`);
+
+export default function PublishPage() {
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+ const router = useRouter();
+ const { setStatus } = useEditStatus();
+
+ const StatusData: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_AIModelStatus_${params.id}`],
+ () =>
+ GraphQL(
+ FetchAIModelStatus,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: parseInt(params.id),
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ }
+ );
+
+ console.log('StatusData:', StatusData.data);
+ console.log('aiModels array:', StatusData.data?.aiModels);
+
+ const model = StatusData.data?.aiModels?.[0];
+
+ const { mutate, isLoading: updateLoading } = useMutation(
+ (data: any) =>
+ GraphQL(
+ UpdateAIModelStatusMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ input: {
+ id: parseInt(params.id),
+ ...data,
+ },
+ }
+ ),
+ {
+ onSuccess: () => {
+ toast('Model status updated successfully');
+ setStatus('saved');
+ StatusData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ setStatus('unsaved');
+ },
+ }
+ );
+
+ const [formData, setFormData] = useState({
+ status: 'REGISTERED',
+ isPublic: false,
+ isActive: true,
+ });
+
+ useEffect(() => {
+ if (model) {
+ setFormData({
+ status: model.status || 'REGISTERED',
+ isPublic: model.isPublic || false,
+ isActive: model.isActive !== undefined ? model.isActive : true,
+ });
+ }
+ }, [model]);
+
+ const handleInputChange = (field: string, value: any) => {
+ setFormData((prev) => ({ ...prev, [field]: value }));
+ setStatus('unsaved');
+ };
+
+ const handlePublish = () => {
+ setStatus('saving');
+ mutate(
+ {
+ status: 'ACTIVE',
+ isPublic: true,
+ isActive: true,
+ },
+ {
+ onSuccess: () => {
+ toast('Model published successfully');
+ router.push(`/dashboard/${params.entityType}/${params.entitySlug}/aimodels`);
+ },
+ }
+ );
+ };
+
+ const handleSave = () => {
+ setStatus('saving');
+ mutate(formData);
+ };
+
+ const statusOptions = [
+ { label: 'Registered', value: 'REGISTERED' },
+ { label: 'Validating', value: 'VALIDATING' },
+ { label: 'Active', value: 'ACTIVE' },
+ { label: 'Auditing', value: 'AUDITING' },
+ { label: 'Approved', value: 'APPROVED' },
+ { label: 'Flagged', value: 'FLAGGED' },
+ { label: 'Deprecated', value: 'DEPRECATED' },
+ ];
+
+ if (StatusData.isLoading) {
+ return Loading...
;
+ }
+
+ if (!model) {
+ return Model not found
;
+ }
+
+ console.log('Model data:', model);
+ console.log('Checklist checks:', {
+ hasName: !!model?.name,
+ hasDisplayName: !!model?.displayName,
+ hasDescription: !!model?.description,
+ hasEndpoints: model?.endpoints?.length > 0,
+ endpointsLength: model?.endpoints?.length
+ });
+
+ const isPublished = model?.status === 'ACTIVE' && model?.isPublic;
+
+ return (
+
+
+
+ Publication Status
+
+
+ {isPublished ? (
+
+
+ ✓ Model is Published and Active
+
+
+ Your AI model is now publicly accessible and can be discovered by
+ other users.
+
+
+ ) : (
+
+
+ Model is not published
+
+
+ Your AI model is currently in draft mode. Publish it to make it
+ available to other users.
+
+
+ )}
+
+
+ handleInputChange('status', value)}
+ helpText="Current status of the model in the lifecycle"
+ />
+
+
+
+
+
+
+
+ Publication Checklist
+
+
+
+
+
+ Model name and display name set
+
+ Basic information is required
+
+
+
+
+
+
+ Description provided
+
+ Help users understand your model's capabilities
+
+
+
+
+
0}
+ disabled
+ className="mt-1 h-4 w-4"
+ />
+
+ At least one endpoint configured
+
+ Required for model to be functional
+
+
+
+
+
+
+
+
+ Save Changes
+
+ {!isPublished && (
+
+ Publish Model
+
+ )}
+
+
+ );
+}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/context.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/context.tsx
new file mode 100644
index 00000000..2747111d
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/context.tsx
@@ -0,0 +1,34 @@
+'use client';
+
+import React, { createContext, useContext, useState } from 'react';
+
+type EditStatusContextType = {
+ status: 'saved' | 'unsaved' | 'saving';
+ setStatus: (status: 'saved' | 'unsaved' | 'saving') => void;
+};
+
+const EditStatusContext = createContext(
+ undefined
+);
+
+export const EditStatusProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const [status, setStatus] = useState<'saved' | 'unsaved' | 'saving'>('saved');
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useEditStatus = () => {
+ const context = useContext(EditStatusContext);
+ if (!context) {
+ throw new Error('useEditStatus must be used within EditStatusProvider');
+ }
+ return context;
+};
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/layout.tsx
new file mode 100644
index 00000000..47013f47
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/layout.tsx
@@ -0,0 +1,185 @@
+'use client';
+
+import { useParams, usePathname, useRouter } from 'next/navigation';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { Tab, TabList, Tabs, toast } from 'opub-ui';
+
+import { GraphQL } from '@/lib/api';
+import StepNavigation from '../../components/StepNavigation';
+import TitleBar from '../../components/title-bar';
+import { EditStatusProvider, useEditStatus } from './context';
+
+const UpdateAIModelNameMutation: any = graphql(`
+ mutation updateAIModelName($input: UpdateAIModelInput!) {
+ updateAiModel(input: $input) {
+ success
+ data {
+ id
+ displayName
+ }
+ }
+ }
+`);
+
+const FetchAIModelName: any = graphql(`
+ query AIModelName($filters: AIModelFilter) {
+ aiModels(filters: $filters) {
+ id
+ displayName
+ }
+ }
+`);
+
+const TabsAndChildren = ({ children }: { children: React.ReactNode }) => {
+ const router = useRouter();
+ const pathName = usePathname();
+ const params = useParams<{
+ entityType: string;
+ entitySlug: string;
+ id: string;
+ }>();
+
+ const layoutList = ['details', 'endpoints', 'configuration', 'publish'];
+
+ const pathItem = layoutList.find(function (v) {
+ return pathName.indexOf(v) >= 0;
+ });
+
+ const AIModelData: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_AIModelData`],
+ () =>
+ GraphQL(
+ FetchAIModelName,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ id: parseInt(params.id),
+ },
+ }
+ ),
+ {
+ refetchOnMount: true,
+ refetchOnReconnect: true,
+ }
+ );
+
+ const { mutate, isLoading: editMutationLoading } = useMutation(
+ (data: { displayName: string }) =>
+ GraphQL(
+ UpdateAIModelNameMutation,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ input: {
+ id: parseInt(params.id),
+ displayName: data.displayName,
+ },
+ }
+ ),
+ {
+ onSuccess: () => {
+ toast('AI Model updated successfully');
+ AIModelData.refetch();
+ },
+ onError: (error: any) => {
+ toast(`Error: ${error.message}`);
+ },
+ }
+ );
+
+ const links = [
+ {
+ label: 'Model Details',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/aimodels/edit/${params.id}/details`,
+ selected: pathItem === 'details',
+ },
+ {
+ label: 'Endpoints',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/aimodels/edit/${params.id}/endpoints`,
+ selected: pathItem === 'endpoints',
+ },
+ {
+ label: 'Configuration',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/aimodels/edit/${params.id}/configuration`,
+ selected: pathItem === 'configuration',
+ },
+ {
+ label: 'Publish',
+ url: `/dashboard/${params.entityType}/${params.entitySlug}/aimodels/edit/${params.id}/publish`,
+ selected: pathItem === 'publish',
+ },
+ ];
+
+ const handleTabClick = (url: string) => {
+ router.replace(url);
+ };
+
+ const initialTabLabel =
+ links.find((option) => option.selected)?.label || 'Model Details';
+
+ const { status, setStatus } = useEditStatus();
+
+ // Map our status to TitleBar's expected status
+ const titleBarStatus: 'loading' | 'success' =
+ status === 'saving' ? 'loading' : 'success';
+
+ const handleStatusChange = (s: 'loading' | 'success') => {
+ setStatus(s === 'loading' ? 'saving' : 'saved');
+ };
+
+ return (
+
+
mutate({ displayName: e })}
+ loading={editMutationLoading}
+ status={titleBarStatus}
+ setStatus={handleStatusChange}
+ />
+
+ handleTabClick(
+ links.find((link) => link.label === newValue)?.url || ''
+ )
+ }
+ >
+
+ {links.map((item, index) => (
+ handleTabClick(item.url)}
+ className="uppercase"
+ >
+ {item.label}
+
+ ))}
+
+
+ {children}
+
+
+
+
+ );
+};
+
+const EditAIModel = ({ children }: { children: React.ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default EditAIModel;
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/page.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/page.tsx
new file mode 100644
index 00000000..4bee0a5f
--- /dev/null
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/page.tsx
@@ -0,0 +1,300 @@
+'use client';
+
+import { useEffect } from 'react';
+import { useRouter } from 'next/navigation';
+import { graphql } from '@/gql';
+import { useMutation, useQuery } from '@tanstack/react-query';
+import { parseAsString, useQueryState } from 'next-usequerystate';
+import { Button, DataTable, Icon, IconButton, Text, toast } from 'opub-ui';
+import { twMerge } from 'tailwind-merge';
+
+import { GraphQL } from '@/lib/api';
+import { formatDate } from '@/lib/utils';
+import { Icons } from '@/components/icons';
+import { LinkButton } from '@/components/Link';
+import { Loading } from '@/components/loading';
+import { ActionBar } from '../dataset/components/action-bar';
+import { Navigation } from '../dataset/components/navigate-org-datasets';
+
+const allAIModels: any = graphql(`
+ query AIModelsData($filters: AIModelFilter, $order: AIModelOrder) {
+ aiModels(filters: $filters, order: $order) {
+ id
+ displayName
+ name
+ modelType
+ provider
+ status
+ createdAt
+ updatedAt
+ }
+ }
+`);
+
+const deleteAIModel: any = graphql(`
+ mutation deleteAIModel($modelId: Int!) {
+ deleteAiModel(modelId: $modelId) {
+ success
+ }
+ }
+`);
+
+const createAIModel: any = graphql(`
+ mutation CreateAIModel($input: CreateAIModelInput!) {
+ createAiModel(input: $input) {
+ success
+ data {
+ id
+ name
+ displayName
+ createdAt
+ }
+ }
+ }
+`);
+
+export default function AIModelsPage({
+ params,
+}: {
+ params: { entityType: string; entitySlug: string };
+}) {
+ const router = useRouter();
+
+ const [navigationTab, setNavigationTab] = useQueryState('tab', parseAsString);
+
+ let navigationOptions = [
+ {
+ label: 'Registered',
+ url: `registered`,
+ selected: navigationTab === 'registered',
+ },
+ {
+ label: 'Active',
+ url: `active`,
+ selected: navigationTab === 'active',
+ },
+ {
+ label: 'Approved',
+ url: `approved`,
+ selected: navigationTab === 'approved',
+ },
+ ];
+
+ const AllAIModels: { data: any; isLoading: boolean; refetch: any } = useQuery(
+ [`fetch_AIModels`, navigationTab],
+ () =>
+ GraphQL(
+ allAIModels,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ filters: {
+ status:
+ navigationTab === 'active'
+ ? 'ACTIVE'
+ : navigationTab === 'approved'
+ ? 'APPROVED'
+ : 'REGISTERED',
+ },
+ order: { updatedAt: 'DESC' },
+ }
+ )
+ );
+
+ useEffect(() => {
+ if (navigationTab === null || navigationTab === undefined)
+ setNavigationTab('registered');
+ AllAIModels.refetch();
+ }, [navigationTab]);
+
+ const DeleteAIModelMutation: {
+ mutate: any;
+ isLoading: boolean;
+ error: any;
+ } = useMutation(
+ [`delete_AIModel`],
+ (data: { id: number }) =>
+ GraphQL(
+ deleteAIModel,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ { modelId: data.id }
+ ),
+ {
+ onSuccess: () => {
+ toast(`Deleted AI Model successfully`);
+ AllAIModels.refetch();
+ },
+ onError: (err: any) => {
+ toast('Error: ' + err.message.split(':')[0]);
+ },
+ }
+ );
+
+ const CreateAIModel: {
+ mutate: any;
+ isLoading: boolean;
+ error: any;
+ } = useMutation(
+ [`create_AIModel`],
+ () =>
+ GraphQL(
+ createAIModel,
+ {
+ [params.entityType]: params.entitySlug,
+ },
+ {
+ input: {
+ name: 'new-model',
+ displayName: 'New Model',
+ description: 'A new AI model',
+ modelType: 'TEXT_GENERATION',
+ provider: 'CUSTOM',
+ },
+ }
+ ),
+ {
+ onSuccess: (response: any) => {
+ toast(`AI Model created successfully`);
+ router.push(
+ `/dashboard/${params.entityType}/${params.entitySlug}/aimodels/edit/${response.createAiModel.data.id}/details`
+ );
+ AllAIModels.refetch();
+ },
+ onError: (err: any) => {
+ toast('Error: ' + err.message.split(':')[0]);
+ },
+ }
+ );
+
+ const modelsListColumns = [
+ {
+ accessorKey: 'displayName',
+ header: 'Display Name',
+ cell: ({ row }: any) => (
+
+
+ {row.original.displayName}
+
+
+ ),
+ },
+ {
+ accessorKey: 'name',
+ header: 'Model Name',
+ cell: ({ row }: any) => (
+
+ {row.original.name}
+
+ ),
+ },
+ {
+ accessorKey: 'modelType',
+ header: 'Type',
+ cell: ({ row }: any) => (
+ {row.original.modelType}
+ ),
+ },
+ {
+ accessorKey: 'provider',
+ header: 'Provider',
+ cell: ({ row }: any) => (
+ {row.original.provider}
+ ),
+ },
+ { accessorKey: 'createdAt', header: 'Date Created' },
+ { accessorKey: 'updatedAt', header: 'Last Updated' },
+ {
+ accessorKey: 'delete',
+ header: 'Delete',
+ cell: ({ row }: any) => (
+ {
+ DeleteAIModelMutation.mutate({
+ id: row.original?.id,
+ });
+ }}
+ >
+ Delete
+
+ ),
+ },
+ ];
+
+ const generateTableData = (list: Array) => {
+ return list.map((item: any) => {
+ return {
+ id: item.id,
+ displayName: item.displayName,
+ name: item.name,
+ modelType: item.modelType,
+ provider: item.provider,
+ status: item.status,
+ createdAt: formatDate(item.createdAt),
+ updatedAt: formatDate(item.updatedAt),
+ };
+ });
+ };
+
+ return (
+ <>
+
+
+
+ {AllAIModels.data?.aiModels.length > 0 ? (
+
+
item.selected)?.label || ''
+ }
+ primaryAction={{
+ content: 'Add New AI Model',
+ onAction: () => CreateAIModel.mutate(),
+ }}
+ />
+
+
+
+ ) : AllAIModels.isLoading ? (
+
+ ) : (
+ <>
+
+
+
+
+ You have not added any AI models yet.
+
+ CreateAIModel.mutate()}>
+ Add New AI Model
+
+
+
+ >
+ )}
+
+ >
+ );
+}
diff --git a/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx b/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx
index a89271c0..862d89fb 100644
--- a/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx
+++ b/app/[locale]/dashboard/[entityType]/[entitySlug]/layout.tsx
@@ -78,6 +78,11 @@ export default function OrgDashboardLayout({ children }: DashboardLayoutProps) {
href: `/dashboard/${params.entityType}/${params.entitySlug}/usecases`,
icon: 'light',
},
+ {
+ title: 'AI Models',
+ href: `/dashboard/${params.entityType}/${params.entitySlug}/aimodels`,
+ icon: 'light',
+ },
{
title: 'Collaboratives',
href: `/dashboard/${params.entityType}/${params.entitySlug}/collaboratives`,