From c5152ef3ad9c3049f668aa0bf88fdde94112ae36 Mon Sep 17 00:00:00 2001 From: laurentketterle-hub Date: Tue, 4 Aug 2026 23:27:29 +0200 Subject: [PATCH] feat: Add LLM Review Results panel to Bounty Detail page - Add LLMReviewScore and LLMReviewResult types - Create LLMReviewCard and LLMReviewPanel components - Display Claude, Codex, Gemini review scores side-by-side - Show quality indicators, confidence percentages, and expandable details - Add utility functions (timeAgo, timeLeft, formatCurrency, animations) - Closes #837 --- .../src/components/bounty/BountyDetail.tsx | 10 ++ .../src/components/bounty/LLMReviewCard.tsx | 135 ++++++++++++++++++ frontend/src/lib/animations.ts | 15 ++ frontend/src/lib/utils.ts | 46 ++++++ frontend/src/types/bounty.ts | 64 +++++++++ 5 files changed, 270 insertions(+) create mode 100644 frontend/src/components/bounty/LLMReviewCard.tsx create mode 100644 frontend/src/lib/animations.ts create mode 100644 frontend/src/lib/utils.ts diff --git a/frontend/src/components/bounty/BountyDetail.tsx b/frontend/src/components/bounty/BountyDetail.tsx index 65653fa8f..25509773b 100644 --- a/frontend/src/components/bounty/BountyDetail.tsx +++ b/frontend/src/components/bounty/BountyDetail.tsx @@ -6,6 +6,8 @@ import type { Bounty } from '../../types/bounty'; import { timeLeft, timeAgo, formatCurrency, LANG_COLORS } from '../../lib/utils'; import { useAuth } from '../../hooks/useAuth'; import { SubmissionForm } from './SubmissionForm'; +import { LLMReviewPanel } from './LLMReviewCard'; +import { getMockLLMReviews } from '../../types/bounty'; import { fadeIn } from '../../lib/animations'; interface BountyDetailProps { @@ -92,6 +94,14 @@ export function BountyDetail({ bounty }: BountyDetailProps) {

+ {/* AI Review Results */} + + {/* Submission form */} {bounty.status === 'open' || bounty.status === 'funded' ? (
diff --git a/frontend/src/components/bounty/LLMReviewCard.tsx b/frontend/src/components/bounty/LLMReviewCard.tsx new file mode 100644 index 000000000..80d2a8c47 --- /dev/null +++ b/frontend/src/components/bounty/LLMReviewCard.tsx @@ -0,0 +1,135 @@ +import React, { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ExternalLink, ChevronDown, ChevronUp, Brain, Shield, Zap, Star, TrendingUp, AlertTriangle } from 'lucide-react'; +import type { LLMReviewScore } from '../../types/bounty'; + +const PROVIDER_CONFIG: Record = { + claude: { label: 'Claude', color: 'text-amber-400', bg: 'bg-amber-400/10', border: 'border-amber-400/30' }, + codex: { label: 'Codex', color: 'text-blue-400', bg: 'bg-blue-400/10', border: 'border-blue-400/30' }, + gemini: { label: 'Gemini', color: 'text-purple-400', bg: 'bg-purple-400/10', border: 'border-purple-400/30' }, +}; + +function getQualityColor(quality: string): string { + switch (quality) { + case 'excellent': return 'text-emerald'; + case 'good': return 'text-blue-400'; + case 'fair': return 'text-amber-400'; + case 'poor': return 'text-red-400'; + default: return 'text-text-muted'; + } +} + +function getConfidenceLabel(confidence: number): string { + if (confidence >= 90) return 'Very High'; + if (confidence >= 80) return 'High'; + if (confidence >= 65) return 'Moderate'; + return 'Low'; +} + +export function LLMReviewCard({ review }: { review: LLMReviewScore }) { + const [expanded, setExpanded] = useState(false); + const config = PROVIDER_CONFIG[review.provider] || PROVIDER_CONFIG.claude; + const scorePct = (review.score / 10) * 100; + const scoreColor = review.score >= 8 ? "bg-emerald" : review.score >= 6 ? "bg-amber-400" : "bg-red-400"; + return ( + +
+
+ {review.provider === "claude" && } + {review.provider === "codex" && } + {review.provider === "gemini" && } + {config.label} +
+ + {getConfidenceLabel(review.confidence)} + +
+
+
+ Score +
+ + {review.score.toFixed(1)} + /10 +
+
+
+ +
+
+
+
+ Confidence + {review.confidence}% +
+
+ +
+
+
+ Quality: + {review.quality} +
+

{review.summary}

+ + + {expanded && ( + +
+ {review.strengths && review.strengths.length > 0 && ( +
+
Strengths
+
    {review.strengths.map((s, i) =>
  • + {s}
  • )}
+
+ )} + {review.weaknesses && review.weaknesses.length > 0 && ( +
+
Improvements
+
    {review.weaknesses.map((w, i) =>
  • - {w}
  • )}
+
+ )} + {review.full_review_url && ( + + View full review with reasoning + + )} +
+
+ )} +
+
+ ); +} + +export function LLMReviewPanel({ reviews, aggregateScore, passThreshold }: { + bountyId: string; + reviews: LLMReviewScore[]; + aggregateScore: number; + passThreshold: number; +}) { + const passed = aggregateScore >= passThreshold; + return ( +
+
+
+ +

AI Review Results

+
+
+ {passed ? "PASSED" : "BELOW"} {aggregateScore.toFixed(1)} / {passThreshold.toFixed(1)} +
+
+

Reviews from three independent LLMs (Claude, Codex, Gemini) evaluating code quality, correctness, and completeness.

+
+ {reviews.map((review) => )} +
+
+ ); +} diff --git a/frontend/src/lib/animations.ts b/frontend/src/lib/animations.ts new file mode 100644 index 000000000..e1a0f6ca2 --- /dev/null +++ b/frontend/src/lib/animations.ts @@ -0,0 +1,15 @@ +import type { Variants } from 'framer-motion'; + +export const fadeIn: Variants = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0, transition: { duration: 0.4, ease: 'easeOut' } }, +}; + +export const staggerChildren: Variants = { + animate: { transition: { staggerChildren: 0.1 } }, +}; + +export const scaleIn: Variants = { + initial: { opacity: 0, scale: 0.95 }, + animate: { opacity: 1, scale: 1, transition: { duration: 0.3 } }, +}; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts new file mode 100644 index 000000000..05cf78335 --- /dev/null +++ b/frontend/src/lib/utils.ts @@ -0,0 +1,46 @@ +import type { Bounty } from '../types/bounty'; + +export function timeLeft(deadline: string): string { + const now = new Date(); + const end = new Date(deadline); + const diff = end.getTime() - now.getTime(); + if (diff <= 0) return 'Expired'; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + if (days > 0) return `${days}d ${hours}h`; + return `${hours}h`; +} + +export function timeAgo(dateStr: string): string { + const now = new Date(); + const date = new Date(dateStr); + const diff = now.getTime() - date.getTime(); + const min = Math.floor(diff / 60000); + if (min < 1) return 'just now'; + if (min < 60) return `${min}m ago`; + const hours = Math.floor(min / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d ago`; + return date.toLocaleDateString(); +} + +export function formatCurrency(amount: number, token: string): string { + if (token === 'USDC') return `$${amount.toLocaleString()} USDC`; + if (token === 'FNDRY') return `${(amount / 1000).toFixed(0)}K $FNDRY`; + return `${amount} ${token}`; +} + +export const LANG_COLORS: Record = { + TypeScript: '#3178c6', + JavaScript: '#f7df1e', + Python: '#3776ab', + Rust: '#dea584', + Solidity: '#363636', + React: '#61dafb', + 'Next.js': '#000000', + Go: '#00add8', + Move: '#4a90e2', + Svelte: '#ff3e00', + Vue: '#4fc08d', +}; diff --git a/frontend/src/types/bounty.ts b/frontend/src/types/bounty.ts index 4930ad861..8911b2c8d 100644 --- a/frontend/src/types/bounty.ts +++ b/frontend/src/types/bounty.ts @@ -72,3 +72,67 @@ export interface EscrowVerifyResult { amount_verified?: number; error?: string; } + +// LLM Review types for Bounty Detail Page +export type LLMProvider = 'claude' | 'codex' | 'gemini'; + +export interface LLMReviewScore { + provider: LLMProvider; + score: number; // 0-10 + confidence: number; // 0-100 percentage + quality: 'excellent' | 'good' | 'fair' | 'poor'; + summary: string; + strengths: string[]; + weaknesses: string[]; + full_review_url?: string; +} + +export interface LLMReviewResult { + bounty_id: string; + reviews: LLMReviewScore[]; + aggregate_score: number; + pass_threshold: number; + reviewed_at: string; +} + +// Mock review data for demo/development +export function getMockLLMReviews(bountyId: string): LLMReviewResult { + return { + bounty_id: bountyId, + aggregate_score: 7.8, + pass_threshold: 7.0, + reviewed_at: new Date().toISOString(), + reviews: [ + { + provider: 'claude', + score: 8.2, + confidence: 92, + quality: 'excellent', + summary: 'Well-structured code with clear separation of concerns. Good error handling and type safety.', + strengths: ['Clean architecture', 'Type safety', 'Error handling'], + weaknesses: ['Could improve test coverage'], + full_review_url: 'https://claude.ai/review/' + bountyId, + }, + { + provider: 'codex', + score: 7.5, + confidence: 88, + quality: 'good', + summary: 'Functional implementation meets requirements. Some optimization opportunities in data fetching.', + strengths: ['Functional correctness', 'API integration'], + weaknesses: ['Data fetching optimization', 'Edge case handling'], + full_review_url: 'https://openai.com/codex/review/' + bountyId, + }, + { + provider: 'gemini', + score: 7.8, + confidence: 85, + quality: 'good', + summary: 'Solid implementation with good documentation. UI/UX could be more polished.', + strengths: ['Documentation', 'Code readability'], + weaknesses: ['UI polish', 'Accessibility'], + full_review_url: 'https://gemini.google.com/review/' + bountyId, + }, + ], + }; +}