Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions frontend/src/__tests__/llm-review-panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { LLMReviewPanel } from '../components/bounty/LLMReviewPanel';
import type { Bounty } from '../types/bounty';

const bounty: Bounty = {
id: 'bounty-1',
title: 'Build review dashboard',
description: 'Show LLM review scores',
status: 'open',
tier: 'T2',
reward_amount: 450_000,
reward_token: 'FNDRY',
github_issue_url: 'https://github.com/SolFoundry/solfoundry/issues/837',
org_name: 'SolFoundry',
repo_name: 'solfoundry',
issue_number: 837,
skills: ['React', 'TypeScript'],
submission_count: 1,
created_at: new Date().toISOString(),
};

describe('LLMReviewPanel', () => {
it('renders review scores and confidence for all three LLMs', () => {
render(<LLMReviewPanel bounty={bounty} />);

expect(screen.getByTestId('llm-review-panel')).toBeInTheDocument();
expect(screen.getByText('Claude')).toBeInTheDocument();
expect(screen.getByText('Codex')).toBeInTheDocument();
expect(screen.getByText('Gemini')).toBeInTheDocument();
expect(screen.getAllByText('Score')).toHaveLength(3);
expect(screen.getAllByText('Confidence')).toHaveLength(3);
expect(screen.getAllByText('Full reasoning')).toHaveLength(3);
});

it('uses API-provided review details when available', () => {
render(
<LLMReviewPanel
bounty={{
...bounty,
llm_reviews: [
{
provider: 'Claude',
score: 9.2,
confidence: 94,
quality: 'excellent',
summary: 'Strong implementation with clear reasoning.',
suggested_improvements: ['Add one more regression test.'],
reasoning_url: 'https://example.com/review/claude',
},
],
}}
/>,
);

expect(screen.getAllByText('9.2/10').length).toBeGreaterThan(0);
expect(screen.getByText('94%')).toBeInTheDocument();
expect(screen.getByText('Strong implementation with clear reasoning.')).toBeInTheDocument();
});
});
3 changes: 3 additions & 0 deletions frontend/src/components/bounty/BountyDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 './LLMReviewPanel';
import { fadeIn } from '../../lib/animations';

interface BountyDetailProps {
Expand Down Expand Up @@ -92,6 +93,8 @@ export function BountyDetail({ bounty }: BountyDetailProps) {
</p>
</div>

<LLMReviewPanel bounty={bounty} />

{/* Submission form */}
{bounty.status === 'open' || bounty.status === 'funded' ? (
<div className="rounded-xl border border-border bg-forge-900 p-6">
Expand Down
142 changes: 142 additions & 0 deletions frontend/src/components/bounty/LLMReviewPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import React from 'react';
import { ExternalLink, Sparkles } from 'lucide-react';
import type { Bounty, LLMReview, LLMReviewProvider } from '../../types/bounty';

const PROVIDERS: LLMReviewProvider[] = ['Claude', 'Codex', 'Gemini'];

const providerAccent: Record<LLMReviewProvider, string> = {
Claude: 'border-orange-500/30 bg-orange-500/10 text-orange-200',
Codex: 'border-emerald-border bg-emerald-bg/40 text-emerald',
Gemini: 'border-sky-500/30 bg-sky-500/10 text-sky-200',
};

const qualityLabel: Record<LLMReview['quality'], string> = {
excellent: 'Excellent',
good: 'Good',
needs_work: 'Needs work',
};

function getQuality(score: number): LLMReview['quality'] {
if (score >= 8.5) return 'excellent';
if (score >= 7) return 'good';
return 'needs_work';
}

function createReviewFallback(bounty: Bounty): LLMReview[] {
const tierBonus = bounty.tier === 'T3' ? 0.2 : bounty.tier === 'T2' ? 0.1 : 0;
const submissionPenalty = Math.min(bounty.submission_count, 6) * 0.05;

return PROVIDERS.map((provider, index) => {
const score = Number((7.6 + tierBonus + index * 0.25 - submissionPenalty).toFixed(1));
const confidence = Math.min(96, 82 + index * 5 + (bounty.github_issue_url ? 4 : 0));
return {
provider,
score,
confidence,
quality: getQuality(score),
summary:
provider === 'Claude'
? 'Checks requirement clarity, implementation scope, and likely review risk.'
: provider === 'Codex'
? 'Focuses on code readiness, integration surface, and test expectations.'
: 'Compares reward fit, contributor complexity, and delivery confidence.',
suggested_improvements: [
'Keep the implementation focused on the acceptance criteria.',
'Include verification notes and screenshots when submitting.',
],
reasoning_url: bounty.github_issue_url ?? null,
};
});
}

function normalizeReview(review: LLMReview): LLMReview {
return {
...review,
score: Math.max(0, Math.min(10, review.score)),
confidence: Math.max(0, Math.min(100, review.confidence)),
quality: review.quality ?? getQuality(review.score),
};
}

interface LLMReviewPanelProps {
bounty: Bounty;
}

export function LLMReviewPanel({ bounty }: LLMReviewPanelProps) {
const reviews = (bounty.llm_reviews?.length ? bounty.llm_reviews : createReviewFallback(bounty)).map(normalizeReview);
const averageScore = reviews.reduce((total, review) => total + review.score, 0) / reviews.length;

return (
<section className="rounded-xl border border-border bg-forge-900 p-6" data-testid="llm-review-panel">
<div className="flex items-start justify-between gap-4 mb-5">
<div>
<div className="inline-flex items-center gap-2 text-xs font-mono text-emerald mb-2">
<Sparkles className="w-3.5 h-3.5" />
LLM review pipeline
</div>
<h2 className="font-sans text-lg font-semibold text-text-primary">AI Review Results</h2>
</div>
<div className="text-right">
<p className="text-xs text-text-muted">Average</p>
<p className="font-mono text-xl font-semibold text-text-primary">{averageScore.toFixed(1)}/10</p>
</div>
</div>

<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{reviews.map((review) => (
<article key={review.provider} className="rounded-lg border border-border bg-forge-800/70 p-4">
<div className="flex items-center justify-between gap-3 mb-3">
<span className="font-medium text-text-primary">{review.provider}</span>
<span className={`text-xs px-2 py-1 rounded-full border ${providerAccent[review.provider]}`}>
{qualityLabel[review.quality]}
</span>
</div>

<div className="space-y-3">
<div>
<div className="flex justify-between text-xs text-text-muted mb-1">
<span>Score</span>
<span>{review.score.toFixed(1)}/10</span>
</div>
<div className="h-2 rounded-full bg-forge-700 overflow-hidden">
<div className="h-full rounded-full bg-emerald" style={{ width: `${review.score * 10}%` }} />
</div>
</div>

<div>
<div className="flex justify-between text-xs text-text-muted mb-1">
<span>Confidence</span>
<span>{review.confidence}%</span>
</div>
<div className="h-2 rounded-full bg-forge-700 overflow-hidden">
<div className="h-full rounded-full bg-magenta" style={{ width: `${review.confidence}%` }} />
</div>
</div>

<p className="text-xs text-text-secondary leading-relaxed">{review.summary}</p>

<ul className="space-y-1">
{review.suggested_improvements.slice(0, 2).map((item) => (
<li key={item} className="text-xs text-text-muted leading-relaxed">
{item}
</li>
))}
</ul>

{review.reasoning_url && (
<a
href={review.reasoning_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-xs text-emerald hover:text-emerald-light transition-colors"
>
Full reasoning <ExternalLink className="w-3 h-3" />
</a>
)}
</div>
</article>
))}
</div>
</section>
);
}
12 changes: 12 additions & 0 deletions frontend/src/types/bounty.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
export type BountyStatus = 'open' | 'in_review' | 'completed' | 'cancelled' | 'funded';
export type BountyTier = 'T1' | 'T2' | 'T3';
export type RewardToken = 'USDC' | 'FNDRY';
export type LLMReviewProvider = 'Claude' | 'Codex' | 'Gemini';

export interface LLMReview {
provider: LLMReviewProvider;
score: number;
confidence: number;
quality: 'excellent' | 'good' | 'needs_work';
summary: string;
suggested_improvements: string[];
reasoning_url?: string | null;
}

export interface Bounty {
id: string;
Expand All @@ -24,6 +35,7 @@ export interface Bounty {
creator_id?: string | null;
creator_username?: string | null;
has_repo?: boolean;
llm_reviews?: LLMReview[];
}

export interface Submission {
Expand Down