diff --git a/app/admin_web.py b/app/admin_web.py index ea30494..7032643 100644 --- a/app/admin_web.py +++ b/app/admin_web.py @@ -181,6 +181,8 @@ def _render_admin_page(title: str, page_title: str, body: str) -> HTMLResponse: label {{ display: block; font-size: 14px; font-weight: 600; margin: 14px 0 8px; }} button {{ border: 0; border-radius: 10px; padding: 12px 14px; background: #0f172a; color: #fff; font-size: 15px; font-weight: 600; cursor: pointer; }} .actions {{ display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; }} + .row {{ display: flex; align-items: center; gap: 10px; margin-top: 12px; color: #334155; font-size: 14px; }} + .row input {{ width: auto; }} .error {{ margin: 0 0 16px; padding: 12px 14px; border-radius: 10px; background: #fef2f2; color: #991b1b; border: 1px solid #fecaca; }} .success {{ margin: 0 0 16px; padding: 12px 14px; border-radius: 10px; background: #ecfdf5; color: #166534; border: 1px solid #86efac; }} .muted {{ color: #64748b; font-size: 14px; }} @@ -618,6 +620,8 @@ def _basis_item_workspace_body( ai_model: str = settings.OPENROUTER_MODEL_QWEN, generation_count: str = "1", operator_notes: str = "", + include_note_for_admin: bool = True, + include_note_in_prompt: bool = False, ) -> str: error_html = f'
{escape(error)}
' if error else "" success_html = f'
{escape(success)}
' if success else "" @@ -717,6 +721,9 @@ def _basis_item_workspace_body(

Recommended: 1-3 per run. Larger runs increase overlap and review burden.

+ + +

Example note: Use clinical language, avoid negatives, keep stem under 40 words.

@@ -1772,6 +1779,8 @@ async def basis_item_generate_submit( ai_model: str = Form(...), generation_count: int = Form(1), operator_notes: str = Form(""), + include_note_for_admin: str | None = Form(None), + include_note_in_prompt: str | None = Form(None), ): admin = await _current_admin(request) if not admin: @@ -1782,6 +1791,9 @@ async def basis_item_generate_submit( if basis_item is None or basis_item.generated_by == "ai" or basis_item.level != "sedang": return RedirectResponse(url="/admin/basis-items", status_code=HTTP_303_SEE_OTHER) + note_for_admin = include_note_for_admin == "on" + note_in_prompt = include_note_in_prompt == "on" + if not settings.OPENROUTER_API_KEY: run_result = await db.execute( select(AIGenerationRun) @@ -1810,6 +1822,8 @@ async def basis_item_generate_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page( f"Basis Item #{basis_item.id}", @@ -1831,7 +1845,7 @@ async def basis_item_generate_submit( requested_count=generation_count, model=ai_model, created_by=admin.username, - operator_notes=operator_notes.strip() or None, + operator_notes=(operator_notes.strip() or None) if note_for_admin else None, db=db, ) generated = await generate_questions_batch( @@ -1839,6 +1853,7 @@ async def basis_item_generate_submit( target_level=target_level, ai_model=ai_model, count=generation_count, + operator_notes=operator_notes if note_in_prompt else None, ) from app.schemas.ai import GeneratedQuestion @@ -1894,6 +1909,8 @@ async def basis_item_generate_submit( target_level=target_level, ai_model=ai_model, generation_count=str(generation_count), + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page( f"Basis Item #{basis_item.id}", @@ -1981,6 +1998,8 @@ def _ai_form_body( ai_model: str = settings.OPENROUTER_MODEL_QWEN, generation_count: str = "1", operator_notes: str = "", + include_note_for_admin: bool = True, + include_note_in_prompt: bool = False, ) -> str: error_html = f'
{escape(error)}
' if error else "" success_html = f'
{escape(success)}
' if success else "" @@ -2102,6 +2121,9 @@ def _ai_form_body(

Recommended: 1-3 variants per run. Larger runs can increase overlap and review burden. Backend safety cap: 50.

+ + +

Example note: Use simple wording, one-step arithmetic, avoid trick options.

Available Sedang Basis Items

@@ -2176,6 +2198,8 @@ async def ai_playground_submit( ai_model: str = Form(...), generation_count: int = Form(1), operator_notes: str = Form(""), + include_note_for_admin: str | None = Form(None), + include_note_in_prompt: str | None = Form(None), ): admin = await _current_admin(request) if not admin: @@ -2186,6 +2210,9 @@ async def ai_playground_submit( generation_runs = await _recent_generation_runs(db) generated_variants = await _recent_generated_variants(db) + note_for_admin = include_note_for_admin == "on" + note_in_prompt = include_note_in_prompt == "on" + if not settings.OPENROUTER_API_KEY: body = _ai_form_body( False, @@ -2199,6 +2226,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) @@ -2215,6 +2244,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) @@ -2231,6 +2262,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) @@ -2249,6 +2282,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) @@ -2265,6 +2300,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) @@ -2281,6 +2318,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) @@ -2291,7 +2330,7 @@ async def ai_playground_submit( requested_count=generation_count, model=ai_model, created_by=admin.username, - operator_notes=operator_notes.strip() or None, + operator_notes=(operator_notes.strip() or None) if note_for_admin else None, db=db, ) @@ -2300,6 +2339,7 @@ async def ai_playground_submit( target_level=target_level, ai_model=ai_model, count=generation_count, + operator_notes=operator_notes if note_in_prompt else None, ) saved_item_ids: list[int] = [] @@ -2346,6 +2386,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) @@ -2367,6 +2409,8 @@ async def ai_playground_submit( ai_model=ai_model, generation_count=str(generation_count), operator_notes=operator_notes, + include_note_for_admin=note_for_admin, + include_note_in_prompt=note_in_prompt, ) return _render_admin_page("AI Playground", "AI Playground", body) diff --git a/app/services/ai_generation.py b/app/services/ai_generation.py index 097c056..5fa283b 100644 --- a/app/services/ai_generation.py +++ b/app/services/ai_generation.py @@ -49,6 +49,7 @@ def get_prompt_template( basis_correct: str, basis_explanation: Optional[str], target_level: Literal["mudah", "sulit"], + operator_notes: Optional[str] = None, ) -> str: """ Generate standardized prompt for AI question generation. @@ -75,6 +76,15 @@ def get_prompt_template( else "Explanation: (not provided)" ) + notes_block = "" + if operator_notes and operator_notes.strip(): + notes_block = f""" +ADDITIONAL OPERATOR NOTES: +{operator_notes.strip()} + +Apply these notes as style constraints as long as they do not conflict with correctness. +""" + prompt = f"""You are an educational content creator specializing in creating assessment questions. Given a "Sedang" (medium difficulty) question, generate a new question at a different difficulty level. @@ -88,6 +98,7 @@ Correct Answer: {basis_correct} TASK: Generate 1 new question that is {level_desc} than the basis question above. +{notes_block} REQUIREMENTS: 1. Keep the SAME topic/subject matter as the basis question @@ -380,6 +391,7 @@ async def generate_question( basis_item: Item, target_level: Literal["mudah", "sulit"], ai_model: str = settings.OPENROUTER_MODEL_QWEN, + operator_notes: Optional[str] = None, ) -> Optional[GeneratedQuestion]: """ Generate a new question based on a basis item. @@ -399,6 +411,7 @@ async def generate_question( basis_correct=basis_item.correct_answer, basis_explanation=basis_item.explanation, target_level=target_level, + operator_notes=operator_notes, ) max_generation_attempts = 2 @@ -679,6 +692,7 @@ async def generate_questions_batch( target_level: Literal["mudah", "sulit"], ai_model: str, count: int, + operator_notes: Optional[str] = None, ) -> list[GeneratedQuestion]: generated_items: list[GeneratedQuestion] = [] for _ in range(count): @@ -686,6 +700,7 @@ async def generate_questions_batch( basis_item=basis_item, target_level=target_level, ai_model=ai_model, + operator_notes=operator_notes, ) if generated is not None: generated_items.append(generated)