diff --git a/desktop/src/components/DevQALoop.tsx b/desktop/src/components/DevQALoop.tsx new file mode 100644 index 0000000..509bc8c --- /dev/null +++ b/desktop/src/components/DevQALoop.tsx @@ -0,0 +1,468 @@ +/** + * DevQALoop - Developer ↔ QA Review Loop Interface + * + * Visualizes the iterative review cycle between Developer and QA agents, + * showing iteration count, feedback history, and current state. + * + * @module components/DevQALoop + */ + +import { useState, useEffect } from 'react'; +import { useTeamStore } from '../store/teamStore'; +import type { DevQALoop as DevQALoopType, ReviewFeedback, ReviewIssue } from '../types/team'; +import { + RefreshCw, CheckCircle, XCircle, AlertTriangle, ArrowRight, + Clock, MessageSquare, FileCode, Bug, Lightbulb, ChevronDown, ChevronUp, + Send, ThumbsUp, ThumbsDown, AlertOctagon, +} from 'lucide-react'; + +// === Sub-Components === + +interface FeedbackItemProps { + feedback: ReviewFeedback; + iteration: number; + isExpanded: boolean; + onToggle: () => void; +} + +function FeedbackItem({ feedback, iteration, isExpanded, onToggle }: FeedbackItemProps) { + const verdictColors = { + approved: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300', + needs_work: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300', + rejected: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300', + }; + + const verdictIcons = { + approved: , + needs_work: , + rejected: , + }; + + const severityColors = { + critical: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300', + major: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300', + minor: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-300', + suggestion: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300', + }; + + return ( +
+ + + {isExpanded && ( +
+ {/* Comments */} + {feedback.comments.length > 0 && ( +
+
Comments
+
    + {feedback.comments.map((comment, idx) => ( +
  • + + {comment} +
  • + ))} +
+
+ )} + + {/* Issues */} + {feedback.issues.length > 0 && ( +
+
Issues ({feedback.issues.length})
+
+ {feedback.issues.map((issue, idx) => ( +
+
+ + {issue.severity} + + {issue.file && ( + + + {issue.file}{issue.line ? `:${issue.line}` : ''} + + )} +
+

{issue.description}

+ {issue.suggestion && ( +

+ + {issue.suggestion} +

+ )} +
+ ))} +
+
+ )} +
+ )} +
+ ); +} + +interface ReviewFormProps { + loopId: string; + teamId: string; + onSubmit: (feedback: Omit) => void; + onCancel: () => void; +} + +function ReviewForm({ loopId, teamId, onSubmit, onCancel }: ReviewFormProps) { + const [verdict, setVerdict] = useState('needs_work'); + const [comment, setComment] = useState(''); + const [issues, setIssues] = useState([]); + const [newIssue, setNewIssue] = useState>({}); + + const handleAddIssue = () => { + if (!newIssue.description) return; + setIssues([...issues, { + severity: newIssue.severity || 'minor', + description: newIssue.description, + file: newIssue.file, + line: newIssue.line, + suggestion: newIssue.suggestion, + } as ReviewIssue]); + setNewIssue({}); + }; + + const handleSubmit = () => { + onSubmit({ + verdict, + comments: comment ? [comment] : [], + issues, + }); + }; + + return ( +
+

Submit Review

+ + {/* Verdict */} +
+ +
+ {(['approved', 'needs_work', 'rejected'] as const).map(v => ( + + ))} +
+
+ + {/* Comment */} +
+ +