Add qualifier prompt test page

This commit is contained in:
Your Name
2026-06-15 22:56:40 +05:00
parent 2e4cbee4a5
commit d306ca42a1
3 changed files with 298 additions and 1 deletions
+10 -1
View File
@@ -117,6 +117,14 @@
.prompt-hint { max-width:720px; border:1px solid var(--line); border-radius:8px; padding:10px 12px; background:#f8fafc; display:grid; gap:8px; color:var(--text); }
.prompt-hint summary { cursor:pointer; font-weight:700; }
.prompt-hint pre, .prompt-box { white-space:pre-wrap; overflow:auto; padding:10px; border:1px solid var(--line); border-radius:8px; background:#f8fafc; color:var(--text); }
.prompt-test-layout { display:grid; grid-template-columns:minmax(0, 1fr) 420px; gap:16px; align-items:start; }
.prompt-test-main { margin-bottom:0; }
.prompt-test-side { display:grid; gap:16px; position:sticky; top:74px; max-height:calc(100vh - 92px); overflow:auto; }
.prompt-test-model { min-height:38px; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.prompt-test-post { margin:12px 0 14px; border:1px solid var(--line); border-radius:8px; padding:12px; background:var(--panel-soft); }
.prompt-test-post-head { display:flex; justify-content:space-between; gap:12px; align-items:center; margin-bottom:4px; }
.prompt-test-post pre { max-height:260px; overflow:auto; margin:10px 0 0; white-space:pre-wrap; font:inherit; color:var(--text); }
.prompt-test-help p { margin:0; }
.hint-grid { display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:10px; }
.hint-label { color:var(--muted); font-size:12px; font-weight:700; text-transform:uppercase; margin-bottom:3px; }
.small-help { font-size:12px; margin-top:4px; }
@@ -208,7 +216,7 @@
.regen-form { margin-top:16px; border-top:1px solid var(--line); padding-top:14px; }
.compact-kv { margin-top:12px; grid-template-columns:90px minmax(0,1fr); }
.empty-state { text-align:center; color:var(--muted); padding:34px; }
@media (max-width: 900px) { header { padding:8px 12px; align-items:flex-start; } .filter-grid, .sources-filter, .source-add-grid, .hint-grid, .advanced-grid, .tool-row, .tool-row.sort-row, .editor-filter, .page-head, .compare-grid, .dialog-grid, .catalog-layout, .media-upload-form, .category-create { grid-template-columns:1fr; } .page-head { display:grid; } .form-grid { grid-template-columns:1fr; } .detail-grid { grid-template-columns:1fr; } .editor-card { grid-template-columns:1fr; } .editor-actions { grid-template-columns:repeat(3, minmax(0,1fr)); } .context-pane { border-right:0; border-bottom:1px solid var(--line); max-height:none; } .edit-pane { max-height:none; } .side-panel, .filter-sidebar { position:static; max-height:none; } .list-toolbar { display:grid; align-items:start; } .compact-controls { justify-content:start; } main { padding:14px; } }
@media (max-width: 900px) { header { padding:8px 12px; align-items:flex-start; } .filter-grid, .sources-filter, .source-add-grid, .hint-grid, .advanced-grid, .tool-row, .tool-row.sort-row, .editor-filter, .page-head, .compare-grid, .dialog-grid, .catalog-layout, .media-upload-form, .category-create, .prompt-test-layout { grid-template-columns:1fr; } .page-head { display:grid; } .form-grid { grid-template-columns:1fr; } .detail-grid { grid-template-columns:1fr; } .editor-card { grid-template-columns:1fr; } .editor-actions { grid-template-columns:repeat(3, minmax(0,1fr)); } .context-pane { border-right:0; border-bottom:1px solid var(--line); max-height:none; } .edit-pane { max-height:none; } .side-panel, .filter-sidebar, .prompt-test-side { position:static; max-height:none; } .list-toolbar { display:grid; align-items:start; } .compact-controls { justify-content:start; } main { padding:14px; } }
</style>
</head>
<body>
@@ -218,6 +226,7 @@
<a href="/sources">Источники</a>
<a href="/raw">Raw</a>
<a href="/editor">Редакторская</a>
<a href="/prompt-test">Тест промптов</a>
<a href="/workers">Воркеры</a>
<a href="/users">Пользователи</a>
</nav>
@@ -0,0 +1,106 @@
{% extends "base.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Тест промптов</h1>
<p class="muted">Песочница для свободной части <span class="mono">ai_qualifier_prompt</span>. Посты и оценки в БД не меняются.</p>
</div>
<a class="btn" href="/workers">Настройки AI</a>
</div>
<details class="panel prompt-test-help" open>
<summary><strong>Как пользоваться</strong></summary>
<div class="hint-grid">
<div>
<div class="hint-label">Что тестируем</div>
<p>Отправляется только текст из поля ниже как system prompt. <span class="mono">ai_qualifier_contract</span> специально не добавляется, чтобы можно было быстро проверять идеи, тон и критерии.</p>
</div>
<div>
<div class="hint-label">Что уйдёт в модель</div>
<p>Один выбранный raw-пост в том же формате, что использует квалификатор: <span class="mono">id</span>, источник, ссылка, количество медиа, типы медиа и обрезанный текст.</p>
</div>
<div>
<div class="hint-label">Что безопасно менять</div>
<p>Роль, критерии, примеры хороших/плохих постов, шкалу оценки и формулировки причин. Для рабочего воркера JSON-контракт всё равно живёт отдельно.</p>
</div>
<div>
<div class="hint-label">Что не происходит</div>
<p>Статус поста не меняется, batch не создаётся, оценка не сохраняется. Тест только тратит токены текущей модели квалификатора.</p>
</div>
</div>
</details>
<form class="prompt-test-layout" method="post" action="/prompt-test">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<section class="panel prompt-test-main">
<div class="form-grid">
<label>
<span>Пост для теста</span>
<select name="post_id" data-post-select>
{% for post in posts %}
<option value="{{ post.id }}" {% if selected and post.id == selected.id %}selected{% endif %}>{{ post.label }}</option>
{% endfor %}
</select>
</label>
<div>
<span class="muted">Модель</span>
<div class="prompt-test-model"><span class="pill">{{ provider }}</span><span class="mono">{{ model or "не выбрана" }}</span></div>
</div>
</div>
{% if selected %}
<div class="prompt-test-post">
<div class="prompt-test-post-head">
<strong>#{{ selected.id }} · {{ selected.source_name }}</strong>
<a href="{{ selected.original_url }}" target="_blank" rel="noopener">Оригинал</a>
</div>
<div class="muted">{{ selected.posted_at_fmt or selected.created_at_fmt }} · {{ selected.media_count }} медиа · {{ selected.media_types or [] }}</div>
<pre>{{ selected.raw_text }}</pre>
</div>
{% endif %}
<label>
<span>ai_qualifier_prompt для теста</span>
<textarea name="prompt" rows="18" spellcheck="false">{{ prompt }}</textarea>
</label>
<div class="dialog-actions">
<button class="btn primary" type="submit">Отправить тест</button>
</div>
</section>
<aside class="prompt-test-side">
<section class="panel">
<h2>Payload</h2>
<p class="muted">Именно это отправится user-сообщением. Текст обрезан по настройке <span class="mono">ai_qualifier_max_text_chars={{ max_text_chars }}</span>.</p>
<pre class="json-box">{{ payload_json }}</pre>
</section>
<section class="panel">
<h2>Ответ</h2>
{% if error %}
<div class="pill bad">{{ error }}</div>
{% elif result %}
<dl class="kv compact-kv">
<dt>Модель</dt><dd class="mono">{{ result.model }}</dd>
<dt>Токены</dt><dd>{{ result.usage.total_tokens or "—" }} total, {{ result.usage.prompt_tokens or "—" }} prompt, {{ result.usage.completion_tokens or "—" }} completion</dd>
<dt>Стоимость</dt><dd>{{ result.usage.estimated_cost_usd or "—" }}</dd>
</dl>
{% if result.parsed_json %}
<h3>JSON разобран</h3>
<pre class="json-box">{{ result.parsed_json }}</pre>
{% endif %}
<h3>Сырой ответ</h3>
<pre class="json-box">{{ result.content }}</pre>
{% else %}
<p class="muted">После отправки здесь появится ответ модели.</p>
{% endif %}
</section>
</aside>
</form>
<script>
document.querySelector("[data-post-select]")?.addEventListener("change", (event) => {
const id = event.target.value;
if (id) window.location.href = `/prompt-test?post_id=${encodeURIComponent(id)}`;
});
</script>
{% endblock %}