Merge pull request #40 from johndoe6345789/codex/split-errorpanel-into-subcomponents

Refactor ErrorPanel into subcomponents
This commit is contained in:
2026-01-18 00:31:40 +00:00
committed by GitHub
10 changed files with 734 additions and 355 deletions

View File

@@ -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>

View 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}
/>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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,
}
}

View 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)
}
}
}

View 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
View 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"
}
}