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:
@@ -64,10 +64,29 @@ export function fetchTargetFields(): Promise<{ fields: TargetField[] }> {
|
||||
return request<{ fields: TargetField[] }>("/parser-profiles/target-fields", undefined, ADMIN_BASE);
|
||||
}
|
||||
|
||||
export function generateParserProfile(sample_post: string): Promise<{ profile: HeuristicProfile }> {
|
||||
return request<{ profile: HeuristicProfile }>(
|
||||
export function generateParserProfile(payload: {
|
||||
sample_post: string;
|
||||
hint?: string;
|
||||
current_profile?: HeuristicProfile | Record<string, unknown> | null;
|
||||
}): Promise<{
|
||||
profile: HeuristicProfile;
|
||||
preview: Record<string, string>;
|
||||
empty_fields: string[];
|
||||
}> {
|
||||
return request<{
|
||||
profile: HeuristicProfile;
|
||||
preview: Record<string, string>;
|
||||
empty_fields: string[];
|
||||
}>(
|
||||
"/parser-profiles/generate",
|
||||
{ method: "POST", body: JSON.stringify({ sample_post }) },
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
sample_post: payload.sample_post,
|
||||
hint: payload.hint || undefined,
|
||||
current_profile: payload.current_profile || undefined,
|
||||
}),
|
||||
},
|
||||
ADMIN_BASE,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 Населённый пункт Описание… 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;
|
||||
|
||||
Reference in New Issue
Block a user