Improve AI playground review UX
This commit is contained in:
140
app/admin_web.py
140
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(
|
||||
<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="item_ids"]').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="item_ids"]').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)
|
||||
|
||||
Reference in New Issue
Block a user