diff --git a/app/admin_web.py b/app/admin_web.py index 9b9ca2a..3cc1651 100644 --- a/app/admin_web.py +++ b/app/admin_web.py @@ -21,6 +21,12 @@ from starlette.status import HTTP_303_SEE_OTHER, HTTP_401_UNAUTHORIZED from app.core.config import get_settings from app.database import get_db from app.models import Item, Session, Tryout +from app.services.ai_generation import ( + SUPPORTED_MODELS, + generate_question, + get_ai_stats, + validate_ai_model, +) from app.services.irt_calibration import get_calibration_status settings = get_settings() @@ -161,6 +167,7 @@ def _render_admin_page(title: str, page_title: str, body: str) -> HTMLResponse: Calibration Status Item Statistics Session Overview + AI Playground Password Info Logout @@ -352,6 +359,7 @@ async def dashboard_view(request: Request, db: AsyncSession = Depends(get_db)):
Sessions{sessions}
Completed Sessions{completed_sessions}
+

Open AI Playground

""" return _render_admin_page("IRT Bank Soal Admin", "Dashboard", body) @@ -451,6 +459,171 @@ async def session_overview_view(request: Request, db: AsyncSession = Depends(get return _render_admin_page("Session Overview", "Session Overview", body) +def _ai_form_body( + key_configured: bool, + stats: dict[str, Any], + error: str | None = None, + result: dict[str, Any] | None = None, + basis_item_id: str = "", + target_level: str = "mudah", + ai_model: str = settings.OPENROUTER_MODEL_QWEN, +) -> str: + error_html = f'
{escape(error)}
' if error else "" + options_html = "".join( + f'' + for model, label in SUPPORTED_MODELS.items() + ) + result_html = "" + if result: + options = result.get("options") or {} + result_html = f""" +

Preview Result

+

Model: {escape(result.get("ai_model", ""))}

+

Stem:
{escape(result.get("stem", ""))}

+

Options:

+ {_table(["Key", "Text"], [[key, value] for key, value in options.items()])} +

Correct: {escape(result.get("correct", ""))}

+

Explanation:
{escape(result.get("explanation", ""))}

+ """ + + return f""" +

OpenRouter key configured: {"Yes" if key_configured else "No"}

+

Total AI-generated items: {stats.get("total_ai_items", 0)}

+ {error_html} +
+ + + + + + + +
+

This is a preview only. Save-to-database UI is not added yet.

+ {result_html} + """ + + +@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) + if not admin: + return _login_redirect() + + stats = await get_ai_stats(db) + body = _ai_form_body(bool(settings.OPENROUTER_API_KEY), stats) + return _render_admin_page("AI Playground", "AI Playground", body) + + +@router.post("/ai-playground", include_in_schema=False) +async def ai_playground_submit( + request: Request, + db: AsyncSession = Depends(get_db), + basis_item_id: int = Form(...), + target_level: str = Form(...), + ai_model: str = Form(...), +): + admin = await _current_admin(request) + if not admin: + return _login_redirect() + + stats = await get_ai_stats(db) + + if not settings.OPENROUTER_API_KEY: + body = _ai_form_body( + False, + stats, + error="OPENROUTER_API_KEY is not configured in the environment.", + basis_item_id=str(basis_item_id), + target_level=target_level, + ai_model=ai_model, + ) + return _render_admin_page("AI Playground", "AI Playground", body) + + if target_level not in {"mudah", "sulit"}: + body = _ai_form_body( + True, + stats, + error="Target level must be mudah or sulit.", + basis_item_id=str(basis_item_id), + target_level=target_level, + ai_model=ai_model, + ) + return _render_admin_page("AI Playground", "AI Playground", body) + + if not validate_ai_model(ai_model): + body = _ai_form_body( + True, + stats, + error="Unsupported AI model.", + basis_item_id=str(basis_item_id), + target_level=target_level, + ai_model=ai_model, + ) + return _render_admin_page("AI Playground", "AI Playground", body) + + result = await db.execute(select(Item).where(Item.id == basis_item_id)) + basis_item = result.scalar_one_or_none() + if not basis_item: + body = _ai_form_body( + True, + stats, + error=f"Basis item not found: {basis_item_id}", + basis_item_id=str(basis_item_id), + target_level=target_level, + ai_model=ai_model, + ) + return _render_admin_page("AI Playground", "AI Playground", body) + + if basis_item.level != "sedang": + body = _ai_form_body( + True, + stats, + error=f"Basis item must be sedang level, got: {basis_item.level}", + basis_item_id=str(basis_item_id), + target_level=target_level, + ai_model=ai_model, + ) + return _render_admin_page("AI Playground", "AI Playground", body) + + generated = await generate_question( + basis_item=basis_item, + target_level=target_level, + ai_model=ai_model, + ) + if not generated: + body = _ai_form_body( + True, + stats, + error="AI generation failed. Check OPENROUTER_API_KEY, model availability, and server logs.", + basis_item_id=str(basis_item_id), + target_level=target_level, + ai_model=ai_model, + ) + return _render_admin_page("AI Playground", "AI Playground", body) + + body = _ai_form_body( + True, + stats, + result={ + "stem": generated.stem, + "options": generated.options, + "correct": generated.correct, + "explanation": generated.explanation or "", + "ai_model": ai_model, + }, + basis_item_id=str(basis_item_id), + target_level=target_level, + ai_model=ai_model, + ) + return _render_admin_page("AI Playground", "AI Playground", body) + + @router.get("/tryout/list", include_in_schema=False) @router.get("/item/list", include_in_schema=False) @router.get("/user/list", include_in_schema=False)