Skip to content

Commit 628dfc1

Browse files
committed
feat: add AI model categories and enhance model metadata
- Introduced a new sidebar link for admin users to navigate to the "Models" section. - Updated the PromptUploadWizard component to utilize new neobrutalist styles for inputs, selects, and textareas. - Enhanced the model summary to include additional metadata such as family and provider. - Created a new table for AI model categories in the database to manage model families and providers. - Updated the AI models table to include references to the new model categories. - Improved the UI for various components to provide a more cohesive design experience.
2 parents f13600c + eeaeae4 commit 628dfc1

17 files changed

Lines changed: 2681 additions & 74 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { NextResponse } from 'next/server'
2+
import slugify from '@sindresorhus/slugify'
3+
import { createServerComponentClient, createServiceRoleClient } from '@/lib/supabase/server-client'
4+
import type { AdminModelCategory } from '@/utils/types'
5+
6+
const mapCategory = (record: {
7+
id: string
8+
name: string
9+
slug: string
10+
description: string | null
11+
accent_color: string | null
12+
created_at: string
13+
updated_at: string
14+
}): AdminModelCategory => ({
15+
id: record.id,
16+
name: record.name,
17+
slug: record.slug,
18+
description: record.description ?? null,
19+
accentColor: record.accent_color ?? null,
20+
createdAt: record.created_at,
21+
updatedAt: record.updated_at,
22+
})
23+
24+
const getAdminProfile = async (): Promise<
25+
| { response: NextResponse }
26+
| { profile: { id: string } }
27+
> => {
28+
const supabase = createServerComponentClient()
29+
const {
30+
data: { user },
31+
} = await supabase.auth.getUser()
32+
33+
if (!user) {
34+
return {
35+
response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
36+
}
37+
}
38+
39+
const { data: profile, error } = await supabase
40+
.from('profiles')
41+
.select('id, is_admin')
42+
.eq('user_id', user.id)
43+
.maybeSingle()
44+
45+
if (error) {
46+
return {
47+
response: NextResponse.json(
48+
{ error: `Unable to load profile: ${error.message}` },
49+
{ status: 500 },
50+
),
51+
}
52+
}
53+
54+
if (!profile || !profile.is_admin) {
55+
return {
56+
response: NextResponse.json(
57+
{ error: 'Forbidden: admin access required.' },
58+
{ status: 403 },
59+
),
60+
}
61+
}
62+
63+
return { profile: { id: profile.id } }
64+
}
65+
66+
const sanitizeName = (value: unknown): string | null => {
67+
if (typeof value !== 'string') return null
68+
const trimmed = value.trim()
69+
return trimmed.length > 0 ? trimmed : null
70+
}
71+
72+
const sanitizeSlug = (value: unknown): string | null => {
73+
if (typeof value !== 'string') return null
74+
const trimmed = value.trim()
75+
return trimmed.length > 0 ? slugify(trimmed) : null
76+
}
77+
78+
const sanitizeOptionalText = (value: unknown): string | null => {
79+
if (typeof value !== 'string') return null
80+
const trimmed = value.trim()
81+
return trimmed.length > 0 ? trimmed : null
82+
}
83+
84+
export async function PATCH(
85+
request: Request,
86+
{ params }: { params: Promise<{ id: string }> },
87+
) {
88+
const result = await getAdminProfile()
89+
if ('response' in result) {
90+
return result.response
91+
}
92+
93+
const { id } = await params
94+
if (!id) {
95+
return NextResponse.json({ error: 'Category ID is required.' }, { status: 400 })
96+
}
97+
98+
const body = (await request.json()) as Record<string, unknown>
99+
100+
const updates: Record<string, unknown> = {}
101+
102+
const name = sanitizeName(body.name)
103+
if (name !== null) {
104+
updates.name = name
105+
}
106+
107+
const slug = sanitizeSlug(body.slug)
108+
if (slug !== null) {
109+
updates.slug = slug
110+
}
111+
112+
if ('description' in body) {
113+
updates.description = sanitizeOptionalText(body.description)
114+
}
115+
116+
if ('accentColor' in body) {
117+
updates.accent_color = sanitizeOptionalText(body.accentColor)
118+
}
119+
120+
if (Object.keys(updates).length === 0) {
121+
return NextResponse.json({ error: 'No updates provided.' }, { status: 400 })
122+
}
123+
124+
const client = createServiceRoleClient()
125+
const { data, error } = await client
126+
.from('ai_model_categories')
127+
.update(updates)
128+
.eq('id', id)
129+
.select('*')
130+
.single()
131+
132+
if (error || !data) {
133+
const message = error?.message ?? 'Unable to update model category.'
134+
const status = message.includes('duplicate key value') ? 409 : 500
135+
return NextResponse.json({ error: message }, { status })
136+
}
137+
138+
return NextResponse.json({ category: mapCategory(data) })
139+
}
140+
141+
export async function DELETE(
142+
_request: Request,
143+
{ params }: { params: Promise<{ id: string }> },
144+
) {
145+
const result = await getAdminProfile()
146+
if ('response' in result) {
147+
return result.response
148+
}
149+
150+
const { id } = await params
151+
if (!id) {
152+
return NextResponse.json({ error: 'Category ID is required.' }, { status: 400 })
153+
}
154+
155+
const client = createServiceRoleClient()
156+
const { error } = await client.from('ai_model_categories').delete().eq('id', id)
157+
158+
if (error) {
159+
return NextResponse.json(
160+
{ error: `Unable to delete model category: ${error.message}` },
161+
{ status: 500 },
162+
)
163+
}
164+
165+
return NextResponse.json({ success: true })
166+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { NextResponse } from 'next/server'
2+
import slugify from '@sindresorhus/slugify'
3+
import { createServerComponentClient, createServiceRoleClient } from '@/lib/supabase/server-client'
4+
import type { AdminModelCategory } from '@/utils/types'
5+
6+
const mapCategory = (record: {
7+
id: string
8+
name: string
9+
slug: string
10+
description: string | null
11+
accent_color: string | null
12+
created_at: string
13+
updated_at: string
14+
}): AdminModelCategory => ({
15+
id: record.id,
16+
name: record.name,
17+
slug: record.slug,
18+
description: record.description ?? null,
19+
accentColor: record.accent_color ?? null,
20+
createdAt: record.created_at,
21+
updatedAt: record.updated_at,
22+
})
23+
24+
const getAdminProfile = async (): Promise<
25+
| { response: NextResponse }
26+
| { profile: { id: string } }
27+
> => {
28+
const supabase = createServerComponentClient()
29+
const {
30+
data: { user },
31+
} = await supabase.auth.getUser()
32+
33+
if (!user) {
34+
return {
35+
response: NextResponse.json({ error: 'Unauthorized' }, { status: 401 }),
36+
}
37+
}
38+
39+
const { data: profile, error } = await supabase
40+
.from('profiles')
41+
.select('id, is_admin')
42+
.eq('user_id', user.id)
43+
.maybeSingle()
44+
45+
if (error) {
46+
return {
47+
response: NextResponse.json(
48+
{ error: `Unable to load profile: ${error.message}` },
49+
{ status: 500 },
50+
),
51+
}
52+
}
53+
54+
if (!profile || !profile.is_admin) {
55+
return {
56+
response: NextResponse.json(
57+
{ error: 'Forbidden: admin access required.' },
58+
{ status: 403 },
59+
),
60+
}
61+
}
62+
63+
return { profile: { id: profile.id } }
64+
}
65+
66+
const sanitizeName = (value: unknown): string => {
67+
if (typeof value !== 'string') return ''
68+
return value.trim()
69+
}
70+
71+
const sanitizeSlug = (value: unknown, fallback: string): string => {
72+
if (typeof value === 'string' && value.trim().length > 0) {
73+
return slugify(value.trim())
74+
}
75+
return slugify(fallback)
76+
}
77+
78+
const sanitizeOptionalText = (value: unknown): string | null => {
79+
if (typeof value !== 'string') return null
80+
const trimmed = value.trim()
81+
return trimmed.length > 0 ? trimmed : null
82+
}
83+
84+
export async function GET() {
85+
const result = await getAdminProfile()
86+
if ('response' in result) {
87+
return result.response
88+
}
89+
90+
const client = createServiceRoleClient()
91+
const { data, error } = await client
92+
.from('ai_model_categories')
93+
.select('*')
94+
.order('name')
95+
96+
if (error) {
97+
return NextResponse.json(
98+
{ error: `Unable to load model categories: ${error.message}` },
99+
{ status: 500 },
100+
)
101+
}
102+
103+
const categories = (data ?? []).map((record) => mapCategory(record))
104+
return NextResponse.json({ categories })
105+
}
106+
107+
export async function POST(request: Request) {
108+
const result = await getAdminProfile()
109+
if ('response' in result) {
110+
return result.response
111+
}
112+
113+
const body = (await request.json()) as Record<string, unknown>
114+
115+
const name = sanitizeName(body.name)
116+
const slug = sanitizeSlug(body.slug, name)
117+
const description = sanitizeOptionalText(body.description)
118+
const accentColor = sanitizeOptionalText(body.accentColor)
119+
120+
if (!name) {
121+
return NextResponse.json({ error: 'Category name is required.' }, { status: 400 })
122+
}
123+
124+
if (!slug) {
125+
return NextResponse.json({ error: 'Category slug is required.' }, { status: 400 })
126+
}
127+
128+
const client = createServiceRoleClient()
129+
const { data, error } = await client
130+
.from('ai_model_categories')
131+
.insert({
132+
name,
133+
slug,
134+
description,
135+
accent_color: accentColor,
136+
})
137+
.select('*')
138+
.single()
139+
140+
if (error || !data) {
141+
const message = error?.message ?? 'Unable to create model category.'
142+
const status = message.includes('duplicate key value') ? 409 : 500
143+
return NextResponse.json({ error: message }, { status })
144+
}
145+
146+
return NextResponse.json({ category: mapCategory(data) })
147+
}

0 commit comments

Comments
 (0)