mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Merge pull request #40 from johndoe6345789/codex/split-errorpanel-into-subcomponents
Refactor ErrorPanel into subcomponents
This commit is contained in:
@@ -1,38 +1,10 @@
|
||||
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 { ScrollArea } from '@/components/ui/scroll-area'
|
||||
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'
|
||||
import {
|
||||
Badge,
|
||||
ActionButton,
|
||||
Stack,
|
||||
Flex,
|
||||
Heading,
|
||||
Text,
|
||||
EmptyState,
|
||||
IconText,
|
||||
Code,
|
||||
StatusIcon,
|
||||
PanelHeader
|
||||
} from '@/components/atoms'
|
||||
import errorPanelCopy from '@/data/error-panel.json'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { ErrorPanelHeader } from '@/components/error-panel/ErrorPanelHeader'
|
||||
import { ErrorPanelEmptyState } from '@/components/error-panel/ErrorPanelEmptyState'
|
||||
import { ErrorPanelFileList } from '@/components/error-panel/ErrorPanelFileList'
|
||||
import { useErrorPanelState } from '@/components/error-panel/useErrorPanelState'
|
||||
|
||||
interface ErrorPanelProps {
|
||||
files: ProjectFile[]
|
||||
@@ -41,337 +13,68 @@ interface ErrorPanelProps {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}, [])
|
||||
const {
|
||||
errors,
|
||||
errorsByFile,
|
||||
errorCount,
|
||||
warningCount,
|
||||
isScanning,
|
||||
isRepairing,
|
||||
scanForErrors,
|
||||
repairAllErrors,
|
||||
repairFileWithContext,
|
||||
repairSingleError,
|
||||
} = useErrorPanelState({ files, onFileChange })
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col bg-background">
|
||||
<div className="border-b border-border bg-card px-6 py-4">
|
||||
<Flex align="center" justify="between">
|
||||
<Flex align="center" gap="md">
|
||||
<IconText icon={<Wrench size={20} weight="duotone" className="text-accent" />}>
|
||||
<Heading level={3}>Error Detection & Repair</Heading>
|
||||
</IconText>
|
||||
{errors.length > 0 && (
|
||||
<Flex gap="sm">
|
||||
{errorCount > 0 && (
|
||||
<Badge variant="destructive">
|
||||
{errorCount} {errorCount === 1 ? 'Error' : 'Errors'}
|
||||
</Badge>
|
||||
)}
|
||||
{warningCount > 0 && (
|
||||
<Badge variant="secondary">
|
||||
{warningCount} {warningCount === 1 ? 'Warning' : 'Warnings'}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex gap="sm">
|
||||
<ActionButton
|
||||
icon={<Lightning size={16} />}
|
||||
label={isScanning ? 'Scanning...' : 'Scan'}
|
||||
onClick={scanForErrors}
|
||||
disabled={isScanning || isRepairing}
|
||||
variant="outline"
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<Wrench size={16} />}
|
||||
label={isRepairing ? 'Repairing...' : 'Repair All'}
|
||||
onClick={repairAllErrors}
|
||||
disabled={errors.length === 0 || isRepairing || isScanning}
|
||||
variant="default"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
<ErrorPanelHeader
|
||||
title={errorPanelCopy.header.title}
|
||||
scanLabel={errorPanelCopy.header.scan}
|
||||
scanningLabel={errorPanelCopy.header.scanning}
|
||||
repairAllLabel={errorPanelCopy.header.repairAll}
|
||||
repairingLabel={errorPanelCopy.header.repairing}
|
||||
errorCount={errorCount}
|
||||
warningCount={warningCount}
|
||||
errorLabel={errorPanelCopy.counts.errorSingular}
|
||||
errorsLabel={errorPanelCopy.counts.errorPlural}
|
||||
warningLabel={errorPanelCopy.counts.warningSingular}
|
||||
warningsLabel={errorPanelCopy.counts.warningPlural}
|
||||
isScanning={isScanning}
|
||||
isRepairing={isRepairing}
|
||||
onScan={scanForErrors}
|
||||
onRepairAll={repairAllErrors}
|
||||
/>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-6">
|
||||
{errors.length === 0 && !isScanning && (
|
||||
<EmptyState
|
||||
icon={<CheckCircle size={48} weight="duotone" className="text-green-500" />}
|
||||
title="No Issues Found"
|
||||
description="All files are looking good! Click 'Scan' to check again."
|
||||
/>
|
||||
)}
|
||||
|
||||
{isScanning && (
|
||||
<EmptyState
|
||||
icon={<Lightning size={48} weight="duotone" className="text-accent animate-pulse" />}
|
||||
title="Scanning Files..."
|
||||
description="Analyzing your code for errors and issues"
|
||||
{errors.length === 0 && (
|
||||
<ErrorPanelEmptyState
|
||||
isScanning={isScanning}
|
||||
noIssuesTitle={errorPanelCopy.emptyStates.noIssuesTitle}
|
||||
noIssuesDescription={errorPanelCopy.emptyStates.noIssuesDescription}
|
||||
scanningTitle={errorPanelCopy.emptyStates.scanningTitle}
|
||||
scanningDescription={errorPanelCopy.emptyStates.scanningDescription}
|
||||
/>
|
||||
)}
|
||||
|
||||
{errors.length > 0 && (
|
||||
<Stack direction="vertical" spacing="md">
|
||||
{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 align="center" justify="between">
|
||||
<Flex align="center" gap="sm">
|
||||
<FileCode size={18} weight="duotone" />
|
||||
<Text className="font-medium">{file.name}</Text>
|
||||
<Badge variant="outline" size="sm">
|
||||
{fileErrors.length} {fileErrors.length === 1 ? 'issue' : 'issues'}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Flex gap="sm">
|
||||
<ActionButton
|
||||
icon={<ArrowRight size={14} />}
|
||||
label="Open"
|
||||
onClick={() => onFileSelect(fileId)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<Wrench size={14} />}
|
||||
label="Repair"
|
||||
onClick={() => repairFileWithContext(fileId)}
|
||||
disabled={isRepairing}
|
||||
variant="default"
|
||||
size="sm"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</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">
|
||||
<Flex align="center" gap="sm" className="mb-1">
|
||||
<Badge variant={getSeverityColor(error.severity) as any} size="sm">
|
||||
{error.type}
|
||||
</Badge>
|
||||
{error.line && (
|
||||
<Text variant="caption">
|
||||
Line {error.line}
|
||||
</Text>
|
||||
)}
|
||||
{error.isFixed && (
|
||||
<Badge variant="outline" size="sm" className="text-green-500 border-green-500">
|
||||
<CheckCircle size={12} className="mr-1" />
|
||||
Fixed
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
<Text variant="body" className="mb-2">{error.message}</Text>
|
||||
{error.code && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<button
|
||||
className="text-xs text-accent hover:text-accent/80 underline"
|
||||
>
|
||||
{expandedErrors.has(error.id) ? 'Hide' : 'Show'} code
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton
|
||||
icon={<Wrench size={14} />}
|
||||
label=""
|
||||
onClick={() => repairSingleError(error)}
|
||||
disabled={isRepairing || error.isFixed}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
{error.code && (
|
||||
<CollapsibleContent>
|
||||
<div className="ml-8 mt-2 p-3 bg-muted rounded">
|
||||
<Code className="text-xs">{error.code}</Code>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
<ErrorPanelFileList
|
||||
files={files}
|
||||
errorsByFile={errorsByFile}
|
||||
issueLabel={errorPanelCopy.counts.issueSingular}
|
||||
issuesLabel={errorPanelCopy.counts.issuePlural}
|
||||
openLabel={errorPanelCopy.actions.open}
|
||||
repairLabel={errorPanelCopy.actions.repair}
|
||||
lineLabel={errorPanelCopy.labels.line}
|
||||
fixedLabel={errorPanelCopy.actions.fixed}
|
||||
showCodeLabel={errorPanelCopy.actions.showCode}
|
||||
hideCodeLabel={errorPanelCopy.actions.hideCode}
|
||||
isRepairing={isRepairing}
|
||||
onFileSelect={onFileSelect}
|
||||
onRepairFile={repairFileWithContext}
|
||||
onRepairError={repairSingleError}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
36
src/components/error-panel/ErrorPanelEmptyState.tsx
Normal file
36
src/components/error-panel/ErrorPanelEmptyState.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { CheckCircle, Lightning } from '@phosphor-icons/react'
|
||||
import { EmptyState } from '@/components/atoms'
|
||||
|
||||
interface ErrorPanelEmptyStateProps {
|
||||
isScanning: boolean
|
||||
noIssuesTitle: string
|
||||
noIssuesDescription: string
|
||||
scanningTitle: string
|
||||
scanningDescription: string
|
||||
}
|
||||
|
||||
export function ErrorPanelEmptyState({
|
||||
isScanning,
|
||||
noIssuesTitle,
|
||||
noIssuesDescription,
|
||||
scanningTitle,
|
||||
scanningDescription,
|
||||
}: ErrorPanelEmptyStateProps) {
|
||||
if (isScanning) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={<Lightning size={48} weight="duotone" className="text-accent animate-pulse" />}
|
||||
title={scanningTitle}
|
||||
description={scanningDescription}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
icon={<CheckCircle size={48} weight="duotone" className="text-green-500" />}
|
||||
title={noIssuesTitle}
|
||||
description={noIssuesDescription}
|
||||
/>
|
||||
)
|
||||
}
|
||||
104
src/components/error-panel/ErrorPanelErrorItem.tsx
Normal file
104
src/components/error-panel/ErrorPanelErrorItem.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useState } from 'react'
|
||||
import { CheckCircle, Info, Warning, Wrench, X } from '@phosphor-icons/react'
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
||||
import { ActionButton, Badge, Code, Flex, Text } from '@/components/atoms'
|
||||
import { CodeError } from '@/types/errors'
|
||||
|
||||
interface ErrorPanelErrorItemProps {
|
||||
error: CodeError
|
||||
isRepairing: boolean
|
||||
lineLabel: string
|
||||
fixedLabel: string
|
||||
showCodeLabel: string
|
||||
hideCodeLabel: string
|
||||
onRepair: (error: CodeError) => void
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
export function ErrorPanelErrorItem({
|
||||
error,
|
||||
isRepairing,
|
||||
lineLabel,
|
||||
fixedLabel,
|
||||
showCodeLabel,
|
||||
hideCodeLabel,
|
||||
onRepair,
|
||||
}: ErrorPanelErrorItemProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
return (
|
||||
<Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
|
||||
<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">
|
||||
<Flex align="center" gap="sm" className="mb-1">
|
||||
<Badge variant={getSeverityColor(error.severity) as any} size="sm">
|
||||
{error.type}
|
||||
</Badge>
|
||||
{error.line && (
|
||||
<Text variant="caption">
|
||||
{lineLabel} {error.line}
|
||||
</Text>
|
||||
)}
|
||||
{error.isFixed && (
|
||||
<Badge variant="outline" size="sm" className="text-green-500 border-green-500">
|
||||
<CheckCircle size={12} className="mr-1" />
|
||||
{fixedLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
<Text variant="body" className="mb-2">
|
||||
{error.message}
|
||||
</Text>
|
||||
{error.code && (
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="text-xs text-accent hover:text-accent/80 underline">
|
||||
{isExpanded ? hideCodeLabel : showCodeLabel}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
)}
|
||||
</div>
|
||||
<ActionButton
|
||||
icon={<Wrench size={14} />}
|
||||
label=""
|
||||
onClick={() => onRepair(error)}
|
||||
disabled={isRepairing || error.isFixed}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
{error.code && (
|
||||
<CollapsibleContent>
|
||||
<div className="ml-8 mt-2 p-3 bg-muted rounded">
|
||||
<Code className="text-xs">{error.code}</Code>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
)}
|
||||
</Collapsible>
|
||||
)
|
||||
}
|
||||
88
src/components/error-panel/ErrorPanelFileCard.tsx
Normal file
88
src/components/error-panel/ErrorPanelFileCard.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ArrowRight, FileCode, Wrench } from '@phosphor-icons/react'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ActionButton, Badge, Flex, Text } from '@/components/atoms'
|
||||
import { CodeError } from '@/types/errors'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { ErrorPanelErrorItem } from './ErrorPanelErrorItem'
|
||||
|
||||
interface ErrorPanelFileCardProps {
|
||||
file: ProjectFile
|
||||
errors: CodeError[]
|
||||
issueLabel: string
|
||||
issuesLabel: string
|
||||
openLabel: string
|
||||
repairLabel: string
|
||||
lineLabel: string
|
||||
fixedLabel: string
|
||||
showCodeLabel: string
|
||||
hideCodeLabel: string
|
||||
isRepairing: boolean
|
||||
onFileSelect: (fileId: string) => void
|
||||
onRepairFile: (fileId: string) => void
|
||||
onRepairError: (error: CodeError) => void
|
||||
}
|
||||
|
||||
export function ErrorPanelFileCard({
|
||||
file,
|
||||
errors,
|
||||
issueLabel,
|
||||
issuesLabel,
|
||||
openLabel,
|
||||
repairLabel,
|
||||
lineLabel,
|
||||
fixedLabel,
|
||||
showCodeLabel,
|
||||
hideCodeLabel,
|
||||
isRepairing,
|
||||
onFileSelect,
|
||||
onRepairFile,
|
||||
onRepairError,
|
||||
}: ErrorPanelFileCardProps) {
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
<div className="bg-muted px-4 py-3">
|
||||
<Flex align="center" justify="between">
|
||||
<Flex align="center" gap="sm">
|
||||
<FileCode size={18} weight="duotone" />
|
||||
<Text className="font-medium">{file.name}</Text>
|
||||
<Badge variant="outline" size="sm">
|
||||
{errors.length} {errors.length === 1 ? issueLabel : issuesLabel}
|
||||
</Badge>
|
||||
</Flex>
|
||||
<Flex gap="sm">
|
||||
<ActionButton
|
||||
icon={<ArrowRight size={14} />}
|
||||
label={openLabel}
|
||||
onClick={() => onFileSelect(file.id)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<Wrench size={14} />}
|
||||
label={repairLabel}
|
||||
onClick={() => onRepairFile(file.id)}
|
||||
disabled={isRepairing}
|
||||
variant="default"
|
||||
size="sm"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-2">
|
||||
{errors.map((error) => (
|
||||
<ErrorPanelErrorItem
|
||||
key={error.id}
|
||||
error={error}
|
||||
isRepairing={isRepairing}
|
||||
lineLabel={lineLabel}
|
||||
fixedLabel={fixedLabel}
|
||||
showCodeLabel={showCodeLabel}
|
||||
hideCodeLabel={hideCodeLabel}
|
||||
onRepair={onRepairError}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
67
src/components/error-panel/ErrorPanelFileList.tsx
Normal file
67
src/components/error-panel/ErrorPanelFileList.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Stack } from '@/components/atoms'
|
||||
import { CodeError } from '@/types/errors'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { ErrorPanelFileCard } from './ErrorPanelFileCard'
|
||||
|
||||
interface ErrorPanelFileListProps {
|
||||
files: ProjectFile[]
|
||||
errorsByFile: Record<string, CodeError[]>
|
||||
issueLabel: string
|
||||
issuesLabel: string
|
||||
openLabel: string
|
||||
repairLabel: string
|
||||
lineLabel: string
|
||||
fixedLabel: string
|
||||
showCodeLabel: string
|
||||
hideCodeLabel: string
|
||||
isRepairing: boolean
|
||||
onFileSelect: (fileId: string) => void
|
||||
onRepairFile: (fileId: string) => void
|
||||
onRepairError: (error: CodeError) => void
|
||||
}
|
||||
|
||||
export function ErrorPanelFileList({
|
||||
files,
|
||||
errorsByFile,
|
||||
issueLabel,
|
||||
issuesLabel,
|
||||
openLabel,
|
||||
repairLabel,
|
||||
lineLabel,
|
||||
fixedLabel,
|
||||
showCodeLabel,
|
||||
hideCodeLabel,
|
||||
isRepairing,
|
||||
onFileSelect,
|
||||
onRepairFile,
|
||||
onRepairError,
|
||||
}: ErrorPanelFileListProps) {
|
||||
return (
|
||||
<Stack direction="vertical" spacing="md">
|
||||
{Object.entries(errorsByFile).map(([fileId, fileErrors]) => {
|
||||
const file = files.find((entry) => entry.id === fileId)
|
||||
if (!file) return null
|
||||
|
||||
return (
|
||||
<ErrorPanelFileCard
|
||||
key={fileId}
|
||||
file={file}
|
||||
errors={fileErrors}
|
||||
issueLabel={issueLabel}
|
||||
issuesLabel={issuesLabel}
|
||||
openLabel={openLabel}
|
||||
repairLabel={repairLabel}
|
||||
lineLabel={lineLabel}
|
||||
fixedLabel={fixedLabel}
|
||||
showCodeLabel={showCodeLabel}
|
||||
hideCodeLabel={hideCodeLabel}
|
||||
isRepairing={isRepairing}
|
||||
onFileSelect={onFileSelect}
|
||||
onRepairFile={onRepairFile}
|
||||
onRepairError={onRepairError}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
80
src/components/error-panel/ErrorPanelHeader.tsx
Normal file
80
src/components/error-panel/ErrorPanelHeader.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Lightning, Wrench } from '@phosphor-icons/react'
|
||||
import { Badge, ActionButton, Flex, Heading, IconText } from '@/components/atoms'
|
||||
|
||||
interface ErrorPanelHeaderProps {
|
||||
title: string
|
||||
scanLabel: string
|
||||
scanningLabel: string
|
||||
repairAllLabel: string
|
||||
repairingLabel: string
|
||||
errorCount: number
|
||||
warningCount: number
|
||||
errorLabel: string
|
||||
errorsLabel: string
|
||||
warningLabel: string
|
||||
warningsLabel: string
|
||||
isScanning: boolean
|
||||
isRepairing: boolean
|
||||
onScan: () => void
|
||||
onRepairAll: () => void
|
||||
}
|
||||
|
||||
export function ErrorPanelHeader({
|
||||
title,
|
||||
scanLabel,
|
||||
scanningLabel,
|
||||
repairAllLabel,
|
||||
repairingLabel,
|
||||
errorCount,
|
||||
warningCount,
|
||||
errorLabel,
|
||||
errorsLabel,
|
||||
warningLabel,
|
||||
warningsLabel,
|
||||
isScanning,
|
||||
isRepairing,
|
||||
onScan,
|
||||
onRepairAll,
|
||||
}: ErrorPanelHeaderProps) {
|
||||
return (
|
||||
<div className="border-b border-border bg-card px-6 py-4">
|
||||
<Flex align="center" justify="between">
|
||||
<Flex align="center" gap="md">
|
||||
<IconText icon={<Wrench size={20} weight="duotone" className="text-accent" />}>
|
||||
<Heading level={3}>{title}</Heading>
|
||||
</IconText>
|
||||
{(errorCount > 0 || warningCount > 0) && (
|
||||
<Flex gap="sm">
|
||||
{errorCount > 0 && (
|
||||
<Badge variant="destructive">
|
||||
{errorCount} {errorCount === 1 ? errorLabel : errorsLabel}
|
||||
</Badge>
|
||||
)}
|
||||
{warningCount > 0 && (
|
||||
<Badge variant="secondary">
|
||||
{warningCount} {warningCount === 1 ? warningLabel : warningsLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
<Flex gap="sm">
|
||||
<ActionButton
|
||||
icon={<Lightning size={16} />}
|
||||
label={isScanning ? scanningLabel : scanLabel}
|
||||
onClick={onScan}
|
||||
disabled={isScanning || isRepairing}
|
||||
variant="outline"
|
||||
/>
|
||||
<ActionButton
|
||||
icon={<Wrench size={16} />}
|
||||
label={isRepairing ? repairingLabel : repairAllLabel}
|
||||
onClick={onRepairAll}
|
||||
disabled={errorCount + warningCount === 0 || isRepairing || isScanning}
|
||||
variant="default"
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
142
src/components/error-panel/error-panel-repair.ts
Normal file
142
src/components/error-panel/error-panel-repair.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { CodeError } from '@/types/errors'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { ErrorRepairService } from '@/lib/error-repair-service'
|
||||
import errorPanelCopy from '@/data/error-panel.json'
|
||||
|
||||
const formatWithCount = (template: string, count: number) =>
|
||||
template
|
||||
.replace('{count}', String(count))
|
||||
.replace('{plural}', count === 1 ? '' : 's')
|
||||
|
||||
const formatWithValue = (template: string, token: string, value: string) =>
|
||||
template.replace(token, value)
|
||||
|
||||
interface RepairHandlersParams {
|
||||
files: ProjectFile[]
|
||||
errors: CodeError[]
|
||||
onFileChange: (fileId: string, content: string) => void
|
||||
scanForErrors: () => Promise<void>
|
||||
setErrors: Dispatch<SetStateAction<CodeError[]>>
|
||||
setIsRepairing: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export function createRepairHandlers({
|
||||
files,
|
||||
errors,
|
||||
onFileChange,
|
||||
scanForErrors,
|
||||
setErrors,
|
||||
setIsRepairing,
|
||||
}: RepairHandlersParams) {
|
||||
const repairSingleError = async (error: CodeError) => {
|
||||
const file = files.find((entry) => entry.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((entry) =>
|
||||
entry.id === error.id
|
||||
? { ...entry, isFixed: true, fixedCode: result.fixedCode }
|
||||
: entry
|
||||
)
|
||||
)
|
||||
|
||||
toast.success(
|
||||
formatWithValue(errorPanelCopy.toast.fixedSingle, '{message}', error.message),
|
||||
{
|
||||
description: result.explanation,
|
||||
}
|
||||
)
|
||||
|
||||
await scanForErrors()
|
||||
} else {
|
||||
toast.error(errorPanelCopy.toast.repairErrorFailed)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(errorPanelCopy.toast.repairFailed)
|
||||
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(formatWithCount(errorPanelCopy.toast.repairedFiles, fixedCount))
|
||||
await scanForErrors()
|
||||
} else {
|
||||
toast.error(errorPanelCopy.toast.noFilesRepaired)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(errorPanelCopy.toast.batchRepairFailed)
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsRepairing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const repairFileWithContext = async (fileId: string) => {
|
||||
const file = files.find((entry) => entry.id === fileId)
|
||||
if (!file) return
|
||||
|
||||
const fileErrors = errors.filter((entry) => entry.fileId === fileId)
|
||||
if (fileErrors.length === 0) return
|
||||
|
||||
setIsRepairing(true)
|
||||
try {
|
||||
const relatedFiles = files.filter((entry) => entry.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(
|
||||
formatWithValue(errorPanelCopy.toast.repairedFile, '{fileName}', file.name),
|
||||
{
|
||||
description: result.explanation,
|
||||
}
|
||||
)
|
||||
|
||||
await scanForErrors()
|
||||
} else {
|
||||
toast.error(errorPanelCopy.toast.repairFileFailed)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(errorPanelCopy.toast.contextRepairFailed)
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsRepairing(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
repairSingleError,
|
||||
repairAllErrors,
|
||||
repairFileWithContext,
|
||||
}
|
||||
}
|
||||
48
src/components/error-panel/error-panel-scan.ts
Normal file
48
src/components/error-panel/error-panel-scan.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { CodeError } from '@/types/errors'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { ErrorRepairService } from '@/lib/error-repair-service'
|
||||
import errorPanelCopy from '@/data/error-panel.json'
|
||||
|
||||
const formatWithCount = (template: string, count: number) =>
|
||||
template
|
||||
.replace('{count}', String(count))
|
||||
.replace('{plural}', count === 1 ? '' : 's')
|
||||
|
||||
interface ScanForErrorsParams {
|
||||
files: ProjectFile[]
|
||||
setErrors: Dispatch<SetStateAction<CodeError[]>>
|
||||
setIsScanning: Dispatch<SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export function createScanForErrors({
|
||||
files,
|
||||
setErrors,
|
||||
setIsScanning,
|
||||
}: ScanForErrorsParams) {
|
||||
return 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(errorPanelCopy.toast.noErrorsFound)
|
||||
} else {
|
||||
toast.info(formatWithCount(errorPanelCopy.toast.foundIssues, allErrors.length))
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(errorPanelCopy.toast.scanFailed)
|
||||
console.error(error)
|
||||
} finally {
|
||||
setIsScanning(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/components/error-panel/useErrorPanelState.ts
Normal file
64
src/components/error-panel/useErrorPanelState.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { CodeError } from '@/types/errors'
|
||||
import { ProjectFile } from '@/types/project'
|
||||
import { createScanForErrors } from './error-panel-scan'
|
||||
import { createRepairHandlers } from './error-panel-repair'
|
||||
|
||||
interface UseErrorPanelStateParams {
|
||||
files: ProjectFile[]
|
||||
onFileChange: (fileId: string, content: string) => void
|
||||
}
|
||||
|
||||
export function useErrorPanelState({ files, onFileChange }: UseErrorPanelStateParams) {
|
||||
const [errors, setErrors] = useState<CodeError[]>([])
|
||||
const [isScanning, setIsScanning] = useState(false)
|
||||
const [isRepairing, setIsRepairing] = useState(false)
|
||||
|
||||
const scanForErrors = useMemo(
|
||||
() => createScanForErrors({ files, setErrors, setIsScanning }),
|
||||
[files]
|
||||
)
|
||||
|
||||
const repairHandlers = useMemo(
|
||||
() =>
|
||||
createRepairHandlers({
|
||||
files,
|
||||
errors,
|
||||
onFileChange,
|
||||
scanForErrors,
|
||||
setErrors,
|
||||
setIsRepairing,
|
||||
}),
|
||||
[errors, files, onFileChange, scanForErrors]
|
||||
)
|
||||
|
||||
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((error) => error.severity === 'error').length
|
||||
const warningCount = errors.filter((error) => error.severity === 'warning').length
|
||||
|
||||
useEffect(() => {
|
||||
if (files.length > 0 && errors.length === 0) {
|
||||
scanForErrors()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
errors,
|
||||
errorsByFile,
|
||||
errorCount,
|
||||
warningCount,
|
||||
isScanning,
|
||||
isRepairing,
|
||||
scanForErrors,
|
||||
repairAllErrors: repairHandlers.repairAllErrors,
|
||||
repairFileWithContext: repairHandlers.repairFileWithContext,
|
||||
repairSingleError: repairHandlers.repairSingleError,
|
||||
}
|
||||
}
|
||||
47
src/data/error-panel.json
Normal file
47
src/data/error-panel.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"header": {
|
||||
"title": "Error Detection & Repair",
|
||||
"scan": "Scan",
|
||||
"scanning": "Scanning...",
|
||||
"repairAll": "Repair All",
|
||||
"repairing": "Repairing..."
|
||||
},
|
||||
"counts": {
|
||||
"errorSingular": "Error",
|
||||
"errorPlural": "Errors",
|
||||
"warningSingular": "Warning",
|
||||
"warningPlural": "Warnings",
|
||||
"issueSingular": "issue",
|
||||
"issuePlural": "issues"
|
||||
},
|
||||
"actions": {
|
||||
"open": "Open",
|
||||
"repair": "Repair",
|
||||
"fixed": "Fixed",
|
||||
"showCode": "Show code",
|
||||
"hideCode": "Hide code"
|
||||
},
|
||||
"labels": {
|
||||
"line": "Line"
|
||||
},
|
||||
"emptyStates": {
|
||||
"noIssuesTitle": "No Issues Found",
|
||||
"noIssuesDescription": "All files are looking good! Click 'Scan' to check again.",
|
||||
"scanningTitle": "Scanning Files...",
|
||||
"scanningDescription": "Analyzing your code for errors and issues"
|
||||
},
|
||||
"toast": {
|
||||
"noErrorsFound": "No errors found!",
|
||||
"foundIssues": "Found {count} issue{plural}",
|
||||
"scanFailed": "Error scanning failed",
|
||||
"fixedSingle": "Fixed: {message}",
|
||||
"repairErrorFailed": "Failed to repair error",
|
||||
"repairFailed": "Repair failed",
|
||||
"repairedFiles": "Repaired {count} file{plural}",
|
||||
"noFilesRepaired": "No files could be repaired",
|
||||
"batchRepairFailed": "Batch repair failed",
|
||||
"repairedFile": "Repaired {fileName}",
|
||||
"repairFileFailed": "Failed to repair file",
|
||||
"contextRepairFailed": "Context-aware repair failed"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user