Files
supabase/apps/studio/pages/api/ai/table-quickstart/generate-schemas.ts
Sean Oliver f73ea9bd74 feat: add AI-powered table generation variant (2 of 3) (#38934)
* Add table quickstart with templates and AI generation

Implement table quickstart feature with two variants:
- Template-based: Pre-built schemas across multiple categories (social, ecommerce, content, productivity)
- AI-powered: Generate custom table schemas from natural language prompts

Key features:
- Template selector with category tabs and schema previews
- AI generation using gpt-oss-120b via Bedrock for fast response times
- Streaming responses with real-time table generation
- Quick idea chips for common use cases
- Success feedback and seamless integration with table editor
- Feature flag gated (tableQuickstart) with 7-day new project window
- Comprehensive error handling and retry logic
- ARIA labels and accessibility improvements

Technical implementation:
- Custom React hooks for AI generation and state management
- Zod schema validation for type-safe streaming
- API endpoint with structured object streaming
- localStorage-based dismissal state
- Consistent styling with existing UI patterns

* Refactor and optimize AI table generation

Refactor to use AI SDK streaming with structured object generation:
- Use streamObject() with Zod schema validation for type-safe streaming
- Add 50KB buffer limit to prevent memory issues from oversized responses
- Replace custom stream parsing with Object.assign() for efficient merging
- Use fetchHandler utility for consistent error messages across codebase
- Clean up comments and unused code

* Update quickstart widget styling to match action card

- Changed background from bg-surface-75 to bg-surface-100
- Changed hover background to bg-surface-200
- Applied to AI widget, template widget, and quick idea buttons

* Address PR review feedback

- Persist AI results to localStorage so users can create multiple tables from same generation
- Remove unnecessary isMountedRef checks before synchronous state updates
- Change Quick ideas label to semantic h4 element for accessibility
- Add TODO comment about migrating to experimental_useObject() hook

* Clean up table quickstart implementation

- Match SQL Editor ActionCard styling with circular icons
- Implement smart Generate/Regenerate button logic
- Add tooltips for column counts
- Persist prompt to localStorage
- Fix React hook dependency warnings
- Remove debugging artifacts
2025-10-23 11:35:43 -07:00

130 lines
4.1 KiB
TypeScript

import { streamObject } from 'ai'
import { NextApiRequest, NextApiResponse } from 'next'
import { z } from 'zod'
import { LIMITS } from 'components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/constants'
import { getModel } from 'lib/ai/model'
import apiWrapper from 'lib/api/apiWrapper'
export const maxDuration = 30
const ColumnSchema = z.object({
name: z.string().describe('The column name in snake_case'),
type: z.string().describe('PostgreSQL data type (e.g., bigint, text, timestamp with time zone)'),
isPrimary: z.boolean().optional().nullable().describe('Whether this is a primary key'),
isNullable: z.boolean().optional().nullable().describe('Whether the column can be null'),
defaultValue: z.string().optional().nullable().describe('Default value or expression'),
isUnique: z
.boolean()
.optional()
.nullable()
.describe('Whether the column has a unique constraint'),
})
const TableSchema = z.object({
name: z.string().describe('The table name in snake_case'),
description: z.string().describe('A brief description of what this table stores'),
columns: z.array(ColumnSchema).describe('Array of columns in the table'),
})
const ResponseSchema = z.object({
tables: z.array(TableSchema).describe('Array of related database tables for the application'),
summary: z.string().optional().describe('Brief summary of the generated schema'),
})
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { method } = req
switch (method) {
case 'POST':
return handlePost(req, res)
default:
res.setHeader('Allow', ['POST'])
res.status(405).json({
data: null,
error: { message: `Method ${method} Not Allowed` },
})
}
}
async function handlePost(req: NextApiRequest, res: NextApiResponse) {
const modelName = 'gpt-5-mini'
const modelResult = await getModel({
provider: 'openai',
model: modelName,
routingKey: 'table-quickstart',
isLimited: false,
})
if (modelResult.error || !modelResult.model) {
return res.status(500).json({
error:
modelResult.error?.message || 'AI service temporarily unavailable. Try again in a moment.',
})
}
const { model, providerOptions } = modelResult
const { prompt } = req.body
if (!prompt) {
return res.status(400).json({ error: 'Please provide a description of your app' })
}
if (typeof prompt !== 'string' || prompt.length > LIMITS.MAX_PROMPT_LENGTH) {
return res.status(400).json({
error: `Description too long. Keep it under ${LIMITS.MAX_PROMPT_LENGTH} characters.`,
})
}
try {
const abortController = new AbortController()
req.on('close', () => abortController.abort())
req.on('aborted', () => abortController.abort())
const systemPrompt = `Generate exactly 3 core database tables.
Schema rules:
- Primary key: id (uuid) with isPrimary: true, defaultValue: 'gen_random_uuid()'
- Timestamps: created_at, updated_at (timestamptz) with defaultValue: 'now()'
- Use text for strings, timestamptz for dates, bigint for integers
- snake_case naming
Output rules (minimize tokens):
- ALWAYS include: name, type, isNullable
- Include isPrimary/isUnique/defaultValue ONLY when true/set
- Omit summary field`
const userPrompt = `Application: ${prompt}`
const result = streamObject({
model,
abortSignal: abortController.signal,
schema: ResponseSchema,
mode: 'json',
...(providerOptions && { providerOptions }),
system: systemPrompt,
prompt: userPrompt,
})
result.pipeTextStreamToResponse(res)
} catch (error) {
if (error instanceof Error) {
if (error.message.includes('context_length') || error.message.includes('too long')) {
return res.status(400).json({
error: 'Description too complex. Try using fewer words.',
})
}
}
return res.status(500).json({
error: 'Unable to generate schema. Try a different description.',
})
}
}
const wrapper = (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
export default wrapper