mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Generated by Spark: OpenAI integration could help with various pages in the app
This commit is contained in:
44
PRD.md
44
PRD.md
@@ -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.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{
|
||||
"templateVersion": 1
|
||||
}
|
||||
"templateVersion": 1,
|
||||
"dbType": "kv"
|
||||
}
|
||||
24
src/App.tsx
24
src/App.tsx
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
249
src/lib/ai-service.ts
Normal 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
26
src/vite-end.d.ts
vendored
@@ -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']
|
||||
}
|
||||
Reference in New Issue
Block a user