Files
new_vk_parser/src/vk_parser_app/templates/editor.html
T
2026-07-18 21:29:05 +05:00

360 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Редакторская</h1>
<div class="muted">Посты после AI-рерайта: быстрая проверка, правка и принятие.</div>
</div>
<div class="status-tabs">
{% for st in status_options %}
<a class="tab {% if st.value in editorial_statuses %}active{% endif %}" href="/editor?editorial_status={{ st.value }}">
{{ st.label }} <span>{{ counts.get(st.value, 0) }}</span>
</a>
{% endfor %}
</div>
</div>
<div class="catalog-layout editor-catalog">
<section class="catalog-main">
<div class="panel list-toolbar">
<div class="muted">Всего: {{ pagination.total }}</div>
<form class="row compact-controls" method="get" action="/editor">
{% for item in preserved_query %}
{% if item.key != "per_page" and item.key != "sort" %}
<input type="hidden" name="{{ item.key }}" value="{{ item.value }}">
{% endif %}
{% endfor %}
<label><span>Сортировка</span>
<select name="sort" data-autosubmit>
<option value="rewritten_desc" {% if sort == "rewritten_desc" %}selected{% endif %}>рерайт: новые</option>
<option value="rewritten_asc" {% if sort == "rewritten_asc" %}selected{% endif %}>рерайт: старые</option>
<option value="posted_desc" {% if sort == "posted_desc" %}selected{% endif %}>VK: новые</option>
<option value="posted_asc" {% if sort == "posted_asc" %}selected{% endif %}>VK: старые</option>
<option value="score_desc" {% if sort == "score_desc" %}selected{% endif %}>оценка: выше</option>
<option value="score_asc" {% if sort == "score_asc" %}selected{% endif %}>оценка: ниже</option>
</select>
</label>
<label><span>На странице</span>
<select name="per_page" data-autosubmit>
{% for n in [10,25,50,100] %}
<option value="{{ n }}" {% if pagination.per_page == n %}selected{% endif %}>{{ n }}</option>
{% endfor %}
</select>
</label>
</form>
</div>
<div class="editor-list">
{% for p in posts %}
<article class="editor-card">
<div class="editor-media">
<div class="media-strip">
{% for m in p.media_items %}
{% if m.type == "photo" and m.url %}
<button class="media-thumb" type="button" data-gallery-src="{{ m.url }}" data-gallery-title="#{{ p.id }} · фото {{ loop.index }}" title="{{ m.status }}">
<img src="{{ m.url }}" alt="photo">
</button>
{% elif m.url %}
<a class="media-thumb media-video" href="{{ m.url }}" target="_blank" title="{{ m.error or m.status }}"><span></span></a>
{% endif %}
{% endfor %}
</div>
<div class="muted small-help">{{ p.media_count or 0 }} медиа</div>
</div>
<div class="editor-main">
<div class="editor-meta">
<a href="{{ p.original_url }}" target="_blank">{{ p.source_name }}</a>
<span>#{{ p.review_source_tag or p.source_tag or "source" }}</span>
<span>{{ p.posted_at_fmt or p.created_at_fmt }}</span>
</div>
<div class="editor-title-row">
<span class="pill {% if p.editorial_status in ['accepted', 'published'] %}ok{% elif p.editorial_status in ['rejected', 'publish_failed'] %}bad{% elif p.editorial_status == 'regenerating' %}warn{% endif %}" title="{{ p.publication_error or p.tg_publication_url or '' }}">
{{ p.editorial_status_label }}
</span>
<span class="pill">{{ p.review_category or "без категории" }}</span>
<span class="muted">AI {{ p.qualification_score or "?" }}/10</span>
</div>
<div class="publication-badges">
{% for pub in p.publication_platforms %}
{% if pub.url %}
<a class="pill {{ pub.class }}" href="{{ pub.url }}" target="_blank" title="{{ pub.error or pub.status_label }}">{{ pub.label }} · {{ pub.status_label }}</a>
{% else %}
<span class="pill {{ pub.class }}" title="{{ pub.error or pub.status_label }}">{{ pub.label }} · {{ pub.status_label }}</span>
{% endif %}
{% endfor %}
</div>
<div class="editor-text">{{ p.publication_preview_text }}</div>
<details class="source-compare">
<summary>Исходник и AI-заметки</summary>
<div class="compare-grid">
<div>
<div class="hint-label">Оригинал</div>
<div class="raw-full-text">{{ p.raw_text }}</div>
</div>
<div>
<div class="hint-label">Заметки</div>
<div>{{ p.rewrite_notes or p.qualification_reason or "—" }}</div>
</div>
</div>
</details>
</div>
<div class="editor-actions">
<form method="post" action="/editor/{{ p.id }}/accept">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn primary" type="submit">Принять</button>
</form>
<button class="btn" type="button" data-open-editor="edit-{{ p.id }}">Редактировать</button>
<form method="post" action="/editor/{{ p.id }}/reject" onsubmit="return confirm('Отклонить пост #{{ p.id }}?');">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<button class="btn danger" type="submit">Отклонить</button>
</form>
</div>
</article>
<dialog class="editor-dialog" id="edit-{{ p.id }}">
<div class="dialog-head">
<div>
<h2>Пост #{{ p.id }}</h2>
<div class="muted"><a href="{{ p.original_url }}" target="_blank">{{ p.source_name }}</a> · {{ p.posted_at_fmt or p.created_at_fmt }}</div>
</div>
<button class="icon-btn" type="button" data-close-dialog="edit-{{ p.id }}">×</button>
</div>
<form method="post" action="/editor/{{ p.id }}/save" class="edit-form dialog-grid" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ user.csrf_token }}">
<aside class="context-pane">
<div class="media-strip detail-media">
{% for m in p.media_items %}
<div class="editable-media" data-media-id="{{ m.id }}">
{% if m.type == "photo" and m.url %}
<button class="media-thumb detail-thumb" type="button" data-gallery-src="{{ m.url }}" data-gallery-title="#{{ p.id }} · фото {{ loop.index }}" title="{{ m.status }}">
<img src="{{ m.url }}" alt="photo">
</button>
{% elif m.url %}
<a class="media-thumb detail-thumb media-video" href="{{ m.url }}" target="_blank"><span></span></a>
{% endif %}
<button class="media-remove" type="button" data-mark-media-delete="{{ m.id }}" title="Убрать после сохранения">×</button>
</div>
{% endfor %}
</div>
<label class="media-upload-form"><span>Добавить медиа после сохранения</span><input type="file" name="media_files" accept="image/*,video/*,.pdf,.doc,.docx" multiple></label>
<div class="pending-media-list muted small-help" data-pending-media-list>Выбранные файлы появятся после сохранения.</div>
<details open>
<summary>Исходный пост</summary>
<div class="raw-full-text">{{ p.raw_text }}</div>
</details>
<details>
<summary>AI-рерайт</summary>
<div class="raw-full-text">{{ p.rewritten_text }}</div>
</details>
<dl class="kv compact-kv">
<dt>Оценка</dt><dd>{{ p.qualification_score or "—" }}/10</dd>
<dt>Категория</dt><dd>{{ p.rewrite_category or "—" }}</dd>
<dt>Модель</dt><dd>{{ p.rewrite_model or "—" }}</dd>
</dl>
</aside>
<section class="edit-pane">
<label><span>Текст публикации</span>
<textarea name="final_text" rows="15">{{ p.review_text }}</textarea>
</label>
<div class="form-grid">
<label><span>Категория</span>
<select name="final_category">
{% for category in categories %}
<option value="{{ category.name }}" data-tag="{{ category.tag }}" {% if p.review_category == category.name %}selected{% endif %}>{{ category.name }}</option>
{% endfor %}
{% set category_names = categories | map(attribute="name") | list %}
{% if p.review_category and p.review_category not in category_names %}
<option value="{{ p.review_category }}" data-tag="{{ p.review_category_tag }}" selected>{{ p.review_category }} · старая категория</option>
{% endif %}
</select>
</label>
<label><span>Тэг источника</span>
<input name="final_source_tag" value="{{ p.review_source_tag or p.source_tag or '' }}">
</label>
</div>
<label><span>Предпросмотр публикации</span>
<div class="publication-preview" data-publication-preview>{{ p.publication_preview_text }}</div>
</label>
<div class="hashtag-preview" data-hashtag-preview>#{{ p.review_category_tag or p.review_category or "category" }} #{{ p.review_source_tag or p.source_tag or "source" }}</div>
<label><span>Заметка редактора</span>
<textarea name="editor_notes" rows="3">{{ p.editor_notes or "" }}</textarea>
</label>
<div class="dialog-actions">
<button class="btn" type="submit" name="action" value="save">Сохранить правки</button>
<button class="btn primary" type="submit" name="action" value="accept">Принять к публикации</button>
</div>
</section>
</form>
</dialog>
{% else %}
<div class="empty-state panel">В этой очереди пока нет постов.</div>
{% endfor %}
</div>
{% include "pagination.html" %}
</section>
<aside class="filter-sidebar panel">
<form method="get" action="/editor" data-filter-form>
<input type="hidden" name="sort" value="{{ sort }}">
<div class="filter-head">
<h2>Фильтры</h2>
<a href="/editor">Сбросить</a>
</div>
<div class="filter-section">
<div class="filter-title">Оценка AI</div>
<div class="range-row">
<input name="score_min" inputmode="numeric" placeholder="От" value="{{ score_min }}">
<input name="score_max" inputmode="numeric" placeholder="До" value="{{ score_max }}">
</div>
</div>
<div class="filter-section">
<div class="filter-title">Дата поста</div>
<div class="range-row">
<input type="date" name="date_from" value="{{ date_from }}">
<input type="date" name="date_to" value="{{ date_to }}">
</div>
</div>
<div class="filter-section">
<div class="filter-title">Источник</div>
<input class="facet-search" type="search" placeholder="Найти источник" data-facet-search="editor-sources">
<div class="facet-list" data-facet-list="editor-sources">
{% for item in facets.sources %}
<label class="facet-option"><input type="checkbox" name="source_id" value="{{ item.id }}" {% if item.id in source_ids %}checked{% endif %}> <span>{{ item.name }}</span><small>{{ item.count }}</small></label>
{% endfor %}
</div>
</div>
<div class="filter-section">
<div class="filter-title">Категория</div>
<input class="facet-search" type="search" placeholder="Найти категорию" data-facet-search="editor-categories">
<div class="facet-list" data-facet-list="editor-categories">
{% for item in facets.categories %}
<label class="facet-option"><input type="checkbox" name="category" value="{{ item.value }}" {% if item.value in categories_selected %}checked{% endif %}> <span>{{ item.value }}</span><small>{{ item.count }}</small></label>
{% endfor %}
</div>
</div>
<div class="filter-section">
<div class="filter-title">Статус редакции</div>
<div class="facet-list compact">
{% for st in status_options %}
<label class="facet-option"><input type="checkbox" name="editorial_status" value="{{ st.value }}" {% if st.value in editorial_statuses %}checked{% endif %}> <span>{{ st.label }}</span><small>{{ counts.get(st.value, 0) }}</small></label>
{% endfor %}
</div>
</div>
<button class="btn primary filter-apply" type="submit">Показать</button>
</form>
</aside>
</div>
<dialog class="gallery-modal" id="galleryModal" aria-hidden="true">
<button class="gallery-close" type="button" aria-label="Закрыть">×</button>
<button class="gallery-nav gallery-prev" type="button" aria-label="Назад"></button>
<img id="galleryImage" src="" alt="">
<button class="gallery-nav gallery-next" type="button" aria-label="Вперёд"></button>
<div class="gallery-title" id="galleryTitle"></div>
</dialog>
<script>
(() => {
document.querySelectorAll("[data-open-editor]").forEach((button) => {
button.addEventListener("click", () => document.getElementById(button.dataset.openEditor)?.showModal());
});
document.querySelectorAll("[data-close-dialog]").forEach((button) => {
button.addEventListener("click", () => document.getElementById(button.dataset.closeDialog)?.close());
});
document.querySelectorAll(".edit-form").forEach((form) => {
const textArea = form.querySelector('[name="final_text"]');
const category = form.querySelector('[name="final_category"]');
const source = form.querySelector('[name="final_source_tag"]');
const hashtagPreview = form.querySelector("[data-hashtag-preview]");
const publicationPreview = form.querySelector("[data-publication-preview]");
const normalize = (value, fallback) => {
const tag = String(value || "").trim().replace(/^#+/, "").toLowerCase()
.replace(/\s+/g, "_")
.replace(/[^\wа-яё_]+/gi, "_")
.replace(/_+/g, "_")
.replace(/^_+|_+$/g, "");
return tag || fallback;
};
const update = () => {
const option = category?.selectedOptions?.[0];
const tags = `#${normalize(option?.dataset?.tag || category?.value, "category")} #${normalize(source?.value, "source")}`;
if (hashtagPreview) hashtagPreview.textContent = tags;
if (publicationPreview) {
const cleanText = String(textArea?.value || "").trim();
publicationPreview.textContent = cleanText ? `${cleanText}\n\n${tags}` : tags;
}
};
textArea?.addEventListener("input", update);
category?.addEventListener("change", update);
source?.addEventListener("input", update);
update();
});
const items = Array.from(document.querySelectorAll("[data-gallery-src]"));
const modal = document.getElementById("galleryModal");
const image = document.getElementById("galleryImage");
const title = document.getElementById("galleryTitle");
let index = 0;
function show(nextIndex) {
if (!items.length) return;
index = (nextIndex + items.length) % items.length;
const item = items[index];
image.src = item.dataset.gallerySrc;
title.textContent = item.dataset.galleryTitle || "";
modal.classList.add("open");
modal.setAttribute("aria-hidden", "false");
if (typeof modal.showModal === "function" && !modal.open) modal.showModal();
}
function close() {
modal.classList.remove("open");
modal.setAttribute("aria-hidden", "true");
image.src = "";
if (typeof modal.close === "function" && modal.open) modal.close();
}
items.forEach((item, i) => item.addEventListener("click", () => show(i)));
modal.querySelector(".gallery-close").addEventListener("click", close);
modal.querySelector(".gallery-prev").addEventListener("click", () => show(index - 1));
modal.querySelector(".gallery-next").addEventListener("click", () => show(index + 1));
modal.addEventListener("click", (event) => { if (event.target === modal) close(); });
document.querySelectorAll("[data-autosubmit]").forEach((control) => {
control.addEventListener("change", () => control.form?.submit());
});
document.querySelectorAll("[data-facet-search]").forEach((input) => {
input.addEventListener("input", () => {
const list = document.querySelector(`[data-facet-list="${input.dataset.facetSearch}"]`);
const q = input.value.trim().toLowerCase();
list?.querySelectorAll(".facet-option").forEach((option) => {
option.hidden = q && !option.textContent.toLowerCase().includes(q);
});
});
});
document.querySelectorAll("[data-mark-media-delete]").forEach((button) => {
button.addEventListener("click", () => {
const form = button.closest("form");
const media = button.closest("[data-media-id]");
const mediaId = button.dataset.markMediaDelete;
if (!form || !mediaId || form.querySelector(`input[name="delete_media_ids"][value="${mediaId}"]`)) return;
const hidden = document.createElement("input");
hidden.type = "hidden";
hidden.name = "delete_media_ids";
hidden.value = mediaId;
form.appendChild(hidden);
media?.classList.add("marked-delete");
});
});
document.querySelectorAll('input[type="file"][name="media_files"]').forEach((input) => {
input.addEventListener("change", () => {
const list = input.closest(".context-pane")?.querySelector("[data-pending-media-list]");
if (!list) return;
const names = Array.from(input.files || []).map((file) => file.name);
list.textContent = names.length ? `Будет добавлено: ${names.join(", ")}` : "Выбранные файлы появятся после сохранения.";
});
});
})();
</script>
{% endblock %}