diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/TableTemplateSelector.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/TableTemplateSelector.tsx index 8cf4f6bff52..23834abd2ce 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/TableTemplateSelector.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/TableTemplateSelector.tsx @@ -1,18 +1,17 @@ -import { useState, useCallback, useRef, useEffect } from 'react' +import { Columns3, Wand2 } from 'lucide-react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' -import { useAITableGeneration } from './useAITableGeneration' -import { tableTemplates } from './templates' -import { QuickstartVariant, ViewMode } from './types' -import { convertTableSuggestionToTableField } from './utils' -import { InitialView } from './components/InitialView' -import { CategoryView } from './components/CategoryView' -import { AIInputView } from './components/AIInputView' -import { ResultsView } from './components/ResultsView' -import type { TableSuggestion } from './types' +import { Button, Input_Shadcn_ as Input, cn } from 'ui' import type { TableField } from '../TableEditor.types' +import { AI_QUICK_IDEAS } from './constants' +import { tableTemplates } from './templates' +import type { TableSuggestion } from './types' +import { QuickstartVariant } from './types' +import { useAITableGeneration } from './useAITableGeneration' +import { convertTableSuggestionToTableField } from './utils' interface TableTemplateSelectorProps { - variant: Exclude + variant: Exclude onSelectTemplate: (tableField: Partial) => void onDismiss?: () => void disabled?: boolean @@ -20,45 +19,44 @@ interface TableTemplateSelectorProps { const SUCCESS_MESSAGE_DURATION_MS = 3000 -interface ViewState { - mode: ViewMode - selectedCategory: string | null - selectedTemplate: TableSuggestion | null - generatedTables: TableSuggestion[] - isLoading: boolean -} - -const initialViewState: ViewState = { - mode: ViewMode.INITIAL, - selectedCategory: null, - selectedTemplate: null, - generatedTables: [], - isLoading: false, -} - export const TableTemplateSelector = ({ variant, onSelectTemplate, onDismiss, disabled, }: TableTemplateSelectorProps) => { - const [viewState, setViewState] = useState(initialViewState) + // Templates variant state + const [activeCategory, setActiveCategory] = useState(null) + const [selectedTemplate, setSelectedTemplate] = useState(null) + + // AI variant state const [aiPrompt, setAiPrompt] = useState('') + const [generatedTables, setGeneratedTables] = useState([]) const inputRef = useRef(null) const { generateTables, isGenerating, error: apiError } = useAITableGeneration() - // Focus management + const isAI = variant === QuickstartVariant.AI + const categories = useMemo(() => Object.keys(tableTemplates), []) + + // Auto-select first category for templates variant useEffect(() => { - if (viewState.mode === ViewMode.AI_INPUT && inputRef.current) { + if (!isAI && activeCategory === null && categories.length > 0) { + setActiveCategory(categories[0]) + } + }, [categories, activeCategory, isAI]) + + // Focus AI input when in AI mode + useEffect(() => { + if (isAI && inputRef.current) { inputRef.current.focus() } - }, [viewState.mode]) + }, [isAI]) const handleSelectTemplate = useCallback( (template: TableSuggestion) => { const tableField = convertTableSuggestionToTableField(template) onSelectTemplate(tableField) - setViewState((prev) => ({ ...prev, selectedTemplate: template })) + setSelectedTemplate(template) toast.success( `${template.tableName} template applied. You can add or modify the fields below.`, { @@ -69,122 +67,200 @@ export const TableTemplateSelector = ({ [onSelectTemplate] ) - const handleGenerateTables = useCallback(async (promptOverride?: string) => { - const promptToUse = promptOverride ?? aiPrompt - if (!promptToUse.trim() || isGenerating) return + const handleGenerateTables = useCallback( + async (promptOverride?: string) => { + const promptToUse = promptOverride ?? aiPrompt + if (!promptToUse.trim() || isGenerating) return - setViewState((prev) => ({ ...prev, isLoading: true })) + try { + const tables = await generateTables(promptToUse) - try { - const tables = await generateTables(promptToUse) - - if (tables.length > 0) { - setViewState({ - mode: ViewMode.AI_RESULTS, - selectedCategory: null, - selectedTemplate: tables[0], - generatedTables: tables, - isLoading: false, - }) - handleSelectTemplate(tables[0]) - } else { - setViewState((prev) => ({ - ...prev, - isLoading: false, - })) + if (tables.length > 0) { + setGeneratedTables(tables) + handleSelectTemplate(tables[0]) + } + } catch (error) { + // Error handling is done in the hook } - } catch (error) { - setViewState((prev) => ({ - ...prev, - isLoading: false, - })) - } - }, [aiPrompt, generateTables, isGenerating, handleSelectTemplate]) + }, + [aiPrompt, generateTables, isGenerating, handleSelectTemplate] + ) const handleQuickIdea = useCallback( (idea: string) => { setAiPrompt(idea) - setViewState((prev) => ({ ...prev, mode: ViewMode.AI_INPUT })) handleGenerateTables(idea) }, [handleGenerateTables] ) - const handleCategorySelect = useCallback((category: string) => { - setViewState((prev) => ({ - ...prev, - mode: ViewMode.CATEGORY_SELECTED, - selectedCategory: category, - })) - }, []) + const displayedTemplates = useMemo(() => { + if (isAI) { + return generatedTables + } + return activeCategory ? tableTemplates[activeCategory] || [] : [] + }, [isAI, generatedTables, activeCategory]) - const handleAISelect = useCallback(() => { - setViewState((prev) => ({ ...prev, mode: ViewMode.AI_INPUT })) - }, []) + return ( +
+ {/* Header */} +
+
+

+ {isAI ? 'Generate tables with AI' : 'Start faster with a table template'} +

+

+ {isAI + ? "Describe the app you're building and AI will create ready-made table schema." + : 'Save time by starting from a ready-made table schema.'} +

+
+ {onDismiss && ( + + )} +
- const handleBack = useCallback(() => { - setViewState(initialViewState) - setAiPrompt('') - }, []) + {/* AI Variant: Input and Quick Ideas */} + {isAI && ( +
+
+ setAiPrompt(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + if (aiPrompt.trim()) { + handleGenerateTables() + } + } + }} + disabled={isGenerating || disabled} + aria-label="Table description for AI generation" + className="pr-24" + /> + +
- const { mode, selectedCategory, selectedTemplate, generatedTables } = viewState + {apiError && ( +
+ {apiError} +
+ )} - // Initial view - if (mode === ViewMode.INITIAL) { - return ( - - ) - } + {/* Quick Ideas */} + {generatedTables.length === 0 && ( +
+
Quick ideas:
+
+ {AI_QUICK_IDEAS.map((idea) => ( + + ))} +
+
+ )} +
+ )} - // Category selection view - if (mode === ViewMode.CATEGORY_SELECTED && selectedCategory) { - const templates = tableTemplates[selectedCategory] ?? [] - return ( - - ) - } + {/* Templates Variant: Category Tabs */} + {!isAI && ( +
+ {categories.map((category) => ( + + ))} +
+ )} - // AI input view - if (mode === ViewMode.AI_INPUT) { - return ( - - ) - } - - // AI results view - if (mode === ViewMode.AI_RESULTS && generatedTables.length > 0) { - return ( - - ) - } - - return null + {/* Templates List (both variants use this) */} + {displayedTemplates.length > 0 && ( +
+ {isAI && ( +
+ + Generated {generatedTables.length} table{generatedTables.length !== 1 ? 's' : ''} + + +
+ )} + {displayedTemplates.map((template) => ( + + ))} +
+ )} +
+ ) } diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/AIInputView.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/AIInputView.tsx deleted file mode 100644 index 5a0aede7d98..00000000000 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/AIInputView.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { memo } from 'react' -import { Button, Input_Shadcn_ as Input } from 'ui' -import { BackButton } from './SharedComponents' - -interface AIInputViewProps { - prompt: string - error: string | null - isGenerating: boolean - isLoading: boolean - inputRef: React.RefObject - onBack: () => void - onPromptChange: (value: string) => void - onGenerate: () => void -} - -export const AIInputView = memo( - ({ - prompt, - error, - isGenerating, - isLoading, - inputRef, - onBack, - onPromptChange, - onGenerate, - }: AIInputViewProps) => ( -
-
- -
- {error && ( -
- {error} -
- )} -
- onPromptChange(e.target.value)} - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - if (prompt.trim()) { - onGenerate() - } - } - }} - disabled={isGenerating || isLoading} - aria-label="Table description for AI generation" - aria-describedby="ai-prompt-help" - className="pr-24" - /> - -
- - Enter a description of your tables and press Enter or click Generate - -
- ) -) - -AIInputView.displayName = 'AIInputView' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/CategoryView.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/CategoryView.tsx deleted file mode 100644 index a6ca5a133e5..00000000000 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/CategoryView.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { memo } from 'react' -import type { TableSuggestion } from '../types' -import { BackButton, TemplateList } from './SharedComponents' - -interface CategoryViewProps { - category: string - templates: TableSuggestion[] - selectedTemplate: TableSuggestion | null - onBack: () => void - onSelectTemplate: (template: TableSuggestion) => void -} - -export const CategoryView = memo( - ({ category, templates, selectedTemplate, onBack, onSelectTemplate }: CategoryViewProps) => { - if (templates.length === 0) { - return ( -
- -

- No templates available for this category. -

-
- ) - } - - return ( -
-
- - {category} -
- -
- ) - } -) - -CategoryView.displayName = 'CategoryView' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/InitialView.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/InitialView.tsx deleted file mode 100644 index 7d87b6f3f84..00000000000 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/InitialView.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { memo } from 'react' -import { Wand2, ChevronDown, Database } from 'lucide-react' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, - Button, -} from 'ui' -import { QuickstartVariant } from '../types' -import { tableTemplates } from '../templates' -import { AI_QUICK_IDEAS } from '../constants' -import { SelectorHeader } from './SharedComponents' - -interface InitialViewProps { - variant: Exclude - disabled?: boolean - onDismiss?: () => void - onCategorySelect: (category: string) => void - onAISelect: () => void - onQuickIdea: (idea: string) => void -} - -export const InitialView = memo( - ({ - variant, - disabled, - onDismiss, - onCategorySelect, - onAISelect, - onQuickIdea, - }: InitialViewProps) => { - const isAI = variant === QuickstartVariant.AI - const icon = isAI ? : - const label = isAI ? 'Generate with AI' : 'Select from templates' - - return ( -
- - - - - - - {isAI ? ( - <> - - - -
Quick ideas
- {AI_QUICK_IDEAS.map((example) => ( - onQuickIdea(example)}> - {example} - - ))} - - ) : ( - Object.keys(tableTemplates).map((category) => ( - onCategorySelect(category)}> - - )) - )} -
-
-
- ) - } -) - -InitialView.displayName = 'InitialView' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/ResultsView.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/ResultsView.tsx deleted file mode 100644 index c71b2154f3e..00000000000 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/ResultsView.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { memo } from 'react' -import type { TableSuggestion } from '../types' -import { BackButton, TemplateList } from './SharedComponents' - -interface ResultsViewProps { - tables: TableSuggestion[] - selectedTemplate: TableSuggestion | null - title: string - onBack: () => void - onSelectTemplate: (template: TableSuggestion) => void -} - -export const ResultsView = memo( - ({ tables, selectedTemplate, title, onBack, onSelectTemplate }: ResultsViewProps) => ( -
-
- - {title} -
-
- -
-
- ) -) - -ResultsView.displayName = 'ResultsView' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/SharedComponents.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/SharedComponents.tsx deleted file mode 100644 index a9d0b1323b3..00000000000 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/components/SharedComponents.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { memo } from 'react' -import { ArrowLeft, Columns3, X } from 'lucide-react' -import { Button, cn } from 'ui' -import type { TableSuggestion } from '../types' - -export const BackButton = memo( - ({ - onClick, - disabled = false, - label = 'Back', - }: { - onClick: () => void - disabled?: boolean - label?: string - }) => ( - - ) -) -BackButton.displayName = 'BackButton' - -export const SelectorHeader = memo(({ onDismiss }: { onDismiss?: () => void }) => ( -
- - {onDismiss && ( - - )} -
-)) -SelectorHeader.displayName = 'SelectorHeader' - -export const TemplateItem = memo( - ({ - template, - isSelected, - onClick, - }: { - template: TableSuggestion - isSelected: boolean - onClick: () => void - }) => ( - - ) -) -TemplateItem.displayName = 'TemplateItem' - -export const TemplateList = memo( - ({ - templates, - selectedTemplate, - onSelectTemplate, - }: { - templates: TableSuggestion[] - selectedTemplate: TableSuggestion | null - onSelectTemplate: (template: TableSuggestion) => void - }) => ( -
- {templates.map((template) => ( - onSelectTemplate(template)} - /> - ))} -
- ) -) -TemplateList.displayName = 'TemplateList' diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/types.ts b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/types.ts index aeceaa3acdb..f56008794cd 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/types.ts +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableQuickstart/types.ts @@ -47,13 +47,6 @@ export enum QuickstartVariant { TEMPLATES = 'templates', } -export enum ViewMode { - INITIAL = 'initial', - AI_INPUT = 'ai-input', - AI_RESULTS = 'ai-results', - CATEGORY_SELECTED = 'category-selected', -} - export type TableSuggestion = { tableName: string fields: TableField[] diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index f531e687291..13917978303 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -15,6 +15,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/ai/onboarding/design', '/ai/feedback/classify', '/ai/docs', + '/ai/table-quickstart/generate-schemas', '/get-ip-address', '/get-utc-time', '/get-deployment-commit',