Add profile generate refine via manager hint and auto-preview.

When Preview leaves fields empty, managers can send a hint plus the current profile so DeepSeek revises rules; generate now returns preview and empty_fields for the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-16 22:25:27 +03:00
co-authored by Cursor
parent 71f8cf169e
commit 52185aeaa8
4 changed files with 203 additions and 35 deletions
@@ -20,10 +20,12 @@ const success = ref("");
const submitting = ref(false);
const samplePost = ref("");
const generateHint = ref("");
const profileName = ref("");
const targetFields = ref<TargetField[]>([]);
const profileJson = ref("");
const previewFields = ref<Record<string, string> | null>(null);
const emptyFields = ref<string[]>([]);
const generating = ref(false);
const previewing = ref(false);
const loadingFields = ref(true);
@@ -73,10 +75,32 @@ async function handleGenerate() {
error.value = "";
success.value = "";
previewFields.value = null;
emptyFields.value = [];
try {
const data = await generateParserProfile(samplePost.value);
let current: HeuristicProfile | null = null;
if (profileJson.value.trim()) {
current = syncProfileFromJson();
if (!current) return;
}
const data = await generateParserProfile({
sample_post: samplePost.value,
hint: generateHint.value.trim() || undefined,
current_profile: current,
});
profileJson.value = JSON.stringify(data.profile, null, 2);
success.value = "Профиль сгенерирован. Preview и runtime — только по правилам, без LLM.";
previewFields.value = data.preview;
emptyFields.value = data.empty_fields || [];
if (emptyFields.value.length) {
success.value =
`Профиль обновлён, но пустые поля: ${emptyFields.value.join(", ")}. ` +
"Допишите подсказку ниже и снова нажмите Generate.";
if (!generateHint.value.trim() && emptyFields.value.includes("description")) {
generateHint.value =
"description должен содержать многострочный текст тела поста после заголовка и до координат (47.…, 36.…), без строки заголовка и без хвоста Источник/Геопривязка/#теги";
}
} else {
success.value = "Профиль сгенерирован. Preview и runtime — только по правилам, без LLM.";
}
} catch (err) {
error.value = err instanceof Error ? err.message : "Ошибка генерации";
} finally {
@@ -99,6 +123,9 @@ async function handlePreview() {
try {
const data = await previewParserProfile(samplePost.value, current);
previewFields.value = data.fields;
emptyFields.value = Object.entries(data.fields)
.filter(([, v]) => !v)
.map(([k]) => k);
} catch (err) {
error.value = err instanceof Error ? err.message : "Ошибка preview";
} finally {
@@ -110,9 +137,11 @@ function resetEditor() {
editingId.value = null;
profileName.value = "";
samplePost.value = "";
generateHint.value = "";
profileJson.value = "";
profileStatus.value = "draft";
previewFields.value = null;
emptyFields.value = [];
success.value = "";
}
@@ -120,11 +149,13 @@ function openEdit(p: ParserProfile) {
editingId.value = p.id;
profileName.value = p.name;
samplePost.value = p.sample_post || "";
generateHint.value = "";
profileJson.value = p.heuristic_profile
? JSON.stringify(p.heuristic_profile, null, 2)
: "";
profileStatus.value = p.status === "ready" ? "ready" : "draft";
previewFields.value = null;
emptyFields.value = [];
success.value = "";
error.value = "";
window.scrollTo({ top: 0, behavior: "smooth" });
@@ -197,8 +228,9 @@ onMounted(() => {
<div class="page">
<h2 class="page-heading">Профили парсера</h2>
<p class="intro">
Статичный эвристический профиль: Generate (DeepSeek один раз) → Preview → Сохранить.
Дальше профиль подключается к каналу на странице
Generate строит статичные правила по образцу поста. Если Preview пустой по полю —
напишите подсказку и снова Generate (агент правит текущий JSON). Можно править JSON
вручную. Дальше профиль подключается к каналу на
<router-link to="/parsers">Парсеры</router-link>.
</p>
@@ -231,6 +263,16 @@ onMounted(() => {
placeholder="15.03.2024 Населённый пункт&#10;&#10;Описание…&#10;&#10;48.123456, 37.654321"
/>
</label>
<label>
Подсказка агенту
<span class="muted"> (если Generate ошибся — опишите, что исправить, и нажмите Generate снова)</span>
<textarea
v-model="generateHint"
class="hint-area"
rows="3"
placeholder="например: description — абзацы между заголовком и координатами, без #хештегов"
/>
</label>
<div class="form-actions">
<button
type="button"
@@ -238,7 +280,13 @@ onMounted(() => {
:disabled="generating || !samplePost.trim()"
@click="handleGenerate"
>
{{ generating ? "Генерация..." : "Generate" }}
{{
generating
? "Генерация..."
: generateHint.trim() || profileJson.trim()
? "Generate / Refine"
: "Generate"
}}
</button>
<button
type="button"
@@ -297,6 +345,9 @@ onMounted(() => {
<section v-if="previewFields" class="card">
<h3>Preview строки</h3>
<p v-if="emptyFields.length" class="form-warn">
Пустые поля: <code>{{ emptyFields.join(", ") }}</code> — уточните подсказку и Generate / Refine.
</p>
<div class="table-wrap">
<table class="admin-table">
<thead>
@@ -306,7 +357,12 @@ onMounted(() => {
</thead>
<tbody>
<tr>
<td v-for="(val, key) in previewFields" :key="key" class="preview-cell">
<td
v-for="(val, key) in previewFields"
:key="key"
class="preview-cell"
:class="{ empty: !val }"
>
{{ val || "—" }}
</td>
</tr>
@@ -376,7 +432,8 @@ onMounted(() => {
}
.sample-area,
.profile-area {
.profile-area,
.hint-area {
width: 100%;
font: inherit;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
@@ -389,6 +446,10 @@ onMounted(() => {
margin-top: 0.35rem;
}
.hint-area {
font-family: inherit;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -408,12 +469,23 @@ onMounted(() => {
font-size: 0.8rem;
}
.preview-cell.empty {
background: #fff8e1;
color: #b26a00;
}
.form-success {
color: #2e7d32;
font-size: 0.875rem;
margin-top: 0.5rem;
}
.form-warn {
color: #b26a00;
font-size: 0.875rem;
margin: 0 0 0.75rem;
}
.actions {
display: flex;
gap: 0.35rem;