mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-30 16:44:55 +00:00
Generated by Spark: Auto repair on error pages
This commit is contained in:
36
src/App.tsx
36
src/App.tsx
@@ -1,10 +1,12 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { useAutoRepair } from '@/hooks/use-auto-repair'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'
|
||||
import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play } from '@phosphor-icons/react'
|
||||
import { Code, Database, Tree, PaintBrush, Download, Sparkle, Flask, BookOpen, Play, Wrench } from '@phosphor-icons/react'
|
||||
import { ProjectFile, PrismaModel, ComponentNode, ThemeConfig, PlaywrightTest, StorybookStory, UnitTest } from '@/types/project'
|
||||
import { CodeEditor } from '@/components/CodeEditor'
|
||||
import { ModelDesigner } from '@/components/ModelDesigner'
|
||||
@@ -14,6 +16,7 @@ import { FileExplorer } from '@/components/FileExplorer'
|
||||
import { PlaywrightDesigner } from '@/components/PlaywrightDesigner'
|
||||
import { StorybookDesigner } from '@/components/StorybookDesigner'
|
||||
import { UnitTestDesigner } from '@/components/UnitTestDesigner'
|
||||
import { ErrorPanel } from '@/components/ErrorPanel'
|
||||
import { generateNextJSProject, generatePrismaSchema, generateMUITheme, generatePlaywrightTests, generateStorybookStories, generateUnitTests } from '@/lib/generators'
|
||||
import { AIService } from '@/lib/ai-service'
|
||||
import { toast } from 'sonner'
|
||||
@@ -109,6 +112,8 @@ function App() {
|
||||
const safeStorybookStories = storybookStories || []
|
||||
const safeUnitTests = unitTests || []
|
||||
|
||||
const { errors: autoDetectedErrors } = useAutoRepair(safeFiles, false)
|
||||
|
||||
const handleFileChange = (fileId: string, content: string) => {
|
||||
setFiles((currentFiles) =>
|
||||
(currentFiles || []).map((f) => (f.id === fileId ? { ...f, content } : f))
|
||||
@@ -200,6 +205,16 @@ function App() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{autoDetectedErrors.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setActiveTab('errors')}
|
||||
className="border-destructive text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||
>
|
||||
<Wrench size={16} className="mr-2" />
|
||||
{autoDetectedErrors.length} {autoDetectedErrors.length === 1 ? 'Error' : 'Errors'}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={handleGenerateWithAI}>
|
||||
<Sparkle size={16} className="mr-2" weight="duotone" />
|
||||
AI Generate
|
||||
@@ -243,6 +258,15 @@ function App() {
|
||||
<Flask size={18} />
|
||||
Unit Tests
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="errors" className="gap-2">
|
||||
<Wrench size={18} />
|
||||
Error Repair
|
||||
{autoDetectedErrors.length > 0 && (
|
||||
<Badge variant="destructive" className="ml-1">
|
||||
{autoDetectedErrors.length}
|
||||
</Badge>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
@@ -296,6 +320,14 @@ function App() {
|
||||
<TabsContent value="unit-tests" className="h-full m-0">
|
||||
<UnitTestDesigner tests={safeUnitTests} onTestsChange={setUnitTests} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="errors" className="h-full m-0">
|
||||
<ErrorPanel
|
||||
files={safeFiles}
|
||||
onFileChange={handleFileChange}
|
||||
onFileSelect={setActiveFileId}
|
||||
/>
|
||||
</TabsContent>
|
||||
</div>
|
||||
</Tabs>
|
||||
|
||||
|
||||
377
src/components/ErrorPanel.tsx
Normal file
377
src/components/ErrorPanel.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { CodeError } from '@/types/errors'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { ErrorRepairService } from '@/lib/error-repair-service'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
Warning,
|
||||
X,
|
||||
Wrench,
|
||||
CheckCircle,
|
||||
Info,
|
||||
Lightning,
|
||||
FileCode,
|
||||
ArrowRight
|
||||
} from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible'
|
||||
|
||||
interface ErrorPanelProps {
|
||||
files: ProjectFile[]
|
||||
onFileChange: (fileId: string, content: string) => void
|
||||
onFileSelect: (fileId: string) => void
|
||||
}
|
||||
|
||||
export function ErrorPanel({ files, onFileChange, onFileSelect }: ErrorPanelProps) {
|
||||
const [errors, setErrors] = useState<CodeError[]>([])
|
||||
const [isScanning, setIsScanning] = useState(false)
|
||||
const [isRepairing, setIsRepairing] = useState(false)
|
||||
const [autoRepairEnabled, setAutoRepairEnabled] = useState(false)
|
||||
const [expandedErrors, setExpandedErrors] = useState<Set<string>>(new Set())
|
||||
|
||||
const scanForErrors = async () => {
|
||||
setIsScanning(true)
|
||||
try {
|
||||
const allErrors: CodeError[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const fileErrors = await ErrorRepairService.detectErrors(file)
|
||||
allErrors.push(...fileErrors)
|
||||
}
|
||||
|
||||
setErrors(allErrors)
|
||||
|
||||
if (allErrors.length === 0) {
|
||||
toast.success('No errors found!')
|
||||
} else {
|
||||
toast.info(`Found ${allErrors.length} issue${allErrors.length > 1 ? 's' : ''}`)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Error scanning failed')
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsScanning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const repairSingleError = async (error: CodeError) => {
|
||||
const file = files.find(f => f.id === error.fileId)
|
||||
if (!file) return
|
||||
|
||||
setIsRepairing(true)
|
||||
try {
|
||||
const result = await ErrorRepairService.repairCode(file, [error])
|
||||
|
||||
if (result.success && result.fixedCode) {
|
||||
onFileChange(file.id, result.fixedCode)
|
||||
|
||||
setErrors(prev => prev.map(e =>
|
||||
e.id === error.id ? { ...e, isFixed: true, fixedCode: result.fixedCode } : e
|
||||
))
|
||||
|
||||
toast.success(`Fixed: ${error.message}`, {
|
||||
description: result.explanation,
|
||||
})
|
||||
|
||||
await scanForErrors()
|
||||
} else {
|
||||
toast.error('Failed to repair error')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Repair failed')
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsRepairing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const repairAllErrors = async () => {
|
||||
setIsRepairing(true)
|
||||
try {
|
||||
const results = await ErrorRepairService.repairMultipleFiles(files, errors)
|
||||
|
||||
let fixedCount = 0
|
||||
results.forEach((result, fileId) => {
|
||||
if (result.success && result.fixedCode) {
|
||||
onFileChange(fileId, result.fixedCode)
|
||||
fixedCount++
|
||||
}
|
||||
})
|
||||
|
||||
if (fixedCount > 0) {
|
||||
toast.success(`Repaired ${fixedCount} file${fixedCount > 1 ? 's' : ''}`)
|
||||
await scanForErrors()
|
||||
} else {
|
||||
toast.error('No files could be repaired')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Batch repair failed')
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsRepairing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const repairFileWithContext = async (fileId: string) => {
|
||||
const file = files.find(f => f.id === fileId)
|
||||
if (!file) return
|
||||
|
||||
const fileErrors = errors.filter(e => e.fileId === fileId)
|
||||
if (fileErrors.length === 0) return
|
||||
|
||||
setIsRepairing(true)
|
||||
try {
|
||||
const relatedFiles = files.filter(f => f.id !== fileId).slice(0, 3)
|
||||
|
||||
const result = await ErrorRepairService.repairWithContext(file, fileErrors, relatedFiles)
|
||||
|
||||
if (result.success && result.fixedCode) {
|
||||
onFileChange(file.id, result.fixedCode)
|
||||
|
||||
toast.success(`Repaired ${file.name}`, {
|
||||
description: result.explanation,
|
||||
})
|
||||
|
||||
await scanForErrors()
|
||||
} else {
|
||||
toast.error('Failed to repair file')
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Context-aware repair failed')
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsRepairing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleErrorExpanded = (errorId: string) => {
|
||||
setExpandedErrors(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(errorId)) {
|
||||
next.delete(errorId)
|
||||
} else {
|
||||
next.add(errorId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'error':
|
||||
return <X size={16} weight="bold" className="text-destructive" />
|
||||
case 'warning':
|
||||
return <Warning size={16} weight="bold" className="text-yellow-500" />
|
||||
case 'info':
|
||||
return <Info size={16} weight="bold" className="text-blue-500" />
|
||||
default:
|
||||
return <Info size={16} />
|
||||
}
|
||||
}
|
||||
|
||||
const getSeverityColor = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'error':
|
||||
return 'destructive'
|
||||
case 'warning':
|
||||
return 'secondary'
|
||||
case 'info':
|
||||
return 'outline'
|
||||
default:
|
||||
return 'outline'
|
||||
}
|
||||
}
|
||||
|
||||
const errorsByFile = errors.reduce((acc, error) => {
|
||||
if (!acc[error.fileId]) {
|
||||
acc[error.fileId] = []
|
||||
}
|
||||
acc[error.fileId].push(error)
|
||||
return acc
|
||||
}, {} as Record<string, CodeError[]>)
|
||||
|
||||
const errorCount = errors.filter(e => e.severity === 'error').length
|
||||
const warningCount = errors.filter(e => e.severity === 'warning').length
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length > 0 && errors.length === 0) {
|
||||
scanForErrors()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
<div className="border-b border-border bg-card px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wrench size={20} weight="duotone" className="text-accent" />
|
||||
<h2 className="text-lg font-semibold">Error Detection & Repair</h2>
|
||||
</div>
|
||||
{errors.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
{errorCount > 0 && (
|
||||
<Badge variant="destructive">
|
||||
{errorCount} {errorCount === 1 ? 'Error' : 'Errors'}
|
||||
</Badge>
|
||||
)}
|
||||
{warningCount > 0 && (
|
||||
<Badge variant="secondary">
|
||||
{warningCount} {warningCount === 1 ? 'Warning' : 'Warnings'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={scanForErrors}
|
||||
disabled={isScanning || isRepairing}
|
||||
>
|
||||
<Lightning size={16} className="mr-2" />
|
||||
{isScanning ? 'Scanning...' : 'Scan'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={repairAllErrors}
|
||||
disabled={errors.length === 0 || isRepairing || isScanning}
|
||||
>
|
||||
<Wrench size={16} className="mr-2" />
|
||||
{isRepairing ? 'Repairing...' : 'Repair All'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-6">
|
||||
{errors.length === 0 && !isScanning && (
|
||||
<Card className="p-8 text-center">
|
||||
<CheckCircle size={48} weight="duotone" className="text-green-500 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-semibold mb-2">No Issues Found</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
All files are looking good! Click "Scan" to check again.
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{isScanning && (
|
||||
<Card className="p-8 text-center">
|
||||
<Lightning size={48} weight="duotone" className="text-accent mx-auto mb-4 animate-pulse" />
|
||||
<h3 className="text-lg font-semibold mb-2">Scanning Files...</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Analyzing your code for errors and issues
|
||||
</p>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{errors.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{Object.entries(errorsByFile).map(([fileId, fileErrors]) => {
|
||||
const file = files.find(f => f.id === fileId)
|
||||
if (!file) return null
|
||||
|
||||
return (
|
||||
<Card key={fileId} className="overflow-hidden">
|
||||
<div className="bg-muted px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileCode size={18} weight="duotone" />
|
||||
<span className="font-medium">{file.name}</span>
|
||||
<Badge variant="outline">
|
||||
{fileErrors.length} {fileErrors.length === 1 ? 'issue' : 'issues'}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onFileSelect(fileId)}
|
||||
>
|
||||
<ArrowRight size={14} className="mr-1" />
|
||||
Open
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => repairFileWithContext(fileId)}
|
||||
disabled={isRepairing}
|
||||
>
|
||||
<Wrench size={14} className="mr-1" />
|
||||
Repair
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-2">
|
||||
{fileErrors.map((error) => (
|
||||
<Collapsible key={error.id}>
|
||||
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-muted/50 transition-colors">
|
||||
<div className="mt-0.5">
|
||||
{getSeverityIcon(error.severity)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant={getSeverityColor(error.severity) as any} className="text-xs">
|
||||
{error.type}
|
||||
</Badge>
|
||||
{error.line && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Line {error.line}
|
||||
</span>
|
||||
)}
|
||||
{error.isFixed && (
|
||||
<Badge variant="outline" className="text-green-500 border-green-500">
|
||||
<CheckCircle size={12} className="mr-1" />
|
||||
Fixed
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm mb-2">{error.message}</p>
|
||||
{error.code && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-auto p-0 text-xs text-accent hover:text-accent/80"
|
||||
>
|
||||
{expandedErrors.has(error.id) ? 'Hide' : 'Show'} code
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => repairSingleError(error)}
|
||||
disabled={isRepairing || error.isFixed}
|
||||
>
|
||||
<Wrench size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
{error.code && (
|
||||
<CollapsibleContent>
|
||||
<div className="ml-8 mt-2 p-3 bg-muted rounded text-xs font-mono">
|
||||
{error.code}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
src/hooks/use-auto-repair.ts
Normal file
48
src/hooks/use-auto-repair.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { CodeError } from '@/types/errors'
|
||||
import { ErrorRepairService } from '@/lib/error-repair-service'
|
||||
|
||||
export function useAutoRepair(
|
||||
files: ProjectFile[],
|
||||
enabled: boolean = false
|
||||
) {
|
||||
const [errors, setErrors] = useState<CodeError[]>([])
|
||||
const [isScanning, setIsScanning] = useState(false)
|
||||
|
||||
const scanFiles = useCallback(async () => {
|
||||
if (!enabled || files.length === 0) return
|
||||
|
||||
setIsScanning(true)
|
||||
try {
|
||||
const allErrors: CodeError[] = []
|
||||
|
||||
for (const file of files) {
|
||||
const fileErrors = await ErrorRepairService.detectErrors(file)
|
||||
allErrors.push(...fileErrors)
|
||||
}
|
||||
|
||||
setErrors(allErrors)
|
||||
} catch (error) {
|
||||
console.error('Auto-scan failed:', error)
|
||||
} finally {
|
||||
setIsScanning(false)
|
||||
}
|
||||
}, [files, enabled])
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
scanFiles()
|
||||
}, 2000)
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
}
|
||||
}, [files, enabled, scanFiles])
|
||||
|
||||
return {
|
||||
errors,
|
||||
isScanning,
|
||||
scanFiles,
|
||||
}
|
||||
}
|
||||
316
src/lib/error-repair-service.ts
Normal file
316
src/lib/error-repair-service.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import { CodeError, ErrorRepairResult } from '@/types/errors'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
|
||||
/**
|
||||
* ErrorRepairService - AI-powered code error detection and repair
|
||||
*
|
||||
* Features:
|
||||
* - detectErrors: Scans files for syntax, import, type, and lint errors
|
||||
* - repairCode: Fixes errors in a single file using AI
|
||||
* - repairMultipleFiles: Batch repair multiple files with errors
|
||||
* - repairWithContext: Context-aware repair using related files for better accuracy
|
||||
*
|
||||
* Error Types Detected:
|
||||
* - Syntax errors (missing parentheses, unbalanced braces)
|
||||
* - Import errors (unused imports)
|
||||
* - Type errors (use of 'any' type)
|
||||
* - Lint errors (var instead of const/let)
|
||||
*/
|
||||
export class ErrorRepairService {
|
||||
static async detectErrors(file: ProjectFile): Promise<CodeError[]> {
|
||||
const errors: CodeError[] = []
|
||||
|
||||
const syntaxErrors = this.detectSyntaxErrors(file)
|
||||
const importErrors = this.detectImportErrors(file)
|
||||
const typeErrors = this.detectBasicTypeErrors(file)
|
||||
|
||||
return [...syntaxErrors, ...importErrors, ...typeErrors]
|
||||
}
|
||||
|
||||
private static detectSyntaxErrors(file: ProjectFile): CodeError[] {
|
||||
const errors: CodeError[] = []
|
||||
const lines = file.content.split('\n')
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (file.language === 'typescript' || file.language === 'javascript') {
|
||||
if (line.includes('function') && !line.includes('(') && line.includes('{')) {
|
||||
errors.push({
|
||||
id: `syntax-${file.id}-${index}`,
|
||||
fileId: file.id,
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
line: index + 1,
|
||||
message: 'Function declaration missing parentheses',
|
||||
severity: 'error',
|
||||
type: 'syntax',
|
||||
code: line.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
const openBraces = (line.match(/{/g) || []).length
|
||||
const closeBraces = (line.match(/}/g) || []).length
|
||||
if (openBraces !== closeBraces && !line.trim().startsWith('//')) {
|
||||
const nextLine = lines[index + 1]
|
||||
if (nextLine && !nextLine.includes('}') && !nextLine.includes('{')) {
|
||||
errors.push({
|
||||
id: `syntax-${file.id}-${index}`,
|
||||
fileId: file.id,
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
line: index + 1,
|
||||
message: 'Possible unbalanced braces',
|
||||
severity: 'warning',
|
||||
type: 'syntax',
|
||||
code: line.trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
private static detectImportErrors(file: ProjectFile): CodeError[] {
|
||||
const errors: CodeError[] = []
|
||||
const lines = file.content.split('\n')
|
||||
|
||||
const importedNames = new Set<string>()
|
||||
const usedNames = new Set<string>()
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (line.trim().startsWith('import ')) {
|
||||
const importMatch = line.match(/import\s+(?:{([^}]+)}|(\w+))\s+from/)
|
||||
if (importMatch) {
|
||||
const namedImports = importMatch[1]
|
||||
const defaultImport = importMatch[2]
|
||||
|
||||
if (namedImports) {
|
||||
namedImports.split(',').forEach(name => {
|
||||
importedNames.add(name.trim().split(' as ')[0])
|
||||
})
|
||||
}
|
||||
if (defaultImport) {
|
||||
importedNames.add(defaultImport.trim())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
importedNames.forEach(name => {
|
||||
const regex = new RegExp(`\\b${name}\\b`)
|
||||
if (regex.test(line)) {
|
||||
usedNames.add(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
importedNames.forEach(name => {
|
||||
if (!usedNames.has(name)) {
|
||||
errors.push({
|
||||
id: `import-${file.id}-${name}`,
|
||||
fileId: file.id,
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
message: `Unused import: ${name}`,
|
||||
severity: 'warning',
|
||||
type: 'import',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
private static detectBasicTypeErrors(file: ProjectFile): CodeError[] {
|
||||
const errors: CodeError[] = []
|
||||
|
||||
if (file.language !== 'typescript') return errors
|
||||
|
||||
const lines = file.content.split('\n')
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (line.includes('any') && !line.trim().startsWith('//')) {
|
||||
errors.push({
|
||||
id: `type-${file.id}-${index}`,
|
||||
fileId: file.id,
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
line: index + 1,
|
||||
message: 'Use of "any" type - consider using a more specific type',
|
||||
severity: 'warning',
|
||||
type: 'type',
|
||||
code: line.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
const varMatch = line.match(/\bvar\s+/)
|
||||
if (varMatch) {
|
||||
errors.push({
|
||||
id: `lint-${file.id}-${index}`,
|
||||
fileId: file.id,
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
line: index + 1,
|
||||
message: 'Use "const" or "let" instead of "var"',
|
||||
severity: 'warning',
|
||||
type: 'lint',
|
||||
code: line.trim(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
static async repairCode(file: ProjectFile, errors: CodeError[]): Promise<ErrorRepairResult> {
|
||||
if (errors.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
fixedCode: file.content,
|
||||
explanation: 'No errors detected',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const errorDescriptions = errors
|
||||
.map(err => `Line ${err.line || 'unknown'}: ${err.message} - "${err.code || 'N/A'}"`)
|
||||
.join('\n')
|
||||
|
||||
const promptText = `You are a code repair assistant. Fix the following errors in this code:
|
||||
|
||||
File: ${file.name} (${file.language})
|
||||
|
||||
Errors:
|
||||
${errorDescriptions}
|
||||
|
||||
Original code:
|
||||
\`\`\`${file.language}
|
||||
${file.content}
|
||||
\`\`\`
|
||||
|
||||
Return a valid JSON object with the following structure:
|
||||
{
|
||||
"fixedCode": "the complete fixed code here",
|
||||
"explanation": "brief explanation of what was fixed",
|
||||
"remainingIssues": ["any issues that couldn't be fixed"]
|
||||
}
|
||||
|
||||
Rules:
|
||||
- Fix all syntax errors, import errors, and type errors
|
||||
- Remove unused imports
|
||||
- Replace "any" types with appropriate types
|
||||
- Replace "var" with "const" or "let"
|
||||
- Maintain code functionality and structure
|
||||
- Keep the same imports style and formatting
|
||||
- Return the COMPLETE file content, not just the fixes`
|
||||
|
||||
const response = await window.spark.llm(promptText, 'gpt-4o', true)
|
||||
const parsed = JSON.parse(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fixedCode: parsed.fixedCode,
|
||||
explanation: parsed.explanation,
|
||||
remainingIssues: parsed.remainingIssues || [],
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auto-repair failed:', error)
|
||||
return {
|
||||
success: false,
|
||||
explanation: 'Failed to repair code automatically',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async repairMultipleFiles(files: ProjectFile[], allErrors: CodeError[]): Promise<Map<string, ErrorRepairResult>> {
|
||||
const results = new Map<string, ErrorRepairResult>()
|
||||
|
||||
const fileErrorMap = new Map<string, CodeError[]>()
|
||||
allErrors.forEach(error => {
|
||||
if (!fileErrorMap.has(error.fileId)) {
|
||||
fileErrorMap.set(error.fileId, [])
|
||||
}
|
||||
fileErrorMap.get(error.fileId)!.push(error)
|
||||
})
|
||||
|
||||
for (const file of files) {
|
||||
const fileErrors = fileErrorMap.get(file.id) || []
|
||||
if (fileErrors.length > 0) {
|
||||
const result = await this.repairCode(file, fileErrors)
|
||||
results.set(file.id, result)
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
static async repairWithContext(
|
||||
file: ProjectFile,
|
||||
errors: CodeError[],
|
||||
relatedFiles: ProjectFile[]
|
||||
): Promise<ErrorRepairResult> {
|
||||
if (errors.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
fixedCode: file.content,
|
||||
explanation: 'No errors detected',
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const errorDescriptions = errors
|
||||
.map(err => `Line ${err.line || 'unknown'}: ${err.message} - "${err.code || 'N/A'}"`)
|
||||
.join('\n')
|
||||
|
||||
const relatedFilesContext = relatedFiles
|
||||
.map(f => `${f.path}:\n\`\`\`${f.language}\n${f.content.slice(0, 500)}...\n\`\`\``)
|
||||
.join('\n\n')
|
||||
|
||||
const promptText = `You are a code repair assistant. Fix the following errors in this code, considering the context of related files:
|
||||
|
||||
File: ${file.name} (${file.language})
|
||||
|
||||
Errors:
|
||||
${errorDescriptions}
|
||||
|
||||
Related files for context:
|
||||
${relatedFilesContext}
|
||||
|
||||
Original code to fix:
|
||||
\`\`\`${file.language}
|
||||
${file.content}
|
||||
\`\`\`
|
||||
|
||||
Return a valid JSON object with the following structure:
|
||||
{
|
||||
"fixedCode": "the complete fixed code here",
|
||||
"explanation": "brief explanation of what was fixed",
|
||||
"remainingIssues": ["any issues that couldn't be fixed"]
|
||||
}
|
||||
|
||||
Rules:
|
||||
- Fix all syntax errors, import errors, and type errors
|
||||
- Ensure imports match what's exported in related files
|
||||
- Use consistent naming and patterns from related files
|
||||
- Replace "any" types with appropriate types from context
|
||||
- Maintain code functionality and structure
|
||||
- Return the COMPLETE file content, not just the fixes`
|
||||
|
||||
const response = await window.spark.llm(promptText, 'gpt-4o', true)
|
||||
const parsed = JSON.parse(response)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
fixedCode: parsed.fixedCode,
|
||||
explanation: parsed.explanation,
|
||||
remainingIssues: parsed.remainingIssues || [],
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auto-repair with context failed:', error)
|
||||
return {
|
||||
success: false,
|
||||
explanation: 'Failed to repair code automatically',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/types/errors.ts
Normal file
23
src/types/errors.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface CodeError {
|
||||
id: string
|
||||
fileId: string
|
||||
fileName: string
|
||||
filePath: string
|
||||
line?: number
|
||||
column?: number
|
||||
message: string
|
||||
severity: 'error' | 'warning' | 'info'
|
||||
type: 'syntax' | 'type' | 'runtime' | 'lint' | 'import'
|
||||
code?: string
|
||||
suggestion?: string
|
||||
isFixed?: boolean
|
||||
originalCode?: string
|
||||
fixedCode?: string
|
||||
}
|
||||
|
||||
export interface ErrorRepairResult {
|
||||
success: boolean
|
||||
fixedCode?: string
|
||||
explanation?: string
|
||||
remainingIssues?: string[]
|
||||
}
|
||||
Reference in New Issue
Block a user