mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
- Replace HTML checkbox with proper UI Checkbox component - Add system prompt instructions for AI to check PR changes via gh CLI - Add PRAnalysis schema field with recommendation (wait_for_merge, pr_needs_work, no_pr) - Show detailed PR analysis badge in validation dialog - Hide "Convert to Task" button when PR fix is ready (wait_for_merge) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
308 lines
12 KiB
TypeScript
308 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|