Skip to content

Commit bc4b81d

Browse files
mattgodboltclaude
andcommitted
Phase 4: Complete review status indicators and update functionality
Implement comprehensive review management system with: Backend: - New API endpoint `/api/reviews/prompt/<version>` to load existing reviews - Groups reviews by case_id for efficient frontend consumption Frontend: - Visual status indicators: green border + ✅ for reviewed, red border + ⚪ for pending - Automatic form pre-population with existing review data - Dynamic button text: "Save Review" vs "Update Review" based on status - Progress tracking with animated bar showing "X/Y cases reviewed (Z%)" - Real-time status updates when reviews are saved/updated - State management for existing reviews and progress counters This creates a professional review interface that handles the full lifecycle of human review management, making it easy to track progress and update existing reviews without losing work. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent bff83ab commit bc4b81d

File tree

2 files changed

+185
-9
lines changed

2 files changed

+185
-9
lines changed

prompt_testing/templates/review.html

Lines changed: 175 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@
1818
.case-card {
1919
background: white; margin-bottom: 30px; border-radius: 8px;
2020
box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden;
21+
position: relative;
22+
}
23+
.case-card.reviewed {
24+
border-left: 5px solid #16a34a;
25+
}
26+
.case-card.not-reviewed {
27+
border-left: 5px solid #dc2626;
2128
}
2229
.case-header {
2330
background: #f8f9fa; padding: 15px 20px;
@@ -26,6 +33,24 @@
2633
}
2734
.case-id { font-weight: 600; color: #333; }
2835
.case-meta { font-size: 0.9em; color: #666; }
36+
.review-status {
37+
display: inline-flex; align-items: center; gap: 8px;
38+
padding: 4px 12px; border-radius: 16px; font-size: 0.8em; font-weight: 500;
39+
}
40+
.review-status.reviewed {
41+
background: #dcfce7; color: #166534;
42+
}
43+
.review-status.not-reviewed {
44+
background: #fef2f2; color: #dc2626;
45+
}
46+
.progress-bar {
47+
background: #f3f4f6; border-radius: 8px; height: 8px; margin-top: 10px;
48+
overflow: hidden;
49+
}
50+
.progress-fill {
51+
background: linear-gradient(90deg, #16a34a, #22c55e);
52+
height: 100%; transition: width 0.3s ease;
53+
}
2954
.case-content { padding: 20px; }
3055
.section { margin-bottom: 25px; }
3156
.section h4 {
@@ -166,6 +191,13 @@ <h1>📝 Review: {{ data.summary.prompt_version if data.summary else 'Unknown' }
166191
{{ "%.2f"|format(data.summary.average_metrics.overall_score) }}</p>
167192
{% endif %}
168193
{% endif %}
194+
195+
<div id="review-progress" style="display: none;">
196+
<p><strong>Review Progress:</strong> <span id="progress-text">Loading...</span></p>
197+
<div class="progress-bar">
198+
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
199+
</div>
200+
</div>
169201
</div>
170202

171203
<div class="reviewer-info">
@@ -185,11 +217,16 @@ <h4>👤 Reviewer Information</h4>
185217
{% if result.success %}
186218
<div class="case-card">
187219
<div class="case-header">
188-
<div class="case-id">{{ result.case_id }}</div>
189-
<div class="case-meta">
190-
{{ result.test_case.language }} | {{ result.test_case.compiler }} |
191-
Audience: {{ result.test_case.audience }} |
192-
Type: {{ result.test_case.explanation_type }}
220+
<div>
221+
<div class="case-id">{{ result.case_id }}</div>
222+
<div class="case-meta">
223+
{{ result.test_case.language }} | {{ result.test_case.compiler }} |
224+
Audience: {{ result.test_case.audience }} |
225+
Type: {{ result.test_case.explanation_type }}
226+
</div>
227+
</div>
228+
<div class="review-status not-reviewed" id="status-{{ result.case_id }}">
229+
<span></span> Not Reviewed
193230
</div>
194231
</div>
195232

@@ -351,6 +388,10 @@ <h4>✏️ Your Review</h4>
351388
breaks: true
352389
});
353390

391+
// Global state for existing reviews
392+
let existingReviews = {};
393+
let reviewProgress = { total: 0, reviewed: 0 };
394+
354395
// Initialize page on load
355396
document.addEventListener('DOMContentLoaded', function() {
356397
// Load saved reviewer name from localStorage
@@ -364,6 +405,9 @@ <h4>✏️ Your Review</h4>
364405
localStorage.setItem('reviewerName', this.value);
365406
});
366407

408+
// Load existing reviews for this prompt version
409+
loadExistingReviews();
410+
367411
// Render all markdown responses
368412
{% for result in data.results %}
369413
{% if result.success %}
@@ -376,6 +420,116 @@ <h4>✏️ Your Review</h4>
376420
{% endfor %}
377421
});
378422

423+
async function loadExistingReviews() {
424+
try {
425+
const promptVersion = '{{ data.summary.prompt_version if data.summary else "unknown" }}';
426+
const response = await fetch(`/api/reviews/prompt/${encodeURIComponent(promptVersion)}`);
427+
const data = await response.json();
428+
429+
existingReviews = data.reviews_by_case || {};
430+
431+
// Update UI for each case
432+
const caseCards = document.querySelectorAll('.case-card');
433+
reviewProgress.total = caseCards.length;
434+
reviewProgress.reviewed = 0;
435+
436+
caseCards.forEach(card => {
437+
const caseId = extractCaseIdFromCard(card);
438+
if (caseId && existingReviews[caseId]) {
439+
updateCaseReviewStatus(caseId, true, existingReviews[caseId]);
440+
reviewProgress.reviewed++;
441+
} else if (caseId) {
442+
updateCaseReviewStatus(caseId, false);
443+
}
444+
});
445+
446+
updateProgressIndicator();
447+
448+
} catch (error) {
449+
console.error('Failed to load existing reviews:', error);
450+
}
451+
}
452+
453+
function extractCaseIdFromCard(card) {
454+
const statusElement = card.querySelector('[id^="status-"]');
455+
if (statusElement) {
456+
return statusElement.id.replace('status-', '');
457+
}
458+
return null;
459+
}
460+
461+
function updateCaseReviewStatus(caseId, isReviewed, reviewData = null) {
462+
const card = document.querySelector(`#status-${caseId}`).closest('.case-card');
463+
const statusElement = document.getElementById(`status-${caseId}`);
464+
const saveButton = document.querySelector(`button[onclick="saveReview('${caseId}')"]`);
465+
466+
if (isReviewed) {
467+
// Mark as reviewed
468+
card.classList.remove('not-reviewed');
469+
card.classList.add('reviewed');
470+
statusElement.classList.remove('not-reviewed');
471+
statusElement.classList.add('reviewed');
472+
statusElement.innerHTML = '<span>✅</span> Reviewed';
473+
474+
if (saveButton) {
475+
saveButton.textContent = `Update Review for ${caseId}`;
476+
}
477+
478+
// Pre-populate form if review data available
479+
if (reviewData) {
480+
populateReviewForm(caseId, reviewData);
481+
}
482+
} else {
483+
// Mark as not reviewed
484+
card.classList.remove('reviewed');
485+
card.classList.add('not-reviewed');
486+
statusElement.classList.remove('reviewed');
487+
statusElement.classList.add('not-reviewed');
488+
statusElement.innerHTML = '<span>⚪</span> Not Reviewed';
489+
490+
if (saveButton) {
491+
saveButton.textContent = `Save Review for ${caseId}`;
492+
}
493+
}
494+
}
495+
496+
function populateReviewForm(caseId, reviewData) {
497+
// Populate numeric scores
498+
const fields = ['accuracy', 'relevance', 'conciseness', 'insight', 'appropriateness'];
499+
fields.forEach(field => {
500+
const input = document.querySelector(`input[name="${field}"][data-case="${caseId}"]`);
501+
if (input && reviewData[field]) {
502+
input.value = reviewData[field];
503+
}
504+
});
505+
506+
// Populate text areas
507+
const textFields = ['strengths', 'weaknesses', 'suggestions', 'overall_comments'];
508+
textFields.forEach(field => {
509+
const textarea = document.querySelector(`textarea[name="${field}"][data-case="${caseId}"]`);
510+
if (textarea && reviewData[field]) {
511+
if (Array.isArray(reviewData[field])) {
512+
textarea.value = reviewData[field].join('\n');
513+
} else {
514+
textarea.value = reviewData[field];
515+
}
516+
}
517+
});
518+
}
519+
520+
function updateProgressIndicator() {
521+
const progressContainer = document.getElementById('review-progress');
522+
const progressText = document.getElementById('progress-text');
523+
const progressFill = document.getElementById('progress-fill');
524+
525+
if (reviewProgress.total > 0) {
526+
const percentage = Math.round((reviewProgress.reviewed / reviewProgress.total) * 100);
527+
progressText.textContent = `${reviewProgress.reviewed}/${reviewProgress.total} cases reviewed (${percentage}%)`;
528+
progressFill.style.width = `${percentage}%`;
529+
progressContainer.style.display = 'block';
530+
}
531+
}
532+
379533
async function saveReview(caseId) {
380534
const reviewerName = document.getElementById('reviewer-name').value.trim();
381535
if (!reviewerName) {
@@ -429,11 +583,21 @@ <h4>✏️ Your Review</h4>
429583
const result = await response.json();
430584

431585
if (result.success) {
432-
messageEl.textContent = '✅ Saved!';
586+
const wasUpdate = existingReviews[caseId] !== undefined;
587+
messageEl.textContent = wasUpdate ? '✅ Updated!' : '✅ Saved!';
433588
messageEl.className = 'success-msg';
434589

435-
const inputs = document.querySelectorAll(`[data-case="${caseId}"]`);
436-
inputs.forEach(input => input.disabled = true);
590+
// Update the global state
591+
existingReviews[caseId] = formData;
592+
593+
// Update UI to show reviewed status
594+
if (!wasUpdate) {
595+
reviewProgress.reviewed++;
596+
}
597+
updateCaseReviewStatus(caseId, true, formData);
598+
updateProgressIndicator();
599+
600+
// Don't disable inputs for updates, allow further editing
437601
} else {
438602
throw new Error(result.error || 'Failed to save review');
439603
}
@@ -443,7 +607,9 @@ <h4>✏️ Your Review</h4>
443607
messageEl.className = 'error-msg';
444608
} finally {
445609
button.disabled = false;
446-
button.textContent = `Save Review for ${caseId}`;
610+
// Update button text based on current review status
611+
const isReviewed = existingReviews[caseId] !== undefined;
612+
button.textContent = isReviewed ? `Update Review for ${caseId}` : `Save Review for ${caseId}`;
447613
}
448614
}
449615
</script>

prompt_testing/web_review.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,16 @@ def get_reviews(case_id):
228228
reviews = self.review_manager.load_reviews(case_id=case_id)
229229
return jsonify({"reviews": [review.__dict__ for review in reviews]})
230230

231+
@self.app.route("/api/reviews/prompt/<prompt_version>")
232+
def get_reviews_for_prompt(prompt_version):
233+
"""Get all existing reviews for a specific prompt version."""
234+
reviews = self.review_manager.load_reviews(prompt_version=prompt_version)
235+
# Group reviews by case_id for easier frontend consumption
236+
reviews_by_case = {}
237+
for review in reviews:
238+
reviews_by_case[review.case_id] = review.__dict__
239+
return jsonify({"reviews_by_case": reviews_by_case, "total_reviews": len(reviews)})
240+
231241
def start(self, open_browser: bool = True):
232242
"""Start the web server."""
233243
if open_browser:

0 commit comments

Comments
 (0)