Spaces:
Paused
Paused
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogFooter, | |
| DialogHeader, | |
| DialogTitle, | |
| } from '@/components/ui/dialog'; | |
| import { Button } from '@/components/ui/button'; | |
| import { Markdown } from '@/components/ui/markdown'; | |
| import { | |
| CheckCircle2, | |
| XCircle, | |
| AlertCircle, | |
| FileCode, | |
| Lightbulb, | |
| AlertTriangle, | |
| Plus, | |
| GitPullRequest, | |
| Clock, | |
| Wrench, | |
| } from 'lucide-react'; | |
| import { cn } from '@/lib/utils'; | |
| import type { | |
| IssueValidationResult, | |
| IssueValidationVerdict, | |
| IssueValidationConfidence, | |
| IssueComplexity, | |
| GitHubIssue, | |
| } from '@/lib/electron'; | |
| interface ValidationDialogProps { | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| issue: GitHubIssue | null; | |
| validationResult: IssueValidationResult | null; | |
| onConvertToTask?: (issue: GitHubIssue, validation: IssueValidationResult) => void; | |
| } | |
| const verdictConfig: Record< | |
| IssueValidationVerdict, | |
| { label: string; color: string; bgColor: string; icon: typeof CheckCircle2 } | |
| > = { | |
| valid: { | |
| label: 'Valid', | |
| color: 'text-green-500', | |
| bgColor: 'bg-green-500/10', | |
| icon: CheckCircle2, | |
| }, | |
| invalid: { | |
| label: 'Invalid', | |
| color: 'text-red-500', | |
| bgColor: 'bg-red-500/10', | |
| icon: XCircle, | |
| }, | |
| needs_clarification: { | |
| label: 'Needs Clarification', | |
| color: 'text-yellow-500', | |
| bgColor: 'bg-yellow-500/10', | |
| icon: AlertCircle, | |
| }, | |
| }; | |
| const confidenceConfig: Record<IssueValidationConfidence, { label: string; color: string }> = { | |
| high: { label: 'High Confidence', color: 'text-green-500' }, | |
| medium: { label: 'Medium Confidence', color: 'text-yellow-500' }, | |
| low: { label: 'Low Confidence', color: 'text-orange-500' }, | |
| }; | |
| const complexityConfig: Record<IssueComplexity, { label: string; color: string }> = { | |
| trivial: { label: 'Trivial', color: 'text-green-500' }, | |
| simple: { label: 'Simple', color: 'text-blue-500' }, | |
| moderate: { label: 'Moderate', color: 'text-yellow-500' }, | |
| complex: { label: 'Complex', color: 'text-orange-500' }, | |
| very_complex: { label: 'Very Complex', color: 'text-red-500' }, | |
| }; | |
| export function ValidationDialog({ | |
| open, | |
| onOpenChange, | |
| issue, | |
| validationResult, | |
| onConvertToTask, | |
| }: ValidationDialogProps) { | |
| if (!issue) return null; | |
| const handleConvertToTask = () => { | |
| if (validationResult && onConvertToTask) { | |
| onConvertToTask(issue, validationResult); | |
| onOpenChange(false); | |
| } | |
| }; | |
| return ( | |
| <Dialog open={open} onOpenChange={onOpenChange}> | |
| <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto"> | |
| <DialogHeader> | |
| <DialogTitle className="flex items-center gap-2">Issue Validation Result</DialogTitle> | |
| <DialogDescription> | |
| #{issue.number}: {issue.title} | |
| </DialogDescription> | |
| </DialogHeader> | |
| {validationResult ? ( | |
| <div className="space-y-6 py-4"> | |
| {/* Verdict Badge */} | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| {(() => { | |
| const config = verdictConfig[validationResult.verdict]; | |
| const Icon = config.icon; | |
| return ( | |
| <> | |
| <div className={cn('p-2 rounded-lg', config.bgColor)}> | |
| <Icon className={cn('h-6 w-6', config.color)} /> | |
| </div> | |
| <div> | |
| <p className={cn('text-lg font-semibold', config.color)}>{config.label}</p> | |
| <p | |
| className={cn( | |
| 'text-sm', | |
| confidenceConfig[validationResult.confidence].color | |
| )} | |
| > | |
| {confidenceConfig[validationResult.confidence].label} | |
| </p> | |
| </div> | |
| </> | |
| ); | |
| })()} | |
| </div> | |
| {validationResult.estimatedComplexity && ( | |
| <div className="text-right"> | |
| <p className="text-xs text-muted-foreground">Estimated Complexity</p> | |
| <p | |
| className={cn( | |
| 'text-sm font-medium', | |
| complexityConfig[validationResult.estimatedComplexity].color | |
| )} | |
| > | |
| {complexityConfig[validationResult.estimatedComplexity].label} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| {/* Bug Confirmed Badge */} | |
| {validationResult.bugConfirmed && ( | |
| <div className="flex items-center gap-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20"> | |
| <AlertTriangle className="h-5 w-5 text-red-500 shrink-0" /> | |
| <span className="text-sm font-medium text-red-500">Bug Confirmed in Codebase</span> | |
| </div> | |
| )} | |
| {/* PR Analysis Section - Show AI's analysis of linked PRs */} | |
| {validationResult.prAnalysis && validationResult.prAnalysis.hasOpenPR && ( | |
| <div | |
| className={cn( | |
| 'p-3 rounded-lg border', | |
| validationResult.prAnalysis.recommendation === 'wait_for_merge' | |
| ? 'bg-green-500/10 border-green-500/20' | |
| : validationResult.prAnalysis.recommendation === 'pr_needs_work' | |
| ? 'bg-yellow-500/10 border-yellow-500/20' | |
| : 'bg-purple-500/10 border-purple-500/20' | |
| )} | |
| > | |
| <div className="flex items-start gap-2"> | |
| {validationResult.prAnalysis.recommendation === 'wait_for_merge' ? ( | |
| <Clock className="h-5 w-5 text-green-500 shrink-0 mt-0.5" /> | |
| ) : validationResult.prAnalysis.recommendation === 'pr_needs_work' ? ( | |
| <Wrench className="h-5 w-5 text-yellow-500 shrink-0 mt-0.5" /> | |
| ) : ( | |
| <GitPullRequest className="h-5 w-5 text-purple-500 shrink-0 mt-0.5" /> | |
| )} | |
| <div className="flex-1"> | |
| <span | |
| className={cn( | |
| 'text-sm font-medium', | |
| validationResult.prAnalysis.recommendation === 'wait_for_merge' | |
| ? 'text-green-500' | |
| : validationResult.prAnalysis.recommendation === 'pr_needs_work' | |
| ? 'text-yellow-500' | |
| : 'text-purple-500' | |
| )} | |
| > | |
| {validationResult.prAnalysis.recommendation === 'wait_for_merge' | |
| ? 'Fix Ready - Wait for Merge' | |
| : validationResult.prAnalysis.recommendation === 'pr_needs_work' | |
| ? 'PR Needs Work' | |
| : 'Work in Progress'} | |
| </span> | |
| {validationResult.prAnalysis.prNumber && ( | |
| <p className="text-xs text-muted-foreground mt-0.5"> | |
| PR #{validationResult.prAnalysis.prNumber} | |
| {validationResult.prAnalysis.prFixesIssue && ' appears to fix this issue'} | |
| </p> | |
| )} | |
| {validationResult.prAnalysis.prSummary && ( | |
| <p className="text-xs text-muted-foreground mt-1"> | |
| {validationResult.prAnalysis.prSummary} | |
| </p> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Fallback Work in Progress Badge - Show when there's an open PR but no AI analysis */} | |
| {!validationResult.prAnalysis?.hasOpenPR && | |
| issue.linkedPRs?.some((pr) => pr.state === 'open' || pr.state === 'OPEN') && ( | |
| <div className="flex items-center gap-2 p-3 rounded-lg bg-purple-500/10 border border-purple-500/20"> | |
| <GitPullRequest className="h-5 w-5 text-purple-500 shrink-0" /> | |
| <div className="flex-1"> | |
| <span className="text-sm font-medium text-purple-500">Work in Progress</span> | |
| <p className="text-xs text-muted-foreground mt-0.5"> | |
| {issue.linkedPRs | |
| .filter((pr) => pr.state === 'open' || pr.state === 'OPEN') | |
| .map((pr) => `PR #${pr.number}`) | |
| .join(', ')}{' '} | |
| is open for this issue | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| {/* Reasoning */} | |
| <div className="space-y-2"> | |
| <h4 className="text-sm font-medium flex items-center gap-2"> | |
| <Lightbulb className="h-4 w-4 text-muted-foreground" /> | |
| Analysis | |
| </h4> | |
| <div className="bg-muted/30 p-3 rounded-lg border border-border"> | |
| <Markdown>{validationResult.reasoning}</Markdown> | |
| </div> | |
| </div> | |
| {/* Related Files */} | |
| {validationResult.relatedFiles && validationResult.relatedFiles.length > 0 && ( | |
| <div className="space-y-2"> | |
| <h4 className="text-sm font-medium flex items-center gap-2"> | |
| <FileCode className="h-4 w-4 text-muted-foreground" /> | |
| Related Files | |
| </h4> | |
| <div className="space-y-1"> | |
| {validationResult.relatedFiles.map((file, index) => ( | |
| <div | |
| key={index} | |
| className="text-sm font-mono bg-muted/50 px-2 py-1 rounded text-muted-foreground" | |
| > | |
| {file} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| {/* Suggested Fix */} | |
| {validationResult.suggestedFix && ( | |
| <div className="space-y-2"> | |
| <h4 className="text-sm font-medium">Suggested Approach</h4> | |
| <div className="bg-muted/30 p-3 rounded-lg border border-border"> | |
| <Markdown>{validationResult.suggestedFix}</Markdown> | |
| </div> | |
| </div> | |
| )} | |
| {/* Missing Info (for needs_clarification) */} | |
| {validationResult.missingInfo && validationResult.missingInfo.length > 0 && ( | |
| <div className="space-y-2"> | |
| <h4 className="text-sm font-medium flex items-center gap-2"> | |
| <AlertCircle className="h-4 w-4 text-yellow-500" /> | |
| Missing Information | |
| </h4> | |
| <ul className="space-y-1 list-disc list-inside"> | |
| {validationResult.missingInfo.map((info, index) => ( | |
| <li key={index} className="text-sm text-muted-foreground"> | |
| {info} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| ) : ( | |
| <div className="flex flex-col items-center justify-center py-12 text-center"> | |
| <AlertCircle className="h-8 w-8 text-muted-foreground mb-4" /> | |
| <p className="text-sm text-muted-foreground">No validation result available.</p> | |
| </div> | |
| )} | |
| <DialogFooter> | |
| <Button variant="ghost" onClick={() => onOpenChange(false)}> | |
| Close | |
| </Button> | |
| {validationResult?.verdict === 'valid' && | |
| onConvertToTask && | |
| validationResult?.prAnalysis?.recommendation !== 'wait_for_merge' && ( | |
| <Button onClick={handleConvertToTask}> | |
| <Plus className="h-4 w-4 mr-2" /> | |
| Convert to Task | |
| </Button> | |
| )} | |
| </DialogFooter> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |