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 && (
+
+
+
{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,
+ },
+ ],
+ };
+}