Add operator note controls for admin visibility and AI prompt inclusion
This commit is contained in:
@@ -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; }}
|
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; }}
|
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; }}
|
.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; }}
|
.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; }}
|
.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; }}
|
.muted {{ color: #64748b; font-size: 14px; }}
|
||||||
@@ -618,6 +620,8 @@ def _basis_item_workspace_body(
|
|||||||
ai_model: str = settings.OPENROUTER_MODEL_QWEN,
|
ai_model: str = settings.OPENROUTER_MODEL_QWEN,
|
||||||
generation_count: str = "1",
|
generation_count: str = "1",
|
||||||
operator_notes: str = "",
|
operator_notes: str = "",
|
||||||
|
include_note_for_admin: bool = True,
|
||||||
|
include_note_in_prompt: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
error_html = f'<div class="error">{escape(error)}</div>' if error else ""
|
error_html = f'<div class="error">{escape(error)}</div>' if error else ""
|
||||||
success_html = f'<div class="success">{escape(success)}</div>' if success else ""
|
success_html = f'<div class="success">{escape(success)}</div>' if success else ""
|
||||||
@@ -717,6 +721,9 @@ def _basis_item_workspace_body(
|
|||||||
<p class="muted">Recommended: 1-3 per run. Larger runs increase overlap and review burden.</p>
|
<p class="muted">Recommended: 1-3 per run. Larger runs increase overlap and review burden.</p>
|
||||||
<label for="operator_notes">Operator Notes (optional)</label>
|
<label for="operator_notes">Operator Notes (optional)</label>
|
||||||
<textarea id="operator_notes" name="operator_notes" rows="3">{escape(operator_notes)}</textarea>
|
<textarea id="operator_notes" name="operator_notes" rows="3">{escape(operator_notes)}</textarea>
|
||||||
|
<label class="row"><input type="checkbox" name="include_note_for_admin" {"checked" if include_note_for_admin else ""}> Save note for admin team (visible in run history)</label>
|
||||||
|
<label class="row"><input type="checkbox" name="include_note_in_prompt" {"checked" if include_note_in_prompt else ""}> Include note in AI prompt payload</label>
|
||||||
|
<p class="muted" style="margin-top:6px;">Example note: <code>Use clinical language, avoid negatives, keep stem under 40 words.</code></p>
|
||||||
<button type="submit">Generate Variants</button>
|
<button type="submit">Generate Variants</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
@@ -1772,6 +1779,8 @@ async def basis_item_generate_submit(
|
|||||||
ai_model: str = Form(...),
|
ai_model: str = Form(...),
|
||||||
generation_count: int = Form(1),
|
generation_count: int = Form(1),
|
||||||
operator_notes: str = Form(""),
|
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)
|
admin = await _current_admin(request)
|
||||||
if not admin:
|
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":
|
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)
|
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:
|
if not settings.OPENROUTER_API_KEY:
|
||||||
run_result = await db.execute(
|
run_result = await db.execute(
|
||||||
select(AIGenerationRun)
|
select(AIGenerationRun)
|
||||||
@@ -1810,6 +1822,8 @@ async def basis_item_generate_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
operator_notes=operator_notes,
|
||||||
|
include_note_for_admin=note_for_admin,
|
||||||
|
include_note_in_prompt=note_in_prompt,
|
||||||
)
|
)
|
||||||
return _render_admin_page(
|
return _render_admin_page(
|
||||||
f"Basis Item #{basis_item.id}",
|
f"Basis Item #{basis_item.id}",
|
||||||
@@ -1831,7 +1845,7 @@ async def basis_item_generate_submit(
|
|||||||
requested_count=generation_count,
|
requested_count=generation_count,
|
||||||
model=ai_model,
|
model=ai_model,
|
||||||
created_by=admin.username,
|
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,
|
db=db,
|
||||||
)
|
)
|
||||||
generated = await generate_questions_batch(
|
generated = await generate_questions_batch(
|
||||||
@@ -1839,6 +1853,7 @@ async def basis_item_generate_submit(
|
|||||||
target_level=target_level,
|
target_level=target_level,
|
||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
count=generation_count,
|
count=generation_count,
|
||||||
|
operator_notes=operator_notes if note_in_prompt else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
from app.schemas.ai import GeneratedQuestion
|
from app.schemas.ai import GeneratedQuestion
|
||||||
@@ -1894,6 +1909,8 @@ async def basis_item_generate_submit(
|
|||||||
target_level=target_level,
|
target_level=target_level,
|
||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
|
include_note_for_admin=note_for_admin,
|
||||||
|
include_note_in_prompt=note_in_prompt,
|
||||||
)
|
)
|
||||||
return _render_admin_page(
|
return _render_admin_page(
|
||||||
f"Basis Item #{basis_item.id}",
|
f"Basis Item #{basis_item.id}",
|
||||||
@@ -1981,6 +1998,8 @@ def _ai_form_body(
|
|||||||
ai_model: str = settings.OPENROUTER_MODEL_QWEN,
|
ai_model: str = settings.OPENROUTER_MODEL_QWEN,
|
||||||
generation_count: str = "1",
|
generation_count: str = "1",
|
||||||
operator_notes: str = "",
|
operator_notes: str = "",
|
||||||
|
include_note_for_admin: bool = True,
|
||||||
|
include_note_in_prompt: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
error_html = f'<div class="error">{escape(error)}</div>' if error else ""
|
error_html = f'<div class="error">{escape(error)}</div>' if error else ""
|
||||||
success_html = f'<div class="success">{escape(success)}</div>' if success else ""
|
success_html = f'<div class="success">{escape(success)}</div>' if success else ""
|
||||||
@@ -2102,6 +2121,9 @@ def _ai_form_body(
|
|||||||
<p class="muted">Recommended: 1-3 variants per run. Larger runs can increase overlap and review burden. Backend safety cap: 50.</p>
|
<p class="muted">Recommended: 1-3 variants per run. Larger runs can increase overlap and review burden. Backend safety cap: 50.</p>
|
||||||
<label for="operator_notes">Optional Notes (style hints)</label>
|
<label for="operator_notes">Optional Notes (style hints)</label>
|
||||||
<textarea id="operator_notes" name="operator_notes" rows="3" placeholder="Optional generation note for this run">{escape(operator_notes)}</textarea>
|
<textarea id="operator_notes" name="operator_notes" rows="3" placeholder="Optional generation note for this run">{escape(operator_notes)}</textarea>
|
||||||
|
<label class="row"><input type="checkbox" name="include_note_for_admin" {"checked" if include_note_for_admin else ""}> Save note for admin team (visible in run history)</label>
|
||||||
|
<label class="row"><input type="checkbox" name="include_note_in_prompt" {"checked" if include_note_in_prompt else ""}> Include note in AI prompt payload</label>
|
||||||
|
<p class="muted" style="margin-top:6px;">Example note: <code>Use simple wording, one-step arithmetic, avoid trick options.</code></p>
|
||||||
<button type="submit">Generate Run</button>
|
<button type="submit">Generate Run</button>
|
||||||
</form>
|
</form>
|
||||||
<h3 style="margin-top:24px">Available Sedang Basis Items</h3>
|
<h3 style="margin-top:24px">Available Sedang Basis Items</h3>
|
||||||
@@ -2176,6 +2198,8 @@ async def ai_playground_submit(
|
|||||||
ai_model: str = Form(...),
|
ai_model: str = Form(...),
|
||||||
generation_count: int = Form(1),
|
generation_count: int = Form(1),
|
||||||
operator_notes: str = Form(""),
|
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)
|
admin = await _current_admin(request)
|
||||||
if not admin:
|
if not admin:
|
||||||
@@ -2186,6 +2210,9 @@ async def ai_playground_submit(
|
|||||||
generation_runs = await _recent_generation_runs(db)
|
generation_runs = await _recent_generation_runs(db)
|
||||||
generated_variants = await _recent_generated_variants(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:
|
if not settings.OPENROUTER_API_KEY:
|
||||||
body = _ai_form_body(
|
body = _ai_form_body(
|
||||||
False,
|
False,
|
||||||
@@ -2199,6 +2226,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
@@ -2215,6 +2244,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
@@ -2231,6 +2262,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
@@ -2249,6 +2282,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
@@ -2265,6 +2300,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
@@ -2281,6 +2318,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
@@ -2291,7 +2330,7 @@ async def ai_playground_submit(
|
|||||||
requested_count=generation_count,
|
requested_count=generation_count,
|
||||||
model=ai_model,
|
model=ai_model,
|
||||||
created_by=admin.username,
|
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,
|
db=db,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2300,6 +2339,7 @@ async def ai_playground_submit(
|
|||||||
target_level=target_level,
|
target_level=target_level,
|
||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
count=generation_count,
|
count=generation_count,
|
||||||
|
operator_notes=operator_notes if note_in_prompt else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
saved_item_ids: list[int] = []
|
saved_item_ids: list[int] = []
|
||||||
@@ -2346,6 +2386,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
@@ -2367,6 +2409,8 @@ async def ai_playground_submit(
|
|||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
generation_count=str(generation_count),
|
generation_count=str(generation_count),
|
||||||
operator_notes=operator_notes,
|
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)
|
return _render_admin_page("AI Playground", "AI Playground", body)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ def get_prompt_template(
|
|||||||
basis_correct: str,
|
basis_correct: str,
|
||||||
basis_explanation: Optional[str],
|
basis_explanation: Optional[str],
|
||||||
target_level: Literal["mudah", "sulit"],
|
target_level: Literal["mudah", "sulit"],
|
||||||
|
operator_notes: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate standardized prompt for AI question generation.
|
Generate standardized prompt for AI question generation.
|
||||||
@@ -75,6 +76,15 @@ def get_prompt_template(
|
|||||||
else "Explanation: (not provided)"
|
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.
|
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.
|
Given a "Sedang" (medium difficulty) question, generate a new question at a different difficulty level.
|
||||||
@@ -88,6 +98,7 @@ Correct Answer: {basis_correct}
|
|||||||
|
|
||||||
TASK:
|
TASK:
|
||||||
Generate 1 new question that is {level_desc} than the basis question above.
|
Generate 1 new question that is {level_desc} than the basis question above.
|
||||||
|
{notes_block}
|
||||||
|
|
||||||
REQUIREMENTS:
|
REQUIREMENTS:
|
||||||
1. Keep the SAME topic/subject matter as the basis question
|
1. Keep the SAME topic/subject matter as the basis question
|
||||||
@@ -380,6 +391,7 @@ async def generate_question(
|
|||||||
basis_item: Item,
|
basis_item: Item,
|
||||||
target_level: Literal["mudah", "sulit"],
|
target_level: Literal["mudah", "sulit"],
|
||||||
ai_model: str = settings.OPENROUTER_MODEL_QWEN,
|
ai_model: str = settings.OPENROUTER_MODEL_QWEN,
|
||||||
|
operator_notes: Optional[str] = None,
|
||||||
) -> Optional[GeneratedQuestion]:
|
) -> Optional[GeneratedQuestion]:
|
||||||
"""
|
"""
|
||||||
Generate a new question based on a basis item.
|
Generate a new question based on a basis item.
|
||||||
@@ -399,6 +411,7 @@ async def generate_question(
|
|||||||
basis_correct=basis_item.correct_answer,
|
basis_correct=basis_item.correct_answer,
|
||||||
basis_explanation=basis_item.explanation,
|
basis_explanation=basis_item.explanation,
|
||||||
target_level=target_level,
|
target_level=target_level,
|
||||||
|
operator_notes=operator_notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
max_generation_attempts = 2
|
max_generation_attempts = 2
|
||||||
@@ -679,6 +692,7 @@ async def generate_questions_batch(
|
|||||||
target_level: Literal["mudah", "sulit"],
|
target_level: Literal["mudah", "sulit"],
|
||||||
ai_model: str,
|
ai_model: str,
|
||||||
count: int,
|
count: int,
|
||||||
|
operator_notes: Optional[str] = None,
|
||||||
) -> list[GeneratedQuestion]:
|
) -> list[GeneratedQuestion]:
|
||||||
generated_items: list[GeneratedQuestion] = []
|
generated_items: list[GeneratedQuestion] = []
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
@@ -686,6 +700,7 @@ async def generate_questions_batch(
|
|||||||
basis_item=basis_item,
|
basis_item=basis_item,
|
||||||
target_level=target_level,
|
target_level=target_level,
|
||||||
ai_model=ai_model,
|
ai_model=ai_model,
|
||||||
|
operator_notes=operator_notes,
|
||||||
)
|
)
|
||||||
if generated is not None:
|
if generated is not None:
|
||||||
generated_items.append(generated)
|
generated_items.append(generated)
|
||||||
|
|||||||
Reference in New Issue
Block a user