From 5c7080f8605dda12dd84ce3859f6e23468563827 Mon Sep 17 00:00:00 2001 From: dwindown Date: Sun, 7 Jun 2026 00:38:12 +0700 Subject: [PATCH] Improve AI playground review UX --- app/admin_web.py | 140 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 4 deletions(-) diff --git a/app/admin_web.py b/app/admin_web.py index 59ffae6..e210d25 100644 --- a/app/admin_web.py +++ b/app/admin_web.py @@ -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( """ + selected_basis_id = str(basis_item_id or "") + basis_options = [''] + 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'' + ) return f"""
@@ -2269,8 +2282,10 @@ def _ai_generate_tab(
- - + +
@@ -2397,12 +2412,13 @@ def _ai_review_tab( f"{escape(_truncate(item.ai_model or '-', 42))}" f"{escape(stem_preview)}" f"{escape(str(item.created_at))}" + f"View" "" ) variant_table_rows = ( "".join(variant_rows) if variant_rows - else 'No AI-generated variants match this view.' + else 'No AI-generated variants match this view.' ) return f""" @@ -2435,7 +2451,7 @@ def _ai_review_tab(
- + {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"" + f'' + f"" + "" + ) + else: + rows.append( + f'' + ) + + return ( + '
Item IDRun IDBasisLevelStatusModelStemCreated At
Item IDRun IDBasisLevelStatusModelStemCreated AtAction
{escape(str(key).upper())}{escape(_html_to_text(str(value)))}
{escape(_html_to_text(str(options or "")))}
' + + ("".join(rows) if rows else '') + + "
OptionText
No options stored.
" + ) + + +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""" +
+
Item{variant.id}
+
Run{variant.generation_run_id or "-"}
+
Level{escape(variant.level)}
+
Status{escape(variant.variant_status)}
+
+
+

Question

+

{escape(_html_to_text(variant.stem))}

+
+

Answer Options

+ {_options_table(variant.options, variant.correct_answer)} +
+

Correct Answer

+

{escape(variant.correct_answer)}

+

Pembahasan

+

{escape(explanation)}

+
+
+

Generation Context

+

Basis item: {basis_preview}

+

Model: {escape(variant.ai_model or "-")}

+

Created at: {escape(str(variant.created_at))}

+
+ + +
+ + + Back to Review Queue +
+ + """ + + @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 = """ +
Generated variant was not found.
+ Back to Review Queue + """ + 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)