@@ -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(
- | Item ID | Run ID | Basis | Level | Status | Model | Stem | Created At |
+ | Item ID | Run ID | Basis | Level | Status | Model | Stem | Created At | Action |
{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'| {escape(str(key).upper())} | '
+ f"{escape(_html_to_text(str(value)))} | "
+ "
"
+ )
+ else:
+ rows.append(
+ f'| {escape(_html_to_text(str(options or "")))} |
'
+ )
+
+ return (
+ '| Option | Text |
'
+ + ("".join(rows) if rows else '| 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))}
+
+
+ """
+
+
@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)