Generated by Spark: OpenAI integration could help with various pages in the app

This commit is contained in:
2026-01-16 00:47:04 +00:00
committed by GitHub
parent e5557fbfa9
commit 77bacf0ad4
10 changed files with 768 additions and 95 deletions

44
PRD.md
View File

@@ -13,32 +13,39 @@ This is a full-featured low-code IDE with multiple integrated tools (code editor
## Essential Features
### Monaco Code Editor Integration
- **Functionality**: Full-featured code editor with syntax highlighting, autocomplete, and multi-file editing
- **Purpose**: Allows direct code manipulation for users who want precise control
- **Functionality**: Full-featured code editor with syntax highlighting, autocomplete, multi-file editing, and AI-powered code improvement and explanation
- **Purpose**: Allows direct code manipulation for users who want precise control, with AI assistance for learning and optimization
- **Trigger**: Clicking on files in the file tree or switching to code view
- **Progression**: Select file → Editor opens with syntax highlighting → Edit code → Changes auto-saved to state → Preview updates
- **Success criteria**: Code edits persist, syntax highlighting works for JS/TS/CSS, multiple files can be open in tabs
- **Progression**: Select file → Editor opens with syntax highlighting → Edit code → Use AI to improve or explain code → Changes auto-saved to state → Preview updates
- **Success criteria**: Code edits persist, syntax highlighting works for JS/TS/CSS, multiple files can be open in tabs, AI explanations are helpful, AI improvements are relevant
### AI-Powered Code Generation
- **Functionality**: Generate complete files, components, models, and themes using natural language descriptions via OpenAI integration
- **Purpose**: Accelerates development by automating boilerplate and scaffolding based on user intent
- **Trigger**: Clicking AI/Sparkle icons in various sections or the main "AI Generate" button
- **Progression**: User describes intent → AI processes request → Generated code appears → User can refine or accept
- **Success criteria**: Generated code is syntactically valid, follows conventions, matches user intent, integrates with existing project structure
### Prisma Schema Designer
- **Functionality**: Visual model designer for database schemas with drag-and-drop field creation
- **Purpose**: Simplifies database modeling without requiring Prisma syntax knowledge
- **Functionality**: Visual model designer for database schemas with drag-and-drop field creation and AI-powered model generation and field suggestions
- **Purpose**: Simplifies database modeling without requiring Prisma syntax knowledge, with intelligent AI assistance
- **Trigger**: Opening the Models tab
- **Progression**: Create model → Add fields with types → Define relations → Visual graph updates → Generate Prisma schema code
- **Success criteria**: Can create models, fields, relations; generates valid Prisma syntax; visual representation is clear
- **Progression**: Create model manually or with AI → Add fields with types or get AI suggestions → Define relations → Visual graph updates → Generate Prisma schema code
- **Success criteria**: Can create models, fields, relations; AI suggestions are contextually relevant; generates valid Prisma syntax; visual representation is clear
### Component Tree Builder
- **Functionality**: Hierarchical tree view for building React component structure
- **Purpose**: Visual composition of component hierarchy without writing JSX
- **Functionality**: Hierarchical tree view for building React component structure with AI-powered component generation
- **Purpose**: Visual composition of component hierarchy without writing JSX, enhanced by AI scaffolding
- **Trigger**: Opening the Components tab
- **Progression**: Select component type → Add to tree → Configure props → Nest children → View generated JSX → Export component
- **Success criteria**: Can add/remove/reorder components; props are editable; generates valid React code
- **Progression**: Select component type or describe to AI → Add to tree → Configure props → Nest children → View generated JSX → Export component
- **Success criteria**: Can add/remove/reorder components; AI-generated components are well-structured; props are editable; generates valid React code
### Style Designer
- **Functionality**: Visual interface for Material UI theming and component styling
- **Purpose**: Configure colors, typography, spacing without manual theme object creation
- **Functionality**: Visual interface for Material UI theming and component styling with AI theme generation from descriptions
- **Purpose**: Configure colors, typography, spacing without manual theme object creation, with AI design assistance
- **Trigger**: Opening the Styling tab
- **Progression**: Select theme property → Adjust values with controls → Preview updates live → Export theme configuration
- **Success criteria**: Color pickers work; typography scales properly; generates valid MUI theme code
- **Progression**: Select theme property → Adjust values with controls or describe style to AI → Preview updates live → Export theme configuration
- **Success criteria**: Color pickers work; typography scales properly; AI themes match descriptions and have good contrast; generates valid MUI theme code
### Project Generator
- **Functionality**: Exports complete Next.js project with all configurations
@@ -48,11 +55,14 @@ This is a full-featured low-code IDE with multiple integrated tools (code editor
- **Success criteria**: Generated project structure is valid; includes package.json; code runs without errors
## Edge Case Handling
- **Empty Projects**: Show welcome screen with quick-start templates when no project exists
- **Empty Projects**: Show welcome screen with quick-start templates when no project exists; AI can generate entire projects from scratch
- **Invalid Prisma Schemas**: Validate models and show inline errors before generating code
- **Circular Component Dependencies**: Detect and warn when component tree has circular references
- **Missing Required Props**: Highlight components with missing required Material UI props
- **Large Files**: Implement virtual scrolling and lazy loading for large component trees and file lists
- **AI Generation Failures**: Provide clear error messages and fallback to manual editing when AI requests fail
- **Rate Limiting**: Handle OpenAI API rate limits gracefully with user-friendly messages
- **Invalid AI Responses**: Validate and sanitize AI-generated code before insertion
## Design Direction
The design should evoke a professional IDE environment while remaining approachable - think Visual Studio Code meets Figma. Clean panels, clear hierarchy, and purposeful use of space to avoid overwhelming users with options.

View File

@@ -1,3 +1,4 @@
{
"templateVersion": 1
}
"templateVersion": 1,
"dbType": "kv"
}

View File

@@ -12,6 +12,7 @@ import { ComponentTreeBuilder } from '@/components/ComponentTreeBuilder'
import { StyleDesigner } from '@/components/StyleDesigner'
import { FileExplorer } from '@/components/FileExplorer'
import { generateNextJSProject, generatePrismaSchema, generateMUITheme } from '@/lib/generators'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
import {
Dialog,
@@ -108,10 +109,31 @@ function App() {
}
const handleGenerateWithAI = async () => {
const description = prompt('Describe the application you want to generate:')
if (!description) return
try {
toast.info('AI generation coming soon!')
toast.info('Generating application with AI...')
const result = await AIService.generateCompleteApp(description)
if (result) {
if (result.files && result.files.length > 0) {
setFiles((currentFiles) => [...(currentFiles || []), ...result.files])
}
if (result.models && result.models.length > 0) {
setModels((currentModels) => [...(currentModels || []), ...result.models])
}
if (result.theme) {
setTheme((currentTheme) => ({ ...(currentTheme || DEFAULT_THEME), ...result.theme }))
}
toast.success('Application generated successfully!')
} else {
toast.error('AI generation failed. Please try again.')
}
} catch (error) {
toast.error('AI generation failed')
console.error(error)
}
}

View File

@@ -1,8 +1,21 @@
import { useState } from 'react'
import Editor from '@monaco-editor/react'
import { Card } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { ProjectFile } from '@/types/project'
import { FileCode, X } from '@phosphor-icons/react'
import { FileCode, X, Sparkle, Info } from '@phosphor-icons/react'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Textarea } from '@/components/ui/textarea'
import { ScrollArea } from '@/components/ui/scroll-area'
interface CodeEditorProps {
files: ProjectFile[]
@@ -19,37 +32,110 @@ export function CodeEditor({
onFileSelect,
onFileClose,
}: CodeEditorProps) {
const [showExplainDialog, setShowExplainDialog] = useState(false)
const [explanation, setExplanation] = useState('')
const [isExplaining, setIsExplaining] = useState(false)
const activeFile = files.find((f) => f.id === activeFileId)
const openFiles = files.filter((f) => f.id === activeFileId || files.length < 5)
const improveCodeWithAI = async () => {
if (!activeFile) return
const instruction = prompt('How would you like to improve this code?')
if (!instruction) return
try {
toast.info('Improving code with AI...')
const improvedCode = await AIService.improveCode(activeFile.content, instruction)
if (improvedCode) {
onFileChange(activeFile.id, improvedCode)
toast.success('Code improved successfully!')
} else {
toast.error('AI improvement failed. Please try again.')
}
} catch (error) {
toast.error('Failed to improve code')
console.error(error)
}
}
const explainCode = async () => {
if (!activeFile) return
try {
setIsExplaining(true)
setShowExplainDialog(true)
setExplanation('Analyzing code...')
const codeExplanation = await AIService.explainCode(activeFile.content)
if (codeExplanation) {
setExplanation(codeExplanation)
} else {
setExplanation('Failed to generate explanation. Please try again.')
}
} catch (error) {
setExplanation('Error generating explanation.')
console.error(error)
} finally {
setIsExplaining(false)
}
}
return (
<div className="h-full flex flex-col">
{openFiles.length > 0 ? (
<>
<div className="flex items-center gap-1 bg-secondary/50 border-b border-border px-2 py-1">
{openFiles.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<FileCode size={16} />
<span>{file.name}</span>
<div className="flex items-center gap-1 bg-secondary/50 border-b border-border px-2 py-1 justify-between">
<div className="flex items-center gap-1">
{openFiles.map((file) => (
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<X size={14} />
<FileCode size={16} />
<span>{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
>
<X size={14} />
</button>
</button>
</button>
))}
))}
</div>
{activeFile && (
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={explainCode}
className="h-7 text-xs"
>
<Info size={14} className="mr-1" />
Explain
</Button>
<Button
size="sm"
variant="ghost"
onClick={improveCodeWithAI}
className="h-7 text-xs"
>
<Sparkle size={14} className="mr-1" weight="duotone" />
Improve
</Button>
</div>
)}
</div>
<div className="flex-1">
{activeFile && (
@@ -80,6 +166,29 @@ export function CodeEditor({
</div>
</div>
)}
<Dialog open={showExplainDialog} onOpenChange={setShowExplainDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Code Explanation</DialogTitle>
<DialogDescription>
AI-generated explanation of {activeFile?.name}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
<div className="p-4 bg-muted rounded-lg">
{isExplaining ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Sparkle size={16} weight="duotone" className="animate-pulse" />
Analyzing code...
</div>
) : (
<p className="whitespace-pre-wrap text-sm">{explanation}</p>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
</div>
)
}

View File

@@ -6,8 +6,10 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Plus, Trash, Tree, CaretRight, CaretDown } from '@phosphor-icons/react'
import { Plus, Trash, Tree, CaretRight, CaretDown, Sparkle } from '@phosphor-icons/react'
import { Textarea } from '@/components/ui/textarea'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
interface ComponentTreeBuilderProps {
components: ComponentNode[]
@@ -129,6 +131,28 @@ export function ComponentTreeBuilder({
setExpandedNodes(newExpanded)
}
const generateComponentWithAI = async () => {
const description = prompt('Describe the component you want to create:')
if (!description) return
try {
toast.info('Generating component with AI...')
const component = await AIService.generateComponent(description)
if (component) {
onComponentsChange([...components, component])
setSelectedNodeId(component.id)
setExpandedNodes(new Set([...Array.from(expandedNodes), component.id]))
toast.success(`Component "${component.name}" created successfully!`)
} else {
toast.error('AI generation failed. Please try again.')
}
} catch (error) {
toast.error('Failed to generate component')
console.error(error)
}
}
const renderTreeNode = (node: ComponentNode, level: number = 0) => {
const isExpanded = expandedNodes.has(node.id)
const isSelected = selectedNodeId === node.id
@@ -174,9 +198,20 @@ export function ComponentTreeBuilder({
<h3 className="font-semibold text-sm uppercase tracking-wide">
Component Tree
</h3>
<Button size="sm" onClick={addRootComponent} className="h-8 w-8 p-0">
<Plus size={16} />
</Button>
<div className="flex gap-1">
<Button
size="sm"
variant="outline"
onClick={generateComponentWithAI}
className="h-8 w-8 p-0"
title="Generate component with AI"
>
<Sparkle size={16} weight="duotone" />
</Button>
<Button size="sm" onClick={addRootComponent} className="h-8 w-8 p-0">
<Plus size={16} />
</Button>
</div>
</div>
<ScrollArea className="flex-1 border rounded-lg">
<div className="p-2 space-y-1">

View File

@@ -3,7 +3,8 @@ import { ProjectFile } from '@/types/project'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { FileCode, FolderOpen, Plus, Folder } from '@phosphor-icons/react'
import { Textarea } from '@/components/ui/textarea'
import { FileCode, FolderOpen, Plus, Folder, Sparkle } from '@phosphor-icons/react'
import {
Dialog,
DialogContent,
@@ -19,6 +20,9 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
interface FileExplorerProps {
files: ProjectFile[]
@@ -36,6 +40,9 @@ export function FileExplorer({
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [newFileName, setNewFileName] = useState('')
const [newFileLanguage, setNewFileLanguage] = useState('typescript')
const [aiDescription, setAiDescription] = useState('')
const [aiFileType, setAiFileType] = useState<'component' | 'page' | 'api' | 'utility'>('component')
const [isGenerating, setIsGenerating] = useState(false)
const handleAddFile = () => {
if (!newFileName.trim()) return
@@ -53,6 +60,43 @@ export function FileExplorer({
setIsAddDialogOpen(false)
}
const handleGenerateFileWithAI = async () => {
if (!aiDescription.trim() || !newFileName.trim()) {
toast.error('Please provide both a filename and description')
return
}
try {
setIsGenerating(true)
toast.info('Generating code with AI...')
const code = await AIService.generateCodeFromDescription(aiDescription, aiFileType)
if (code) {
const newFile: ProjectFile = {
id: `file-${Date.now()}`,
name: newFileName,
path: `/src/${newFileName}`,
content: code,
language: newFileLanguage,
}
onFileAdd(newFile)
setNewFileName('')
setAiDescription('')
setIsAddDialogOpen(false)
toast.success('File generated successfully!')
} else {
toast.error('AI generation failed. Please try again.')
}
} catch (error) {
toast.error('Failed to generate file')
console.error(error)
} finally {
setIsGenerating(false)
}
}
const groupedFiles = files.reduce((acc, file) => {
const dir = file.path.split('/').slice(0, -1).join('/') || '/'
if (!acc[dir]) acc[dir] = []
@@ -77,40 +121,114 @@ export function FileExplorer({
<DialogHeader>
<DialogTitle>Add New File</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label>File Name</Label>
<Input
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder="example.tsx"
onKeyDown={(e) => {
if (e.key === 'Enter') handleAddFile()
}}
/>
</div>
<div className="space-y-2">
<Label>Language</Label>
<Select
value={newFileLanguage}
onValueChange={setNewFileLanguage}
<Tabs defaultValue="manual">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="manual">Manual</TabsTrigger>
<TabsTrigger value="ai">
<Sparkle size={14} className="mr-2" weight="duotone" />
AI Generate
</TabsTrigger>
</TabsList>
<TabsContent value="manual" className="space-y-4">
<div className="space-y-2">
<Label>File Name</Label>
<Input
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder="example.tsx"
onKeyDown={(e) => {
if (e.key === 'Enter') handleAddFile()
}}
/>
</div>
<div className="space-y-2">
<Label>Language</Label>
<Select
value={newFileLanguage}
onValueChange={setNewFileLanguage}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="typescript">TypeScript</SelectItem>
<SelectItem value="javascript">JavaScript</SelectItem>
<SelectItem value="css">CSS</SelectItem>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="prisma">Prisma</SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={handleAddFile} className="w-full">
Add File
</Button>
</TabsContent>
<TabsContent value="ai" className="space-y-4">
<div className="space-y-2">
<Label>File Name</Label>
<Input
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
placeholder="UserCard.tsx"
/>
</div>
<div className="space-y-2">
<Label>File Type</Label>
<Select
value={aiFileType}
onValueChange={(value: any) => setAiFileType(value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="component">Component</SelectItem>
<SelectItem value="page">Page</SelectItem>
<SelectItem value="api">API Route</SelectItem>
<SelectItem value="utility">Utility</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Description</Label>
<Textarea
value={aiDescription}
onChange={(e) => setAiDescription(e.target.value)}
placeholder="Describe what this file should do..."
rows={4}
/>
</div>
<div className="space-y-2">
<Label>Language</Label>
<Select
value={newFileLanguage}
onValueChange={setNewFileLanguage}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="typescript">TypeScript</SelectItem>
<SelectItem value="javascript">JavaScript</SelectItem>
</SelectContent>
</Select>
</div>
<Button
onClick={handleGenerateFileWithAI}
className="w-full"
disabled={isGenerating}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="typescript">TypeScript</SelectItem>
<SelectItem value="javascript">JavaScript</SelectItem>
<SelectItem value="css">CSS</SelectItem>
<SelectItem value="json">JSON</SelectItem>
<SelectItem value="prisma">Prisma</SelectItem>
</SelectContent>
</Select>
</div>
<Button onClick={handleAddFile} className="w-full">
Add File
</Button>
</div>
{isGenerating ? (
<>Generating...</>
) : (
<>
<Sparkle size={16} className="mr-2" weight="duotone" />
Generate with AI
</>
)}
</Button>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
</div>

View File

@@ -7,8 +7,10 @@ import { Label } from '@/components/ui/label'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Switch } from '@/components/ui/switch'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Plus, Trash, Database } from '@phosphor-icons/react'
import { Plus, Trash, Database, Sparkle, Lightbulb } from '@phosphor-icons/react'
import { Badge } from '@/components/ui/badge'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
interface ModelDesignerProps {
models: PrismaModel[]
@@ -97,14 +99,77 @@ export function ModelDesigner({ models, onModelsChange }: ModelDesignerProps) {
})
}
const generateModelWithAI = async () => {
const description = prompt('Describe the database model you want to create:')
if (!description) return
try {
toast.info('Generating model with AI...')
const model = await AIService.generatePrismaModel(description, models)
if (model) {
onModelsChange([...models, model])
setSelectedModelId(model.id)
toast.success(`Model "${model.name}" created successfully!`)
} else {
toast.error('AI generation failed. Please try again.')
}
} catch (error) {
toast.error('Failed to generate model')
console.error(error)
}
}
const suggestFields = async () => {
if (!selectedModel) return
try {
toast.info('Getting field suggestions...')
const existingFieldNames = selectedModel.fields.map(f => f.name)
const suggestions = await AIService.suggestFieldsForModel(selectedModel.name, existingFieldNames)
if (suggestions && suggestions.length > 0) {
const newFields: PrismaField[] = suggestions.map((fieldName, index) => ({
id: `field-${Date.now()}-${index}`,
name: fieldName,
type: 'String',
isRequired: false,
isUnique: false,
isArray: false,
}))
updateModel(selectedModel.id, {
fields: [...selectedModel.fields, ...newFields],
})
toast.success(`Added ${suggestions.length} suggested fields!`)
} else {
toast.error('No suggestions available')
}
} catch (error) {
toast.error('Failed to get suggestions')
console.error(error)
}
}
return (
<div className="h-full flex gap-4 p-6">
<div className="w-64 flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-sm uppercase tracking-wide">Models</h3>
<Button size="sm" onClick={addModel} className="h-8 w-8 p-0">
<Plus size={16} />
</Button>
<div className="flex gap-1">
<Button
size="sm"
variant="outline"
onClick={generateModelWithAI}
className="h-8 w-8 p-0"
title="Generate model with AI"
>
<Sparkle size={16} weight="duotone" />
</Button>
<Button size="sm" onClick={addModel} className="h-8 w-8 p-0">
<Plus size={16} />
</Button>
</div>
</div>
<ScrollArea className="flex-1">
<div className="space-y-2">
@@ -155,10 +220,21 @@ export function ModelDesigner({ models, onModelsChange }: ModelDesignerProps) {
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm uppercase tracking-wide">Fields</h4>
<Button size="sm" onClick={addField}>
<Plus size={16} className="mr-2" />
Add Field
</Button>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={suggestFields}
title="AI suggest fields"
>
<Lightbulb size={16} className="mr-2" weight="duotone" />
Suggest
</Button>
<Button size="sm" onClick={addField}>
<Plus size={16} className="mr-2" />
Add Field
</Button>
</div>
</div>
<ScrollArea className="h-96">

View File

@@ -3,7 +3,10 @@ import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Slider } from '@/components/ui/slider'
import { PaintBrush } from '@phosphor-icons/react'
import { Button } from '@/components/ui/button'
import { PaintBrush, Sparkle } from '@phosphor-icons/react'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
interface StyleDesignerProps {
theme: ThemeConfig
@@ -15,14 +18,40 @@ export function StyleDesigner({ theme, onThemeChange }: StyleDesignerProps) {
onThemeChange({ ...theme, ...updates })
}
const generateThemeWithAI = async () => {
const description = prompt('Describe the visual style you want (e.g., "modern and professional", "vibrant and playful"):')
if (!description) return
try {
toast.info('Generating theme with AI...')
const generatedTheme = await AIService.generateThemeFromDescription(description)
if (generatedTheme) {
onThemeChange({ ...theme, ...generatedTheme })
toast.success('Theme generated successfully!')
} else {
toast.error('AI generation failed. Please try again.')
}
} catch (error) {
toast.error('Failed to generate theme')
console.error(error)
}
}
return (
<div className="h-full overflow-auto p-6">
<div className="max-w-4xl mx-auto space-y-6">
<div>
<h2 className="text-2xl font-bold mb-2">Material UI Theme Designer</h2>
<p className="text-muted-foreground">
Customize your application's visual theme
</p>
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold mb-2">Material UI Theme Designer</h2>
<p className="text-muted-foreground">
Customize your application's visual theme
</p>
</div>
<Button onClick={generateThemeWithAI} variant="outline">
<Sparkle size={16} className="mr-2" weight="duotone" />
Generate with AI
</Button>
</div>
<Card className="p-6">

249
src/lib/ai-service.ts Normal file
View File

@@ -0,0 +1,249 @@
import { PrismaModel, ComponentNode, ThemeConfig, ProjectFile } from '@/types/project'
export class AIService {
static async generateComponent(description: string): Promise<ComponentNode | null> {
try {
const promptText = `You are a React component generator. Generate a component tree structure based on this description: "${description}"
Return a valid JSON object with a single property "component" containing the component structure. The component should follow this format:
{
"component": {
"id": "unique-id",
"type": "Box" (use Material UI component names like Box, Typography, Button, TextField, Grid, Paper, Card, etc.),
"name": "ComponentName",
"props": {
"sx": { "p": 2 } (Material UI sx props)
},
"children": [] (array of nested components following same structure)
}
}
Make sure to use appropriate Material UI components and props. Keep the structure clean and semantic.`
const response = await window.spark.llm(promptText, 'gpt-4o', true)
const parsed = JSON.parse(response)
return parsed.component
} catch (error) {
console.error('AI component generation failed:', error)
return null
}
}
static async generatePrismaModel(description: string, existingModels: PrismaModel[]): Promise<PrismaModel | null> {
try {
const existingModelNames = existingModels.map(m => m.name).join(', ')
const promptText = `You are a Prisma schema designer. Create a database model based on this description: "${description}"
Existing models in the schema: ${existingModelNames || 'none'}
Return a valid JSON object with a single property "model" containing the model structure:
{
"model": {
"id": "unique-id-here",
"name": "ModelName" (PascalCase, singular),
"fields": [
{
"id": "field-id-1",
"name": "id",
"type": "String",
"isRequired": true,
"isUnique": true,
"isArray": false,
"defaultValue": "uuid()"
},
{
"id": "field-id-2",
"name": "fieldName",
"type": "String" (String, Int, Boolean, DateTime, Float, or existing model name for relations),
"isRequired": true,
"isUnique": false,
"isArray": false
}
]
}
}
Include an id field with uuid() default. Add createdAt and updatedAt DateTime fields with @default(now()) and @updatedAt. Use appropriate field types and relationships.`
const response = await window.spark.llm(promptText, 'gpt-4o', true)
const parsed = JSON.parse(response)
return parsed.model
} catch (error) {
console.error('AI model generation failed:', error)
return null
}
}
static async generateCodeFromDescription(
description: string,
fileType: 'component' | 'page' | 'api' | 'utility'
): Promise<string | null> {
try {
const fileTypeInstructions = {
component: "Create a reusable React component with TypeScript. Use Material UI components and proper typing.",
page: "Create a Next.js page component with 'use client' directive if needed. Use Material UI and proper page structure.",
api: "Create a Next.js API route handler with proper types and error handling.",
utility: "Create a utility function with TypeScript types and JSDoc comments."
}
const promptText = `You are a Next.js developer. ${fileTypeInstructions[fileType]}
Description: "${description}"
Generate clean, production-ready code following Next.js 14 and Material UI best practices. Include all necessary imports.
Return ONLY the code without any markdown formatting or explanations.`
const code = await window.spark.llm(promptText, 'gpt-4o', false)
return code.trim()
} catch (error) {
console.error('AI code generation failed:', error)
return null
}
}
static async improveCode(code: string, instruction: string): Promise<string | null> {
try {
const promptText = `You are a code improvement assistant. Improve the following code based on this instruction: "${instruction}"
Original code:
${code}
Return ONLY the improved code without any markdown formatting or explanations.`
const improved = await window.spark.llm(promptText, 'gpt-4o', false)
return improved.trim()
} catch (error) {
console.error('AI code improvement failed:', error)
return null
}
}
static async generateThemeFromDescription(description: string): Promise<Partial<ThemeConfig> | null> {
try {
const promptText = `You are a UI/UX designer. Generate a Material UI theme configuration based on this description: "${description}"
Return a valid JSON object with a single property "theme" containing:
{
"theme": {
"primaryColor": "#hex-color",
"secondaryColor": "#hex-color",
"errorColor": "#hex-color",
"warningColor": "#ff9800",
"successColor": "#hex-color",
"fontFamily": "font-name, fallback",
"fontSize": {
"small": 12,
"medium": 14,
"large": 20
},
"spacing": 8,
"borderRadius": 4
}
}
Choose colors that match the description and ensure good contrast. Use common font stacks.`
const response = await window.spark.llm(promptText, 'gpt-4o', true)
const parsed = JSON.parse(response)
return parsed.theme
} catch (error) {
console.error('AI theme generation failed:', error)
return null
}
}
static async suggestFieldsForModel(modelName: string, existingFields: string[]): Promise<string[] | null> {
try {
const promptText = `You are a database architect. Suggest additional useful fields for a Prisma model named "${modelName}".
Existing fields: ${existingFields.join(', ')}
Return a valid JSON object with a single property "fields" containing an array of field name suggestions (strings only):
{
"fields": ["fieldName1", "fieldName2", "fieldName3"]
}
Suggest 3-5 common fields that would be useful for this model type. Use camelCase naming.`
const response = await window.spark.llm(promptText, 'gpt-4o', true)
const parsed = JSON.parse(response)
return parsed.fields
} catch (error) {
console.error('AI field suggestion failed:', error)
return null
}
}
static async explainCode(code: string): Promise<string | null> {
try {
const promptText = `You are a code teacher. Explain what this code does in simple terms:
${code}
Provide a clear, concise explanation suitable for developers learning the codebase.`
const explanation = await window.spark.llm(promptText, 'gpt-4o', false)
return explanation.trim()
} catch (error) {
console.error('AI code explanation failed:', error)
return null
}
}
static async generateCompleteApp(description: string): Promise<{ files: ProjectFile[], models: PrismaModel[], theme: Partial<ThemeConfig> } | null> {
try {
const promptText = `You are a full-stack application architect. Design a complete Next.js application based on: "${description}"
Return a valid JSON object with properties "files", "models", and "theme":
{
"files": [
{
"id": "unique-id",
"name": "page.tsx",
"path": "/src/app/page.tsx",
"content": "full code content here",
"language": "typescript"
}
],
"models": [
{
"id": "unique-id",
"name": "User",
"fields": [
{
"id": "field-id",
"name": "id",
"type": "String",
"isRequired": true,
"isUnique": true,
"isArray": false,
"defaultValue": "uuid()"
}
]
}
],
"theme": {
"primaryColor": "#1976d2",
"secondaryColor": "#dc004e",
"errorColor": "#f44336",
"warningColor": "#ff9800",
"successColor": "#4caf50",
"fontFamily": "Roboto, Arial, sans-serif",
"fontSize": { "small": 12, "medium": 14, "large": 20 },
"spacing": 8,
"borderRadius": 4
}
}
Create 2-4 essential files for the app structure. Include appropriate Prisma models. Design a cohesive theme.`
const response = await window.spark.llm(promptText, 'gpt-4o', true)
const parsed = JSON.parse(response)
return parsed
} catch (error) {
console.error('AI app generation failed:', error)
return null
}
}
}

26
src/vite-end.d.ts vendored
View File

@@ -1,3 +1,27 @@
/// <reference types="vite/client" />
declare const GITHUB_RUNTIME_PERMANENT_NAME: string
declare const BASE_KV_SERVICE_URL: string
declare const BASE_KV_SERVICE_URL: string
declare global {
interface Window {
spark: {
llmPrompt(strings: TemplateStringsArray, ...values: any[]): string
llm(prompt: string, modelName?: string, jsonMode?: boolean): Promise<string>
user(): Promise<{
avatarUrl: string
email: string
id: string
isOwner: boolean
login: string
}>
kv: {
keys(): Promise<string[]>
get<T>(key: string): Promise<T | undefined>
set<T>(key: string, value: T): Promise<void>
delete(key: string): Promise<void>
}
}
}
var spark: Window['spark']
}