Compact AI playground admin layout

This commit is contained in:
dwindown
2026-06-06 22:25:49 +07:00
parent fd7989f673
commit 4e7d79501c

View File

@@ -206,6 +206,34 @@ def _render_admin_page(request: Request, title: str, page_title: str, body: str)
.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; }}
.tabs {{ display: flex; gap: 8px; flex-wrap: wrap; margin: 18px 0 18px; border-bottom: 1px solid #e2e8f0; }}
.tabs a {{ display: inline-flex; align-items: center; min-height: 38px; padding: 0 14px; color: #475569; text-decoration: none; border: 1px solid transparent; border-bottom: 0; border-radius: 8px 8px 0 0; font-weight: 700; font-size: 14px; }}
.tabs a.active {{ background: #fff; border-color: #e2e8f0; color: #0f172a; box-shadow: 0 -1px 0 #fff inset; }}
.compact-strip {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin: 14px 0; }}
.compact-stat {{ border: 1px solid #e2e8f0; border-radius: 8px; background: #f8fafc; padding: 12px 14px; }}
.compact-stat span {{ display: block; color: #64748b; font-size: 12px; font-weight: 700; text-transform: uppercase; }}
.compact-stat strong {{ display: block; margin-top: 4px; color: #0f172a; font-size: 20px; line-height: 1.1; }}
.field-grid {{ display: grid; grid-template-columns: repeat(2, minmax(180px, 1fr)); gap: 12px 16px; align-items: end; }}
.field-grid .wide {{ grid-column: 1 / -1; }}
.tab-panel {{ margin-top: 8px; }}
.toolbar {{ display: flex; align-items: end; gap: 12px; flex-wrap: wrap; margin: 12px 0 16px; }}
.toolbar label {{ min-width: 150px; margin-top: 0; }}
.toolbar input, .toolbar select {{ min-width: 150px; }}
.table-wrap {{ width: 100%; overflow-x: auto; }}
.table-wrap table {{ min-width: 860px; }}
.status-pill {{ display: inline-flex; align-items: center; min-height: 22px; padding: 0 8px; border-radius: 999px; background: #e2e8f0; color: #334155; font-size: 12px; font-weight: 700; }}
.status-approved, .status-active {{ background: #dcfce7; color: #166534; }}
.status-rejected, .status-archived {{ background: #fee2e2; color: #991b1b; }}
.status-draft {{ background: #e0f2fe; color: #075985; }}
.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; }}
@media (max-width: 860px) {{
.layout {{ grid-template-columns: 1fr; }}
.sidebar {{ position: static; }}
.content {{ padding: 18px; }}
.field-grid {{ grid-template-columns: 1fr; }}
}}
</style> </style>
</head> </head>
<body> <body>
@@ -586,10 +614,19 @@ async def _recent_generated_variants(
db: AsyncSession, db: AsyncSession,
limit: int = 100, limit: int = 100,
basis_item_id: int | None = None, basis_item_id: int | None = None,
status_filter: str | None = None,
level_filter: str | None = None,
run_id_filter: int | None = None,
) -> list[Item]: ) -> list[Item]:
stmt = select(Item).where(Item.generated_by == "ai") stmt = select(Item).where(Item.generated_by == "ai")
if basis_item_id is not None: if basis_item_id is not None:
stmt = stmt.where(Item.basis_item_id == basis_item_id) stmt = stmt.where(Item.basis_item_id == basis_item_id)
if status_filter:
stmt = stmt.where(Item.variant_status == status_filter)
if level_filter:
stmt = stmt.where(Item.level == level_filter)
if run_id_filter is not None:
stmt = stmt.where(Item.generation_run_id == run_id_filter)
result = await db.execute( result = await db.execute(
stmt.order_by(Item.created_at.desc(), Item.id.desc()).limit(limit) stmt.order_by(Item.created_at.desc(), Item.id.desc()).limit(limit)
) )
@@ -2136,6 +2173,280 @@ async def basis_item_review_bulk(
) )
AI_PLAYGROUND_TABS = (
("generate", "Generate"),
("review", "Review Queue"),
("runs", "Runs"),
("basis", "Basis Items"),
)
AI_VARIANT_STATUSES = ("draft", "approved", "active", "rejected", "archived", "stale")
AI_VARIANT_LEVELS = ("mudah", "sulit")
def _selected_option(value: str, selected_value: str) -> str:
return "selected" if value == selected_value else ""
def _ai_tab_nav(active_tab: str) -> str:
links = []
for tab, label in AI_PLAYGROUND_TABS:
active_class = "active" if tab == active_tab else ""
aria = ' aria-current="page"' if tab == active_tab else ""
links.append(
f'<a class="{active_class}" href="/admin/ai-playground?tab={tab}"{aria}>{escape(label)}</a>'
)
return f'<nav class="tabs" aria-label="AI Playground sections">{"".join(links)}</nav>'
def _status_pill(status: str | None) -> str:
value = status or "unknown"
css_value = re.sub(r"[^a-z0-9_-]+", "-", value.lower())
return f'<span class="status-pill status-{escape(css_value)}">{escape(value)}</span>'
def _ai_status_strip(
key_configured: bool,
stats: dict[str, Any],
generation_runs: list[AIGenerationRun],
generation_summary: dict[str, Any] | None = None,
) -> str:
latest_run = "-"
latest_saved = "-"
if generation_summary:
latest_run = str(generation_summary.get("run_id", "-"))
latest_saved = str(len(generation_summary.get("saved_item_ids") or []))
elif generation_runs:
latest_run = str(generation_runs[0].id)
return f"""
<div class="compact-strip">
<div class="compact-stat"><span>OpenRouter</span><strong>{"Yes" if key_configured else "No"}</strong></div>
<div class="compact-stat"><span>AI Items</span><strong>{stats.get("total_ai_items", 0)}</strong></div>
<div class="compact-stat"><span>Latest Run</span><strong>{escape(latest_run)}</strong></div>
<div class="compact-stat"><span>Saved</span><strong>{escape(latest_saved)}</strong></div>
</div>
"""
def _ai_generation_summary(generation_summary: dict[str, Any] | None) -> str:
if not generation_summary:
return ""
saved_item_ids = generation_summary.get("saved_item_ids") or []
return f"""
<div class="compact-strip">
<div class="compact-stat"><span>Run ID</span><strong>{generation_summary.get("run_id", "-")}</strong></div>
<div class="compact-stat"><span>Requested</span><strong>{generation_summary.get("requested_count", 0)}</strong></div>
<div class="compact-stat"><span>Generated</span><strong>{generation_summary.get("generated_count", 0)}</strong></div>
<div class="compact-stat"><span>Saved</span><strong>{len(saved_item_ids)}</strong></div>
</div>
"""
def _ai_generate_tab(
basis_items: list[Item],
generation_summary: dict[str, Any] | None,
basis_item_id: str,
target_level: str,
ai_model: str,
generation_count: str,
operator_notes: str,
include_note_for_admin: bool,
include_note_in_prompt: bool,
) -> str:
seed_callout = ""
if not basis_items:
seed_callout = """
<div class="success">No <code>sedang</code> basis items found yet.</div>
<form method="post" action="/admin/ai-playground/seed-demo">
<button type="submit">Seed Demo Basis Item</button>
</form>
"""
return f"""
<section class="tab-panel">
{seed_callout}
{_ai_generation_summary(generation_summary)}
<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)}">
</div>
<div>
<label for="target_level">Target Level</label>
<select id="target_level" name="target_level">
<option value="mudah" {_selected_option("mudah", target_level)}>mudah</option>
<option value="sulit" {_selected_option("sulit", target_level)}>sulit</option>
</select>
</div>
<div>
<label for="generation_count">Generate Count</label>
<input id="generation_count" name="generation_count" type="number" min="1" max="50" value="{escape(generation_count)}">
</div>
<div>
<label for="ai_model">Model</label>
<input id="ai_model" name="ai_model" type="text" value="{escape(ai_model or settings.OPENROUTER_MODEL_LLAMA)}" readonly style="background:#f8fafc;">
</div>
<div class="wide">
<label for="operator_notes">Optional Notes</label>
<textarea id="operator_notes" name="operator_notes" rows="3" placeholder="Optional generation note for this run">{escape(operator_notes)}</textarea>
</div>
</div>
<label class="row"><input type="checkbox" name="include_note_for_admin" {"checked" if include_note_for_admin else ""}> Save note for admin team</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>
<div class="actions">
<button type="submit">Generate Run</button>
<a class="secondary-link" href="/admin/ai-playground?tab=basis">Find Basis Item</a>
</div>
</form>
</section>
"""
def _ai_basis_tab(basis_items: list[Item]) -> str:
rows = []
for item in basis_items:
stem_preview = _truncate(_html_to_text(item.stem), 140)
rows.append(
"<tr>"
f"<td>{item.id}</td>"
f"<td>{escape(str(item.tryout_id))}</td>"
f"<td>{item.slot}</td>"
f"<td>{item.website_id}</td>"
f"<td>{escape(stem_preview)}</td>"
f"<td><a class=\"button-link\" href=\"/admin/ai-playground?tab=generate&basis_item_id={item.id}\">Use</a></td>"
"</tr>"
)
table = (
"<div class=\"table-wrap\"><table><thead><tr><th>Item ID</th><th>Tryout</th><th>Slot</th><th>Website</th><th>Stem</th><th>Action</th></tr></thead><tbody>"
+ ("".join(rows) if rows else "<tr><td colspan=\"6\">No sedang basis items found.</td></tr>")
+ "</tbody></table></div>"
)
return f"""
<section class="tab-panel">
<div class="actions" style="margin-top:0">
<form method="post" action="/admin/ai-playground/seed-demo">
<button type="submit">Seed Demo Basis Item</button>
</form>
</div>
{table}
</section>
"""
def _ai_runs_tab(
generation_runs: list[AIGenerationRun],
generation_summary: dict[str, Any] | None,
) -> str:
rows = []
for run in generation_runs:
rows.append(
"<tr>"
f"<td>{run.id}</td>"
f"<td>{run.basis_item_id}</td>"
f"<td>{escape(run.target_level)}</td>"
f"<td>{run.requested_count}</td>"
f"<td>{escape(_truncate(run.model, 54))}</td>"
f"<td>{escape(run.created_by)}</td>"
f"<td>{escape(str(run.created_at))}</td>"
f"<td><a class=\"secondary-link\" href=\"/admin/ai-playground?tab=review&run_id={run.id}\">Review</a></td>"
"</tr>"
)
table = (
"<div class=\"table-wrap\"><table><thead><tr><th>Run ID</th><th>Basis Item</th><th>Target</th><th>Requested</th><th>Model</th><th>Created By</th><th>Created At</th><th>Action</th></tr></thead><tbody>"
+ ("".join(rows) if rows else "<tr><td colspan=\"8\">No generation runs yet.</td></tr>")
+ "</tbody></table></div>"
)
return f"""
<section class="tab-panel">
{_ai_generation_summary(generation_summary)}
{table}
</section>
"""
def _ai_review_tab(
generated_variants: list[Item],
status_filter: str,
level_filter: str,
run_id_filter: str,
) -> str:
status_options = ['<option value="">All statuses</option>']
for status in AI_VARIANT_STATUSES:
status_options.append(
f'<option value="{status}" {_selected_option(status, status_filter)}>{status}</option>'
)
level_options = ['<option value="">All levels</option>']
for level in AI_VARIANT_LEVELS:
level_options.append(
f'<option value="{level}" {_selected_option(level, level_filter)}>{level}</option>'
)
variant_rows = []
for item in generated_variants:
stem_preview = _truncate(_html_to_text(item.stem), 120)
variant_rows.append(
"<tr>"
f"<td><input type=\"checkbox\" name=\"item_ids\" value=\"{item.id}\"></td>"
f"<td>{item.id}</td>"
f"<td>{item.generation_run_id or '-'}</td>"
f"<td>{item.basis_item_id or '-'}</td>"
f"<td>{escape(item.level)}</td>"
f"<td>{_status_pill(item.variant_status)}</td>"
f"<td>{escape(_truncate(item.ai_model or '-', 42))}</td>"
f"<td>{escape(stem_preview)}</td>"
f"<td>{escape(str(item.created_at))}</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>'
)
return f"""
<section class="tab-panel">
<form class="toolbar" method="get" action="/admin/ai-playground">
<input type="hidden" name="tab" value="review">
<label>Status
<select name="status">{"".join(status_options)}</select>
</label>
<label>Level
<select name="level">{"".join(level_options)}</select>
</label>
<label>Run ID
<input name="run_id" type="number" min="1" value="{escape(run_id_filter)}">
</label>
<button type="submit">Filter</button>
<a class="secondary-link" href="/admin/ai-playground?tab=review">Clear</a>
</form>
<form method="post" action="/admin/ai-playground/review-bulk?tab=review">
<div class="actions" style="margin:16px 0">
<select name="action" style="max-width:260px">
<option value="approved">Approve selected</option>
<option value="rejected">Reject selected</option>
<option value="archived">Archive selected</option>
<option value="stale">Mark stale</option>
<option value="active">Activate selected</option>
</select>
<button type="submit">Apply</button>
</div>
<div class="table-wrap">
<table>
<thead>
<tr><th><input type="checkbox" onclick="document.querySelectorAll('input[name=&quot;item_ids&quot;]').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>
</thead>
<tbody>
{variant_table_rows}
</tbody>
</table>
</div>
</form>
</section>
"""
def _ai_form_body( def _ai_form_body(
key_configured: bool, key_configured: bool,
stats: dict[str, Any], stats: dict[str, Any],
@@ -2152,135 +2463,47 @@ def _ai_form_body(
operator_notes: str = "", operator_notes: str = "",
include_note_for_admin: bool = True, include_note_for_admin: bool = True,
include_note_in_prompt: bool = False, include_note_in_prompt: bool = False,
active_tab: str = "generate",
variant_status_filter: str = "",
variant_level_filter: str = "",
variant_run_id_filter: str = "",
) -> 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 ""
basis_items = basis_items or [] basis_items = basis_items or []
generation_runs = generation_runs or [] generation_runs = generation_runs or []
generated_variants = generated_variants or [] generated_variants = generated_variants or []
basis_rows = [ if active_tab not in {tab for tab, _ in AI_PLAYGROUND_TABS}:
[ active_tab = "generate"
item.id,
item.tryout_id,
item.slot,
item.website_id,
_truncate(item.stem, 90),
]
for item in basis_items
]
basis_table = _table(
["Item ID", "Tryout", "Slot", "Website", "Stem"],
basis_rows,
)
seed_callout = ""
if not basis_items:
seed_callout = """
<div class="success">
No <code>sedang</code> basis items found yet. Seed one demo website, tryout, and basis item to test AI generation immediately.
</div>
<form method="post" action="/admin/ai-playground/seed-demo">
<button type="submit">Seed Demo Basis Item</button>
</form>
"""
summary_html = ""
if generation_summary:
saved_item_ids = generation_summary.get("saved_item_ids") or []
summary_html = f"""
<h3 style="margin-top:24px">Latest Generation Run</h3>
<div class="grid">
<div class="stat">Run ID<strong>{generation_summary.get("run_id", "-")}</strong></div>
<div class="stat">Requested<strong>{generation_summary.get("requested_count", 0)}</strong></div>
<div class="stat">Generated<strong>{generation_summary.get("generated_count", 0)}</strong></div>
<div class="stat">Saved<strong>{len(saved_item_ids)}</strong></div>
</div>
<p class="muted">Each saved output starts as <code>draft</code>. Review per item below to approve, reject, archive, stale, or activate.</p>
"""
run_rows = [ tab_html = {
[ "generate": _ai_generate_tab(
run.id, basis_items,
run.basis_item_id, generation_summary,
run.target_level, basis_item_id,
run.requested_count, target_level,
run.model, ai_model,
run.created_by, generation_count,
str(run.created_at), operator_notes,
] include_note_for_admin,
for run in generation_runs include_note_in_prompt,
] ),
runs_table = _table( "review": _ai_review_tab(
["Run ID", "Basis Item", "Target", "Requested", "Model", "Created By", "Created At"], generated_variants,
run_rows, variant_status_filter,
) variant_level_filter,
variant_run_id_filter,
variant_rows = [] ),
for item in generated_variants: "runs": _ai_runs_tab(generation_runs, generation_summary),
variant_rows.append( "basis": _ai_basis_tab(basis_items),
"<tr>" }[active_tab]
f"<td><input type=\"checkbox\" name=\"item_ids\" value=\"{item.id}\"></td>"
f"<td>{item.id}</td>"
f"<td>{item.generation_run_id or '-'}</td>"
f"<td>{item.basis_item_id or '-'}</td>"
f"<td>{escape(item.level)}</td>"
f"<td>{escape(item.variant_status)}</td>"
f"<td>{escape(item.ai_model or '-')}</td>"
f"<td>{escape(_truncate(item.stem, 100))}</td>"
f"<td>{escape(str(item.created_at))}</td>"
"</tr>"
)
variants_table = (
"<form method=\"post\" action=\"/admin/ai-playground/review-bulk\">"
"<div class=\"actions\" style=\"margin:16px 0\">"
"<select name=\"action\" style=\"max-width:260px\">"
"<option value=\"approved\">Approve selected</option>"
"<option value=\"rejected\">Reject selected</option>"
"<option value=\"archived\">Archive selected</option>"
"<option value=\"stale\">Mark stale</option>"
"<option value=\"active\">Activate selected</option>"
"</select>"
"<button type=\"submit\">Apply</button>"
"</div>"
"<table><thead><tr><th><input type=\"checkbox\" onclick=\"document.querySelectorAll('input[name=&quot;item_ids&quot;]').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></thead><tbody>"
+ ("".join(variant_rows) if variant_rows else "<tr><td colspan=\"9\">No AI-generated variants yet.</td></tr>")
+ "</tbody></table></form>"
)
return f""" return f"""
<p class="muted">OpenRouter key configured: <strong>{"Yes" if key_configured else "No"}</strong></p> {_ai_status_strip(key_configured, stats, generation_runs, generation_summary)}
<p class="muted">Total AI-generated items: <strong>{stats.get("total_ai_items", 0)}</strong></p>
<p class="muted">Hybrid workflow: one run can generate one or many variants; each item remains independently reviewable.</p>
{success_html} {success_html}
{error_html} {error_html}
{seed_callout} {_ai_tab_nav(active_tab)}
<form method="post" action="/admin/ai-playground" autocomplete="off"> {tab_html}
<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="target_level">Target Level</label>
<select id="target_level" name="target_level" style="width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:10px;padding:12px 14px;font-size:15px;">
<option value="mudah" {"selected" if target_level == "mudah" else ""}>mudah</option>
<option value="sulit" {"selected" if target_level == "sulit" else ""}>sulit</option>
</select>
<label for="ai_model">Model</label>
<input id="ai_model" name="ai_model" type="text" value="{escape(settings.OPENROUTER_MODEL_LLAMA)}" readonly style="width:100%;box-sizing:border-box;border:1px solid #cbd5e1;border-radius:10px;padding:12px 14px;font-size:15px;background:#f8fafc;">
<label for="generation_count">Generate Count</label>
<input id="generation_count" name="generation_count" type="number" min="1" max="50" value="{escape(generation_count)}">
<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>
<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>
</form>
<h3 style="margin-top:24px">Available Sedang Basis Items</h3>
<p class="muted">The generator needs a <code>sedang</code> item. Use one of these IDs, or seed demo data if the table is empty.</p>
{basis_table}
{summary_html}
<h3 style="margin-top:24px">Recent Generation Runs</h3>
{runs_table}
<h3 style="margin-top:24px">Generated Variants (Review Queue)</h3>
<p class="muted">Use bulk review actions to move items from draft to approved/active, or reject/archive/stale.</p>
{variants_table}
""" """
@@ -2293,6 +2516,17 @@ async def ai_playground_view(request: Request, db: AsyncSession = Depends(get_db
stats = await get_ai_stats(db) stats = await get_ai_stats(db)
basis_items = await _basis_items_for_playground(db) basis_items = await _basis_items_for_playground(db)
basis_item_id = request.query_params.get("basis_item_id", "") basis_item_id = request.query_params.get("basis_item_id", "")
active_tab = request.query_params.get("tab", "generate")
if active_tab not in {tab for tab, _ in AI_PLAYGROUND_TABS}:
active_tab = "generate"
status_filter = request.query_params.get("status", "")
if status_filter not in AI_VARIANT_STATUSES:
status_filter = ""
level_filter = request.query_params.get("level", "")
if level_filter not in AI_VARIANT_LEVELS:
level_filter = ""
run_id_filter = request.query_params.get("run_id", "").strip()
run_id_filter_int = int(run_id_filter) if run_id_filter.isdigit() else None
generation_runs = await _recent_generation_runs(db) generation_runs = await _recent_generation_runs(db)
selected_basis_item_id: int | None = None selected_basis_item_id: int | None = None
if basis_item_id and str(basis_item_id).isdigit(): if basis_item_id and str(basis_item_id).isdigit():
@@ -2300,6 +2534,9 @@ async def ai_playground_view(request: Request, db: AsyncSession = Depends(get_db
generated_variants = await _recent_generated_variants( generated_variants = await _recent_generated_variants(
db, db,
basis_item_id=selected_basis_item_id, basis_item_id=selected_basis_item_id,
status_filter=status_filter or None,
level_filter=level_filter or None,
run_id_filter=run_id_filter_int,
) )
body = _ai_form_body( body = _ai_form_body(
bool(settings.OPENROUTER_API_KEY), bool(settings.OPENROUTER_API_KEY),
@@ -2308,6 +2545,10 @@ async def ai_playground_view(request: Request, db: AsyncSession = Depends(get_db
generation_runs=generation_runs, generation_runs=generation_runs,
generated_variants=generated_variants, generated_variants=generated_variants,
basis_item_id=str(basis_item_id or ""), basis_item_id=str(basis_item_id or ""),
active_tab=active_tab,
variant_status_filter=status_filter,
variant_level_filter=level_filter,
variant_run_id_filter=run_id_filter if run_id_filter_int is not None else "",
) )
return _render_admin_page(request, "AI Playground", "AI Playground", body) return _render_admin_page(request, "AI Playground", "AI Playground", body)
@@ -2654,6 +2895,7 @@ async def ai_playground_review_bulk(
basis_items=basis_items, basis_items=basis_items,
generation_runs=generation_runs, generation_runs=generation_runs,
generated_variants=generated_variants, generated_variants=generated_variants,
active_tab="review",
) )
return _render_admin_page(request, "AI Playground", "AI Playground", body) return _render_admin_page(request, "AI Playground", "AI Playground", body)
@@ -2665,6 +2907,7 @@ async def ai_playground_review_bulk(
basis_items=basis_items, basis_items=basis_items,
generation_runs=generation_runs, generation_runs=generation_runs,
generated_variants=generated_variants, generated_variants=generated_variants,
active_tab="review",
) )
return _render_admin_page(request, "AI Playground", "AI Playground", body) return _render_admin_page(request, "AI Playground", "AI Playground", body)
@@ -2680,6 +2923,7 @@ async def ai_playground_review_bulk(
basis_items=basis_items, basis_items=basis_items,
generation_runs=generation_runs, generation_runs=generation_runs,
generated_variants=generated_variants, generated_variants=generated_variants,
active_tab="review",
) )
return _render_admin_page(request, "AI Playground", "AI Playground", body) return _render_admin_page(request, "AI Playground", "AI Playground", body)
@@ -2702,6 +2946,7 @@ async def ai_playground_review_bulk(
basis_items=updated_basis_items, basis_items=updated_basis_items,
generation_runs=updated_runs, generation_runs=updated_runs,
generated_variants=updated_variants, generated_variants=updated_variants,
active_tab="review",
) )
return _render_admin_page(request, "AI Playground", "AI Playground", body) return _render_admin_page(request, "AI Playground", "AI Playground", body)