Skip to content

Commit 614a888

Browse files
committed
update aiu model metadata collection page
1 parent 795048d commit 614a888

File tree

11 files changed

+1812
-884
lines changed

11 files changed

+1812
-884
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
'use client';
2+
3+
import { graphql } from '@/gql';
4+
import { useQuery } from '@tanstack/react-query';
5+
import { Spinner, Text } from 'opub-ui';
6+
7+
import BreadCrumbs from '@/components/BreadCrumbs';
8+
import JsonLd from '@/components/JsonLd';
9+
import { GraphQL } from '@/lib/api';
10+
import { generateJsonLd } from '@/lib/utils';
11+
import Details from './components/Details';
12+
import Metadata from './components/Metadata';
13+
import PrimaryData from './components/PrimaryData';
14+
15+
const aiModelQuery: any = graphql(`
16+
query getAIModel($modelId: Int!) {
17+
getAiModel(modelId: $modelId) {
18+
id
19+
name
20+
displayName
21+
version
22+
description
23+
modelType
24+
provider
25+
providerModelId
26+
supportsStreaming
27+
maxTokens
28+
supportedLanguages
29+
inputSchema
30+
outputSchema
31+
metadata
32+
status
33+
isPublic
34+
isActive
35+
averageLatencyMs
36+
successRate
37+
lastAuditScore
38+
auditCount
39+
createdAt
40+
updatedAt
41+
lastTestedAt
42+
tags {
43+
id
44+
value
45+
}
46+
sectors {
47+
id
48+
name
49+
}
50+
geographies {
51+
id
52+
name
53+
}
54+
organization {
55+
id
56+
name
57+
logo {
58+
url
59+
}
60+
}
61+
user {
62+
id
63+
fullName
64+
profilePicture {
65+
url
66+
}
67+
}
68+
endpoints {
69+
id
70+
url
71+
httpMethod
72+
authType
73+
timeoutSeconds
74+
isPrimary
75+
isActive
76+
}
77+
}
78+
}
79+
`);
80+
81+
export default function AIModelDetailsPage({
82+
modelId,
83+
}: {
84+
modelId: string;
85+
}) {
86+
const { data, isLoading, error } = useQuery(
87+
[`aimodel_details_${modelId}`],
88+
() => GraphQL(aiModelQuery, {}, { modelId: parseInt(modelId) }),
89+
{
90+
retry: false,
91+
onError: (err) => {
92+
console.error('Error fetching AI model:', err);
93+
},
94+
}
95+
);
96+
97+
const modelData = (data as any)?.getAiModel;
98+
99+
const jsonLd = generateJsonLd({
100+
'@context': 'https://schema.org',
101+
'@type': 'SoftwareApplication',
102+
name: modelData?.displayName || modelData?.name,
103+
url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}/aimodels/${modelId}`,
104+
description: modelData?.description,
105+
applicationCategory: 'AI Model',
106+
publisher: {
107+
'@type': 'Organization',
108+
name: 'CivicDataSpace',
109+
url: `${process.env.NEXT_PUBLIC_PLATFORM_URL}`,
110+
},
111+
});
112+
113+
return (
114+
<>
115+
<JsonLd json={jsonLd} />
116+
<main className="bg-surfaceDefault">
117+
<BreadCrumbs
118+
data={[
119+
{ href: '/', label: 'Home' },
120+
{ href: '/search?types=aimodel', label: 'AI Models' },
121+
{ href: '#', label: 'AI Model Details' },
122+
]}
123+
/>
124+
<div className="flex">
125+
<div className="w-full gap-10 border-r-2 border-solid border-greyExtralight p-6 lg:w-3/4 lg:p-10">
126+
{isLoading ? (
127+
<div className="mt-8 flex justify-center">
128+
<Spinner />
129+
</div>
130+
) : error ? (
131+
<div className="mt-8 flex flex-col items-center gap-4">
132+
<Text variant="heading2xl">Error loading AI Model</Text>
133+
<Text variant="bodyMd" className="text-textSubdued">
134+
{error instanceof Error ? error.message : 'Failed to fetch AI model'}
135+
</Text>
136+
<Text variant="bodySm" className="text-textSubdued">
137+
Model ID: {modelId}
138+
</Text>
139+
</div>
140+
) : modelData ? (
141+
<>
142+
<PrimaryData data={modelData} isLoading={isLoading} />
143+
<Details data={modelData} />
144+
</>
145+
) : (
146+
<div className="mt-8 flex justify-center">
147+
<Text variant="heading2xl">AI Model not found</Text>
148+
</div>
149+
)}
150+
</div>
151+
<div className="hidden w-1/4 gap-10 px-7 py-10 lg:block">
152+
{isLoading ? (
153+
<div className="mt-8 flex justify-center">
154+
<Spinner />
155+
</div>
156+
) : (
157+
modelData && <Metadata data={modelData} />
158+
)}
159+
</div>
160+
</div>
161+
</main>
162+
</>
163+
);
164+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use client';
2+
3+
import { Text } from 'opub-ui';
4+
5+
interface DetailsProps {
6+
data: any;
7+
}
8+
9+
export default function Details({ data }: DetailsProps) {
10+
if (!data) return null;
11+
12+
return (
13+
<div className="mt-8 flex flex-col gap-6">
14+
{/* Input/Output Schema */}
15+
{(data.inputSchema && Object.keys(data.inputSchema).length > 0) ||
16+
(data.outputSchema && Object.keys(data.outputSchema).length > 0) ? (
17+
<div className="flex flex-col gap-4">
18+
<Text variant="headingLg" fontWeight="semibold">
19+
API Schema
20+
</Text>
21+
22+
{data.inputSchema && Object.keys(data.inputSchema).length > 0 && (
23+
<div className="flex flex-col gap-2">
24+
<Text variant="bodyMd" fontWeight="semibold">
25+
Input Schema
26+
</Text>
27+
<pre className="overflow-x-auto rounded-lg border border-greyExtralight bg-surfaceSubdued p-4 text-sm">
28+
{JSON.stringify(data.inputSchema, null, 2)}
29+
</pre>
30+
</div>
31+
)}
32+
33+
{data.outputSchema && Object.keys(data.outputSchema).length > 0 && (
34+
<div className="flex flex-col gap-2">
35+
<Text variant="bodyMd" fontWeight="semibold">
36+
Output Schema
37+
</Text>
38+
<pre className="overflow-x-auto rounded-lg border border-greyExtralight bg-surfaceSubdued p-4 text-sm">
39+
{JSON.stringify(data.outputSchema, null, 2)}
40+
</pre>
41+
</div>
42+
)}
43+
</div>
44+
) : null}
45+
46+
{/* Additional Metadata */}
47+
{data.metadata && Object.keys(data.metadata).length > 0 && (
48+
<div className="flex flex-col gap-3">
49+
<Text variant="headingLg" fontWeight="semibold">
50+
Additional Information
51+
</Text>
52+
<div className="rounded-lg border border-greyExtralight p-4">
53+
{Object.entries(data.metadata).map(([key, value]) => (
54+
<div key={key} className="mb-3 last:mb-0">
55+
<Text variant="bodySm" className="text-textSubdued">
56+
{key.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase())}
57+
</Text>
58+
<Text variant="bodyMd" className="mt-1">
59+
{typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
60+
</Text>
61+
</div>
62+
))}
63+
</div>
64+
</div>
65+
)}
66+
67+
{/* Endpoints Information */}
68+
{data.endpoints && data.endpoints.length > 0 && (
69+
<div className="flex flex-col gap-3">
70+
<Text variant="headingLg" fontWeight="semibold">
71+
API Endpoints
72+
</Text>
73+
<div className="flex flex-col gap-3">
74+
{data.endpoints.map((endpoint: any, index: number) => (
75+
<div
76+
key={endpoint.id || index}
77+
className="rounded-lg border border-greyExtralight p-4"
78+
>
79+
<div className="mb-2 flex items-center gap-2">
80+
<Text variant="bodyMd" fontWeight="semibold">
81+
{endpoint.url}
82+
</Text>
83+
{endpoint.isPrimary && (
84+
<span className="rounded bg-primaryBlue px-2 py-1 text-xs text-white">
85+
Primary
86+
</span>
87+
)}
88+
</div>
89+
<div className="grid gap-2 text-sm">
90+
<div>
91+
<span className="text-textSubdued">Method: </span>
92+
<span>{endpoint.httpMethod}</span>
93+
</div>
94+
<div>
95+
<span className="text-textSubdued">Auth Type: </span>
96+
<span>{endpoint.authType}</span>
97+
</div>
98+
{endpoint.timeoutSeconds && (
99+
<div>
100+
<span className="text-textSubdued">Timeout: </span>
101+
<span>{endpoint.timeoutSeconds}s</span>
102+
</div>
103+
)}
104+
<div>
105+
<span className="text-textSubdued">Status: </span>
106+
<span>{endpoint.isActive ? 'Active' : 'Inactive'}</span>
107+
</div>
108+
</div>
109+
</div>
110+
))}
111+
</div>
112+
</div>
113+
)}
114+
</div>
115+
);
116+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use client';
2+
3+
import { Badge, Icon, Text } from 'opub-ui';
4+
import { Icons } from '@/components/icons';
5+
import { formatDate } from '@/lib/utils';
6+
7+
interface MetadataProps {
8+
data: any;
9+
}
10+
11+
export default function Metadata({ data }: MetadataProps) {
12+
if (!data) return null;
13+
14+
return (
15+
<div className="flex flex-col gap-6">
16+
<Text variant="headingLg" fontWeight="semibold">
17+
Metadata
18+
</Text>
19+
20+
{/* Status */}
21+
<div className="flex flex-col gap-2">
22+
<Text variant="bodySm" className="text-textSubdued">
23+
Status
24+
</Text>
25+
<Badge>{data.status}</Badge>
26+
</div>
27+
28+
{/* Created Date */}
29+
{data.createdAt && (
30+
<div className="flex flex-col gap-2">
31+
<div className="flex items-center gap-2">
32+
<Icon source={Icons.calendar} />
33+
<Text variant="bodySm" className="text-textSubdued">
34+
Created
35+
</Text>
36+
</div>
37+
<Text variant="bodyMd">{formatDate(data.createdAt)}</Text>
38+
</div>
39+
)}
40+
41+
{/* Updated Date */}
42+
{data.updatedAt && (
43+
<div className="flex flex-col gap-2">
44+
<div className="flex items-center gap-2">
45+
<Icon source={Icons.calendar} />
46+
<Text variant="bodySm" className="text-textSubdued">
47+
Last Updated
48+
</Text>
49+
</div>
50+
<Text variant="bodyMd">{formatDate(data.updatedAt)}</Text>
51+
</div>
52+
)}
53+
54+
{/* Sectors */}
55+
{data.sectors && data.sectors.length > 0 && (
56+
<div className="flex flex-col gap-2">
57+
<Text variant="bodySm" className="text-textSubdued">
58+
Sectors
59+
</Text>
60+
<div className="flex flex-wrap gap-2">
61+
{data.sectors.map((sector: any) => (
62+
<Badge key={sector.id}>{sector.name}</Badge>
63+
))}
64+
</div>
65+
</div>
66+
)}
67+
68+
{/* Geographies */}
69+
{data.geographies && data.geographies.length > 0 && (
70+
<div className="flex flex-col gap-2">
71+
<div className="flex items-center gap-2">
72+
<Icon source={Icons.globe} />
73+
<Text variant="bodySm" className="text-textSubdued">
74+
Geographies
75+
</Text>
76+
</div>
77+
<div className="flex flex-wrap gap-2">
78+
{data.geographies.map((geo: any) => (
79+
<Badge key={geo.id}>{geo.name}</Badge>
80+
))}
81+
</div>
82+
</div>
83+
)}
84+
85+
{/* Visibility */}
86+
<div className="flex flex-col gap-2">
87+
<Text variant="bodySm" className="text-textSubdued">
88+
Visibility
89+
</Text>
90+
<Text variant="bodyMd">{data.isPublic ? 'Public' : 'Private'}</Text>
91+
</div>
92+
93+
{/* Active Status */}
94+
<div className="flex flex-col gap-2">
95+
<Text variant="bodySm" className="text-textSubdued">
96+
Active
97+
</Text>
98+
<Text variant="bodyMd">{data.isActive ? 'Yes' : 'No'}</Text>
99+
</div>
100+
101+
{/* Audit Count */}
102+
{data.auditCount > 0 && (
103+
<div className="flex flex-col gap-2">
104+
<Text variant="bodySm" className="text-textSubdued">
105+
Total Audits
106+
</Text>
107+
<Text variant="bodyMd">{data.auditCount}</Text>
108+
</div>
109+
)}
110+
</div>
111+
);
112+
}

0 commit comments

Comments
 (0)