From c95d31c15f9af592e43deda79652044aaaccc5d9 Mon Sep 17 00:00:00 2001 From: dc Date: Mon, 27 Oct 2025 14:47:49 +0530 Subject: [PATCH] add aimodel creation flow --- .../aimodels/edit/[id]/configuration/page.tsx | 205 ++++++++ .../aimodels/edit/[id]/details/page.tsx | 466 ++++++++++++++++++ .../aimodels/edit/[id]/endpoints/page.tsx | 433 ++++++++++++++++ .../[entitySlug]/aimodels/edit/[id]/page.tsx | 12 + .../aimodels/edit/[id]/publish/page.tsx | 318 ++++++++++++ .../[entitySlug]/aimodels/edit/context.tsx | 34 ++ .../[entitySlug]/aimodels/edit/layout.tsx | 185 +++++++ .../[entitySlug]/aimodels/page.tsx | 300 +++++++++++ .../[entityType]/[entitySlug]/layout.tsx | 5 + 9 files changed, 1958 insertions(+) create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/configuration/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/details/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/endpoints/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/[id]/publish/page.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/context.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/edit/layout.tsx create mode 100644 app/[locale]/dashboard/[entityType]/[entitySlug]/aimodels/page.tsx 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 + /> + +
+ +
+ +
+
+ ); +} 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('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" + /> + + +
+ +
+ ); +} 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 + + +
+ + {endpoints.length > 0 ? ( + + ) : ( +
+ + + No endpoints configured yet + + +
+ )} + + + {isModalOpen && ( + + + setFormData({ ...formData, url: value })} + placeholder="https://api.example.com/v1/chat/completions" + required + /> + 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 }) + } + /> +
+ + +
+
+ + +
+
+
+ )} +
+
+ ); +} 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('isPublic', e.target.checked) + } + className="h-4 w-4" + /> +
+ Public + + Make this model visible to all users + +
+ + + +
+ +
+ +
+ + 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 + +
+
+
+
+ +
+ + {!isPublished && ( + + )} +
+ + ); +} 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. + + +
+
+ + )} +
+ + ); +} 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`,