Improve AI playground review UX

This commit is contained in:
dwindown
2026-06-07 00:38:12 +07:00
parent 4e7d79501c
commit 5c7080f860

View File

@@ -228,6 +228,10 @@ def _render_admin_page(request: Request, title: str, page_title: str, body: str)
.status-stale {{ background: #fef3c7; color: #92400e; }}
.button-link {{ display: inline-block; padding: 9px 12px; border-radius: 8px; background: #0f172a; color: #fff; text-decoration: none; font-size: 13px; font-weight: 700; }}
.secondary-link {{ display: inline-block; padding: 10px 12px; border-radius: 8px; background: #e2e8f0; color: #0f172a; text-decoration: none; font-size: 14px; font-weight: 700; }}
.question-block {{ margin: 16px 0; padding: 16px; border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; }}
.question-block h3 {{ margin-top: 0; }}
.option-key {{ width: 56px; font-weight: 800; color: #0f172a; }}
.correct-option td {{ background: #ecfdf5; color: #166534; font-weight: 700; }}
@media (max-width: 860px) {{
.layout {{ grid-template-columns: 1fr; }}
.sidebar {{ position: static; }}
@@ -2261,6 +2265,15 @@ def _ai_generate_tab(
<button type="submit">Seed Demo Basis Item</button>
</form>
"""
selected_basis_id = str(basis_item_id or "")
basis_options = ['<option value="">Select a sedang basis item</option>']
for item in basis_items:
item_id = str(item.id)
selected = _selected_option(item_id, selected_basis_id)
stem_preview = _truncate(_html_to_text(item.stem), 82)
basis_options.append(
f'<option value="{item.id}" {selected}>#{item.id} | Tryout {escape(str(item.tryout_id))} | Slot {item.slot} | {escape(stem_preview)}</option>'
)
return f"""
<section class="tab-panel">
@@ -2269,8 +2282,10 @@ def _ai_generate_tab(
<form method="post" action="/admin/ai-playground?tab=generate" autocomplete="off">
<div class="field-grid">
<div>
<label for="basis_item_id">Basis Item ID</label>
<input id="basis_item_id" name="basis_item_id" type="number" value="{escape(basis_item_id)}">
<label for="basis_item_id">Basis Item</label>
<select id="basis_item_id" name="basis_item_id" required>
{"".join(basis_options)}
</select>
</div>
<div>
<label for="target_level">Target Level</label>
@@ -2397,12 +2412,13 @@ def _ai_review_tab(
f"<td>{escape(_truncate(item.ai_model or '-', 42))}</td>"
f"<td>{escape(stem_preview)}</td>"
f"<td>{escape(str(item.created_at))}</td>"
f"<td><a class=\"secondary-link\" href=\"/admin/ai-playground/variants/{item.id}\">View</a></td>"
"</tr>"
)
variant_table_rows = (
"".join(variant_rows)
if variant_rows
else '<tr><td colspan="9">No AI-generated variants match this view.</td></tr>'
else '<tr><td colspan="10">No AI-generated variants match this view.</td></tr>'
)
return f"""
@@ -2435,7 +2451,7 @@ def _ai_review_tab(
<div class="table-wrap">
<table>
<thead>
<tr><th><input type="checkbox" onclick="document.querySelectorAll('input[name=&quot;item_ids&quot;]').forEach(el => el.checked = this.checked)"></th><th>Item ID</th><th>Run ID</th><th>Basis</th><th>Level</th><th>Status</th><th>Model</th><th>Stem</th><th>Created At</th></tr>
<tr><th><input type="checkbox" onclick="document.querySelectorAll('input[name=&quot;item_ids&quot;]').forEach(el => el.checked = this.checked)"></th><th>Item ID</th><th>Run ID</th><th>Basis</th><th>Level</th><th>Status</th><th>Model</th><th>Stem</th><th>Created At</th><th>Action</th></tr>
</thead>
<tbody>
{variant_table_rows}
@@ -2507,6 +2523,88 @@ def _ai_form_body(
"""
def _options_table(options: Any, correct_answer: str | None) -> str:
normalized_correct = str(correct_answer or "").strip().upper()
rows = []
if isinstance(options, dict):
options_by_key = {str(key).strip().upper(): value for key, value in options.items()}
option_keys = [key for key in ("A", "B", "C", "D") if key in options_by_key]
option_keys.extend(sorted(key for key in options_by_key.keys() if key not in option_keys))
for key in option_keys:
value = options_by_key.get(key)
row_class = ' class="correct-option"' if str(key).upper() == normalized_correct else ""
rows.append(
f"<tr{row_class}>"
f'<td class="option-key">{escape(str(key).upper())}</td>'
f"<td>{escape(_html_to_text(str(value)))}</td>"
"</tr>"
)
else:
rows.append(
f'<tr><td colspan="2">{escape(_html_to_text(str(options or "")))}</td></tr>'
)
return (
'<div class="table-wrap"><table><thead><tr><th>Option</th><th>Text</th></tr></thead><tbody>'
+ ("".join(rows) if rows else '<tr><td colspan="2">No options stored.</td></tr>')
+ "</tbody></table></div>"
)
def _ai_variant_detail_body(variant: Item, basis_item: Item | None) -> str:
explanation = _html_to_text(variant.explanation) if variant.explanation else "-"
basis_preview = "-"
if basis_item is not None:
basis_preview = (
f"#{basis_item.id} | Tryout {escape(str(basis_item.tryout_id))} | "
f"Slot {basis_item.slot} | {escape(_truncate(_html_to_text(basis_item.stem), 160))}"
)
review_url = "/admin/ai-playground?tab=review"
if variant.generation_run_id:
review_url = f"{review_url}&run_id={variant.generation_run_id}"
return f"""
<div class="compact-strip">
<div class="compact-stat"><span>Item</span><strong>{variant.id}</strong></div>
<div class="compact-stat"><span>Run</span><strong>{variant.generation_run_id or "-"}</strong></div>
<div class="compact-stat"><span>Level</span><strong>{escape(variant.level)}</strong></div>
<div class="compact-stat"><span>Status</span><strong>{escape(variant.variant_status)}</strong></div>
</div>
<div class="question-block">
<h3>Question</h3>
<p>{escape(_html_to_text(variant.stem))}</p>
</div>
<h3>Answer Options</h3>
{_options_table(variant.options, variant.correct_answer)}
<div class="question-block">
<h3>Correct Answer</h3>
<p><strong>{escape(variant.correct_answer)}</strong></p>
<h3>Pembahasan</h3>
<p>{escape(explanation)}</p>
</div>
<div class="question-block">
<h3>Generation Context</h3>
<p class="muted">Basis item: <strong>{basis_preview}</strong></p>
<p class="muted">Model: <strong>{escape(variant.ai_model or "-")}</strong></p>
<p class="muted">Created at: <strong>{escape(str(variant.created_at))}</strong></p>
</div>
<form method="post" action="/admin/ai-playground/review-bulk?tab=review">
<input type="hidden" name="item_ids" value="{variant.id}">
<div class="actions">
<select name="action" style="max-width:260px">
<option value="approved">Approve this item</option>
<option value="rejected">Reject this item</option>
<option value="archived">Archive this item</option>
<option value="stale">Mark stale</option>
<option value="active">Activate this item</option>
</select>
<button type="submit">Apply</button>
<a class="secondary-link" href="{review_url}">Back to Review Queue</a>
</div>
</form>
"""
@router.get("/ai-playground", include_in_schema=False)
async def ai_playground_view(request: Request, db: AsyncSession = Depends(get_db)):
admin = await _current_admin(request)
@@ -2553,6 +2651,40 @@ async def ai_playground_view(request: Request, db: AsyncSession = Depends(get_db
return _render_admin_page(request, "AI Playground", "AI Playground", body)
@router.get("/ai-playground/variants/{item_id}", include_in_schema=False)
async def ai_playground_variant_detail(
item_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
):
admin = await _current_admin(request)
if not admin:
return _login_redirect()
result = await db.execute(
select(Item).where(Item.id == item_id, Item.generated_by == "ai")
)
variant = result.scalar_one_or_none()
if variant is None:
body = """
<div class="error">Generated variant was not found.</div>
<a class="secondary-link" href="/admin/ai-playground?tab=review">Back to Review Queue</a>
"""
return _render_admin_page(request, "Generated Variant", "Generated Variant", body)
basis_item = None
if variant.basis_item_id:
basis_item = await db.get(Item, variant.basis_item_id)
body = _ai_variant_detail_body(variant, basis_item)
return _render_admin_page(
request,
f"Generated Variant #{variant.id}",
f"Generated Variant #{variant.id}",
body,
)
@router.post("/ai-playground/seed-demo", include_in_schema=False)
async def ai_playground_seed_demo(request: Request, db: AsyncSession = Depends(get_db)):
admin = await _current_admin(request)