Skip to content

Commit b71d123

Browse files
committed
feat: add review history detail view with full results
1 parent 26970da commit b71d123

File tree

5 files changed

+220
-68
lines changed

5 files changed

+220
-68
lines changed

backend/.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ ACCESS_TOKEN_EXPIRE_MINUTES=60
1515
APP_ENV=development
1616

1717
# Mock mode — set to true to test without OpenAI API calls (free)
18-
MOCK_LLM=true
18+
MOCK_LLM=false

backend/app/routers/review.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
POST /api/explain — AI code explanation (plain English)
66
POST /api/refactor — AI refactor suggestions (before/after examples)
77
GET /api/history — user's past reviews
8+
GET /api/history/{review_id} — full detail for a single past review
89
"""
910

10-
from fastapi import APIRouter, Depends
11+
from fastapi import APIRouter, Depends, HTTPException, status
1112
from sqlalchemy import select
1213
from sqlalchemy.ext.asyncio import AsyncSession
1314

@@ -20,6 +21,7 @@
2021
ExplainResponse,
2122
RefactorResponse,
2223
HistoryItem,
24+
HistoryDetail,
2325
)
2426
from app.services.llm import llm_service
2527
from app.services.auth import get_current_user
@@ -134,3 +136,32 @@ async def get_history(
134136
)
135137
for r in reviews
136138
]
139+
140+
141+
@router.get("/history/{review_id}", response_model=HistoryDetail)
142+
async def get_history_detail(
143+
review_id: int,
144+
user: User = Depends(get_current_user),
145+
db: AsyncSession = Depends(get_db),
146+
):
147+
"""Get the full detail of a single past review."""
148+
result = await db.execute(
149+
select(Review).where(Review.id == review_id, Review.user_id == user.id)
150+
)
151+
review = result.scalar_one_or_none()
152+
153+
if not review:
154+
raise HTTPException(
155+
status_code=status.HTTP_404_NOT_FOUND,
156+
detail="Review not found",
157+
)
158+
159+
return HistoryDetail(
160+
id=review.id,
161+
code=review.code,
162+
language=review.language,
163+
review_type=review.review_type,
164+
result=review.result,
165+
score=review.score,
166+
created_at=review.created_at.isoformat(),
167+
)

backend/app/schemas/review.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
they are used to force the LLM to return structured JSON instead of freeform text.
66
"""
77

8+
from typing import Any
9+
810
from pydantic import BaseModel, Field
911

1012

@@ -55,7 +57,7 @@ class RefactorResponse(BaseModel):
5557

5658

5759
class HistoryItem(BaseModel):
58-
"""Single item in review history."""
60+
"""Single item in review history list."""
5961
id: int
6062
language: str
6163
review_type: str
@@ -64,3 +66,17 @@ class HistoryItem(BaseModel):
6466

6567
class Config:
6668
from_attributes = True
69+
70+
71+
class HistoryDetail(BaseModel):
72+
"""Full detail of a single past review."""
73+
id: int
74+
code: str
75+
language: str
76+
review_type: str
77+
result: Any
78+
score: int | None
79+
created_at: str
80+
81+
class Config:
82+
from_attributes = True

frontend/src/pages/History.jsx

Lines changed: 169 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { useState, useEffect } from 'react';
22
import { history as historyApi } from '../utils/api';
3+
import ReviewResults from '../components/ReviewResults';
4+
import TextResults from '../components/TextResults';
35
import {
46
Clock,
57
Scan,
@@ -8,6 +10,9 @@ import {
810
Loader2,
911
AlertCircle,
1012
Inbox,
13+
ChevronRight,
14+
ArrowLeft,
15+
Code2,
1116
} from 'lucide-react';
1217

1318
function getTypeIcon(type) {
@@ -56,13 +61,75 @@ function formatDate(isoString) {
5661
if (diffHr < 24) return `${diffHr}h ago`;
5762
if (diffDay < 7) return `${diffDay}d ago`;
5863

59-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
64+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
6065
}
6166

67+
// ── Detail View ──
68+
function HistoryDetailView({ detail, onBack }) {
69+
return (
70+
<div className="animate-fade-in">
71+
{/* Back button + header */}
72+
<button
73+
onClick={onBack}
74+
className="btn-ghost mb-4 -ml-2"
75+
>
76+
<ArrowLeft size={14} />
77+
Back to history
78+
</button>
79+
80+
<div className="flex items-center gap-3 mb-5">
81+
{getTypeIcon(detail.review_type)}
82+
<div>
83+
<div className="flex items-center gap-2">
84+
<span className={`badge border text-[10px] ${getTypeBadgeClass(detail.review_type)}`}>
85+
{getTypeLabel(detail.review_type)}
86+
</span>
87+
<span className="font-mono text-xs text-txt-secondary">{detail.language}</span>
88+
<span className="font-mono text-[10px] text-txt-muted">
89+
{formatDate(detail.created_at)}
90+
</span>
91+
</div>
92+
</div>
93+
</div>
94+
95+
{/* Original code */}
96+
<div className="card p-0 overflow-hidden mb-5">
97+
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-surface-5 bg-surface-1/50">
98+
<Code2 size={13} className="text-txt-muted" />
99+
<span className="font-mono text-[10px] text-txt-muted tracking-wide">
100+
Submitted code · {detail.language}
101+
</span>
102+
</div>
103+
<pre className="p-4 overflow-x-auto">
104+
<code className="font-mono text-xs text-txt-secondary leading-relaxed">
105+
{detail.code}
106+
</code>
107+
</pre>
108+
</div>
109+
110+
{/* Results */}
111+
{detail.review_type === 'review' && detail.result && (
112+
<ReviewResults result={detail.result} cached={false} />
113+
)}
114+
115+
{detail.review_type === 'explain' && detail.result?.explanation && (
116+
<TextResults content={detail.result.explanation} type="explain" cached={false} />
117+
)}
118+
119+
{detail.review_type === 'refactor' && detail.result?.suggestions && (
120+
<TextResults content={detail.result.suggestions} type="refactor" cached={false} />
121+
)}
122+
</div>
123+
);
124+
}
125+
126+
// ── Main History Page ──
62127
export default function History() {
63128
const [items, setItems] = useState([]);
64129
const [loading, setLoading] = useState(true);
65130
const [error, setError] = useState('');
131+
const [detail, setDetail] = useState(null);
132+
const [detailLoading, setDetailLoading] = useState(false);
66133

67134
useEffect(() => {
68135
loadHistory();
@@ -79,81 +146,118 @@ export default function History() {
79146
}
80147
};
81148

149+
const loadDetail = async (id) => {
150+
setDetailLoading(true);
151+
setError('');
152+
try {
153+
const response = await historyApi.getById(id);
154+
setDetail(response.data);
155+
} catch (err) {
156+
setError('Failed to load review details.');
157+
} finally {
158+
setDetailLoading(false);
159+
}
160+
};
161+
162+
const handleBack = () => {
163+
setDetail(null);
164+
};
165+
82166
return (
83167
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
84-
<div className="mb-6">
85-
<h1 className="font-display font-bold text-xl text-txt-primary">Review History</h1>
86-
<p className="text-sm text-txt-muted mt-1">
87-
Your last 50 code analyses, most recent first.
88-
</p>
89-
</div>
168+
{/* Show detail view if selected */}
169+
{detail && !detailLoading && (
170+
<HistoryDetailView detail={detail} onBack={handleBack} />
171+
)}
90172

91-
{loading && (
173+
{detailLoading && (
92174
<div className="card flex items-center justify-center py-20">
93175
<Loader2 size={24} className="text-accent-cyan animate-spin" />
94176
</div>
95177
)}
96178

97-
{error && (
98-
<div className="flex items-center gap-2 bg-accent-red/5 border border-accent-red/20 text-accent-red text-sm px-4 py-3 rounded-lg">
99-
<AlertCircle size={14} />
100-
{error}
101-
</div>
102-
)}
179+
{/* Show list if no detail selected */}
180+
{!detail && !detailLoading && (
181+
<>
182+
<div className="mb-6">
183+
<h1 className="font-display font-bold text-xl text-txt-primary">Review History</h1>
184+
<p className="text-sm text-txt-muted mt-1">
185+
Your last 50 code analyses, most recent first. Click to view details.
186+
</p>
187+
</div>
103188

104-
{!loading && !error && items.length === 0 && (
105-
<div className="card flex flex-col items-center justify-center py-20 text-center">
106-
<Inbox size={28} className="text-txt-muted/30 mb-3" />
107-
<p className="text-sm text-txt-muted">No reviews yet</p>
108-
<p className="font-mono text-[10px] text-txt-muted/60 mt-1">
109-
Your analysis history will appear here
110-
</p>
111-
</div>
112-
)}
189+
{loading && (
190+
<div className="card flex items-center justify-center py-20">
191+
<Loader2 size={24} className="text-accent-cyan animate-spin" />
192+
</div>
193+
)}
194+
195+
{error && (
196+
<div className="flex items-center gap-2 bg-accent-red/5 border border-accent-red/20 text-accent-red text-sm px-4 py-3 rounded-lg">
197+
<AlertCircle size={14} />
198+
{error}
199+
</div>
200+
)}
201+
202+
{!loading && !error && items.length === 0 && (
203+
<div className="card flex flex-col items-center justify-center py-20 text-center">
204+
<Inbox size={28} className="text-txt-muted/30 mb-3" />
205+
<p className="text-sm text-txt-muted">No reviews yet</p>
206+
<p className="font-mono text-[10px] text-txt-muted/60 mt-1">
207+
Your analysis history will appear here
208+
</p>
209+
</div>
210+
)}
211+
212+
{!loading && items.length > 0 && (
213+
<div className="card p-0 overflow-hidden">
214+
<div className="divide-y divide-surface-5">
215+
{items.map((item) => (
216+
<button
217+
key={item.id}
218+
onClick={() => loadDetail(item.id)}
219+
className="w-full flex items-center gap-4 px-5 py-3.5 hover:bg-surface-3/30 transition-colors text-left"
220+
>
221+
{/* Type icon */}
222+
<div className="shrink-0">{getTypeIcon(item.review_type)}</div>
113223

114-
{!loading && items.length > 0 && (
115-
<div className="card p-0 overflow-hidden">
116-
<div className="divide-y divide-surface-5">
117-
{items.map((item) => (
118-
<div
119-
key={item.id}
120-
className="flex items-center gap-4 px-5 py-3.5 hover:bg-surface-3/30 transition-colors"
121-
>
122-
{/* Type icon */}
123-
<div className="shrink-0">{getTypeIcon(item.review_type)}</div>
124-
125-
{/* Info */}
126-
<div className="flex-1 min-w-0">
127-
<div className="flex items-center gap-2 flex-wrap">
128-
<span className={`badge border text-[10px] ${getTypeBadgeClass(item.review_type)}`}>
129-
{getTypeLabel(item.review_type)}
130-
</span>
131-
<span className="font-mono text-xs text-txt-secondary">
132-
{item.language}
133-
</span>
134-
</div>
135-
</div>
136-
137-
{/* Score (review only) */}
138-
<div className="shrink-0 w-12 text-right">
139-
{item.score != null ? (
140-
<span className={`font-display font-bold text-sm ${getScoreColor(item.score)}`}>
141-
{item.score}/10
142-
</span>
143-
) : (
144-
<span className="text-txt-muted/30"></span>
145-
)}
146-
</div>
147-
148-
{/* Time */}
149-
<div className="shrink-0 flex items-center gap-1.5 text-txt-muted">
150-
<Clock size={11} />
151-
<span className="font-mono text-[10px]">{formatDate(item.created_at)}</span>
152-
</div>
224+
{/* Info */}
225+
<div className="flex-1 min-w-0">
226+
<div className="flex items-center gap-2 flex-wrap">
227+
<span className={`badge border text-[10px] ${getTypeBadgeClass(item.review_type)}`}>
228+
{getTypeLabel(item.review_type)}
229+
</span>
230+
<span className="font-mono text-xs text-txt-secondary">
231+
{item.language}
232+
</span>
233+
</div>
234+
</div>
235+
236+
{/* Score (review only) */}
237+
<div className="shrink-0 w-12 text-right">
238+
{item.score != null ? (
239+
<span className={`font-display font-bold text-sm ${getScoreColor(item.score)}`}>
240+
{item.score}/10
241+
</span>
242+
) : (
243+
<span className="text-txt-muted/30"></span>
244+
)}
245+
</div>
246+
247+
{/* Time */}
248+
<div className="shrink-0 flex items-center gap-1.5 text-txt-muted">
249+
<Clock size={11} />
250+
<span className="font-mono text-[10px]">{formatDate(item.created_at)}</span>
251+
</div>
252+
253+
{/* Arrow */}
254+
<ChevronRight size={14} className="shrink-0 text-txt-muted/30" />
255+
</button>
256+
))}
153257
</div>
154-
))}
155-
</div>
156-
</div>
258+
</div>
259+
)}
260+
</>
157261
)}
158262
</div>
159263
);

frontend/src/utils/api.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const codeAnalysis = {
6262
// ── History ──
6363
export const history = {
6464
getAll: () => api.get('/history'),
65+
getById: (id) => api.get(`/history/${id}`),
6566
};
6667

6768
// ── Health ──

0 commit comments

Comments
 (0)