Redesign: add 1-3★ auto-reply pools, filter modes, new settings UI

- app.py: add pool keys for stars 1-3, AUTO_REPLY_STARS_KEY/FILTER_KEY,
  _load_enabled_stars(), _load_filter_mode(), updated _has_review_content()
  with filter_mode param, new /auto-reply-settings POST route, index route
  passes enabled_stars/filter_mode/pools 1-5 to template
- templates/index.html: remove reviews list & controls section, replace pool
  card with full settings card (star toggles, filter mode radios, dynamic
  per-star pool columns), update JS for all 5 stars
- templates/login.html: fix ratings feature text (1-5★), update price to
  7 ₽/день with corrected description
- static/styles.css: login-promo fixed 300px width, logo 140px, add
  settings UI styles (star-toggles, filter-options, pool-columns, star-chips 1-3)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 18:19:38 +03:00
parent 60ea2757bc
commit e5050a7c83
4 changed files with 233 additions and 186 deletions
+81 -176
View File
@@ -40,29 +40,6 @@
<div class="alert alert-success">{{ success_message }}</div>
{% endif %}
<!-- Фильтры и кнопки -->
<div class="controls">
<div class="controls-card">
<form method="get" action="/">
<div class="star-filter">
<span class="star-filter-label">Оценки</span>
{% for star in [5,4,3,2,1] %}
<span class="star-pill" data-star="{{ star }}">
<input type="checkbox" id="star-{{ star }}" name="stars" value="{{ star }}" {% if star in selected_stars %}checked{% endif %}>
<label for="star-{{ star }}">{{ star }}★</label>
</span>
{% endfor %}
</div>
<div class="control-buttons">
<button type="submit" name="action" value="all">Выгрузить отзывы</button>
<button type="submit" name="action" value="unanswered">Только неотвеченные</button>
<button type="submit" name="action" value="unanswered" class="secondary" formaction="/?stars=5&stars=4">Новые 5★ и 4★</button>
<button type="submit" name="action" value="clear" class="secondary">Очистить список</button>
</div>
</form>
</div>
</div>
<!-- Автоответ -->
<div class="card auto-reply">
<div class="auto-reply__info">
@@ -92,33 +69,66 @@
</form>
</div>
<!-- Пулы автоответов -->
<!-- Настройки автоответа -->
<div class="card">
<div class="section-header">
<h2>Пулы автоответов</h2>
<h2>Настройки автоответа</h2>
</div>
<p style="margin-bottom:16px;font-size:13px">Для каждого нового отзыва текст выбирается случайно из пула.</p>
<form method="post" action="{{ url_for('auto_reply_pools') }}" id="pools-form">
<textarea name="pool_5" id="pool-5-hidden" hidden>{{ reply_pool_5_text }}</textarea>
<textarea name="pool_4" id="pool-4-hidden" hidden>{{ reply_pool_4_text }}</textarea>
<div class="pool-grid">
<div class="pool-block">
<div class="pool-label" style="margin-bottom:10px">
Ответы для <span class="star-chip star-chip--5">5★</span>
</div>
<div class="pool-items" id="pool-items-5"></div>
<button type="button" class="btn-add-item" onclick="addPoolItem('5')">+ Добавить ответ</button>
</div>
<div class="pool-block">
<div class="pool-label" style="margin-bottom:10px">
Ответы для <span class="star-chip star-chip--4">4★</span>
</div>
<div class="pool-items" id="pool-items-4"></div>
<button type="button" class="btn-add-item" onclick="addPoolItem('4')">+ Добавить ответ</button>
<form method="post" action="{{ url_for('auto_reply_settings') }}" id="settings-form">
<!-- Звёзды -->
<div class="settings-section">
<div class="settings-label">На какие оценки отвечать</div>
<div class="star-toggles">
{% for star in [1,2,3,4,5] %}
<label class="star-toggle star-toggle--{{ star }}">
<input type="checkbox" name="stars" value="{{ star }}"
{% if star in enabled_stars %}checked{% endif %}
onchange="updateColumns()">
<span>{{ star }}★</span>
</label>
{% endfor %}
</div>
</div>
<div style="margin-top:16px">
<button type="submit">Сохранить пулы</button>
<!-- Фильтр типа отзывов -->
<div class="settings-section">
<div class="settings-label">Отвечать на отзывы</div>
<div class="filter-options">
<label class="filter-option">
<input type="radio" name="filter_mode" value="no_text" {% if filter_mode == 'no_text' %}checked{% endif %}>
<span>Без основного текста <small>(достоинства/недостатки допустимы)</small></span>
</label>
<label class="filter-option">
<input type="radio" name="filter_mode" value="empty" {% if filter_mode == 'empty' %}checked{% endif %}>
<span>Полностью пустые <small>(нет текста, достоинств и недостатков)</small></span>
</label>
<label class="filter-option">
<input type="radio" name="filter_mode" value="all" {% if filter_mode == 'all' %}checked{% endif %}>
<span>Все отзывы <small>(независимо от наличия текста)</small></span>
</label>
</div>
</div>
<!-- Динамические колонки пулов -->
<div class="settings-section">
<div class="settings-label">Шаблоны ответов</div>
<div class="pool-columns" id="pool-columns">
{% for star in [1,2,3,4,5] %}
<div class="pool-col pool-col--{{ star }}" id="pool-col-{{ star }}" {% if star not in enabled_stars %}style="display:none"{% endif %}>
<div class="pool-col__header">
<span class="star-chip star-chip--{{ star }}">{{ star }}★</span>
</div>
<div class="pool-items" id="pool-items-{{ star }}"></div>
<textarea name="pool_{{ star }}_raw" id="pool-{{ star }}-hidden" hidden>{{ vars()['reply_pool_' ~ star ~ '_text'] }}</textarea>
<button type="button" class="btn-add-item" onclick="addPoolItem('{{ star }}')">+ Добавить ответ</button>
</div>
{% endfor %}
</div>
</div>
<div style="margin-top:20px">
<button type="submit">Сохранить настройки</button>
</div>
</form>
</div>
@@ -225,126 +235,12 @@
{% endif %}
</div>
<!-- Список отзывов -->
{% if reviews %}
{% set unanswered_count = reviews | rejectattr('answer') | list | length %}
{% set answered_count = reviews | selectattr('answer') | list | length %}
<div class="summary">
<h2>{{ "Неотвеченные отзывы" if current_filter == 'unanswered' else "Все отзывы" }}</h2>
<p>Оценки: {{ selected_stars_display|join(', ') }}★.</p>
</div>
<!-- Счётчики -->
<div class="stats-bar">
<div class="stat-chip">
<span class="stat-value">{{ reviews|length }}</span>
<span>отзывов загружено</span>
</div>
{% if unanswered_count > 0 %}
<div class="stat-chip stat-chip--warn">
<span class="stat-value">{{ unanswered_count }}</span>
<span>без ответа</span>
</div>
{% endif %}
{% if answered_count > 0 %}
<div class="stat-chip stat-chip--ok">
<span class="stat-value">{{ answered_count }}</span>
<span>отвечено</span>
</div>
{% endif %}
{% for star in [5,4,3,2,1] %}
{% set cnt = reviews | selectattr('rating', 'equalto', star) | list | length %}
{% if cnt > 0 %}
<div class="stat-chip">
<span class="stat-value">{{ cnt }}</span>
<span>{{ star }}★</span>
</div>
{% endif %}
{% endfor %}
</div>
{% if current_filter == 'unanswered' and has_token %}
<form class="reply-form" method="post" action="{{ url_for('reply_all') }}">
<label for="reply-text">Ответ для всех неотвеченных отзывов</label>
<textarea id="reply-text" name="message" rows="4" placeholder="Напишите ответ один раз — он будет отправлен каждому неотвеченному отзыву" required></textarea>
{% for star in selected_stars_display %}
<input type="hidden" name="stars" value="{{ star }}">
{% endfor %}
{% for review in reviews %}
<input type="hidden" name="review_id" value="{{ review.id }}">
{% endfor %}
<div class="reply-form__actions">
<span>Допустимая длина: 2–5000 символов.</span>
<button type="submit">Ответить всем</button>
</div>
</form>
{% endif %}
<div class="reviews">
{% for review in reviews %}
<article class="review" data-rating="{{ review.rating }}">
<header class="review__header">
<div>
<span class="review__product">{{ review.product_name or 'Без названия' }}</span>
<span class="rating rating--{{ review.rating }}">★ {{ review.rating }}</span>
</div>
<div class="review__meta">
<span>{{ review.user_name or 'Покупатель' }}</span>
<span>{{ review.created_at|format_datetime }}</span>
</div>
</header>
{% if review.text %}
<p class="review__text">{{ review.text }}</p>
{% endif %}
<dl class="details">
{% if review.pros %}
<dt>Достоинства</dt>
<dd>{{ review.pros }}</dd>
{% endif %}
{% if review.cons %}
<dt>Недостатки</dt>
<dd>{{ review.cons }}</dd>
{% endif %}
</dl>
{% if review.answer %}
<p class="answer"><strong>Ответ:</strong> {{ review.answer }}</p>
{% else %}
<p class="no-answer">Без ответа</p>
{% endif %}
{% if has_token %}
<details class="inline-reply">
<summary>Ответить на отзыв</summary>
<form method="post" action="{{ url_for('reply_single', review_id=review.id) }}">
<textarea name="message" rows="3" placeholder="Введите ответ" required></textarea>
{% for star in selected_stars_display %}
<input type="hidden" name="stars" value="{{ star }}">
{% endfor %}
<input type="hidden" name="next_action" value="{{ 'unanswered' if current_filter == 'unanswered' else 'all' }}">
<button type="submit">Отправить</button>
</form>
</details>
{% endif %}
</article>
{% endfor %}
</div>
{% elif current_filter %}
<div class="alert">Отзывы с выбранными оценками ({{ selected_stars_display|join(', ') }}★) не найдены.</div>
{% endif %}
</div>
<!-- Нижняя навигация (мобайл) -->
<nav class="bottom-nav">
<a href="{{ url_for('index') }}" class="bottom-nav__item active">
{% if reviews and (reviews | rejectattr('answer') | list | length) > 0 %}
<span class="bottom-nav__badge">{{ reviews | rejectattr('answer') | list | length }}</span>
{% endif %}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/>
<rect x="9" y="3" width="6" height="4" rx="1"/>
@@ -387,40 +283,49 @@
function syncHidden(star) {
const items = document.querySelectorAll(`#pool-items-${star} .pool-item-input`);
const lines = [...items].map(i => i.value.trim()).filter(Boolean);
document.getElementById(`pool-${star}-hidden`).value = lines.join('\n');
// inject as hidden inputs for form submission
const container = document.getElementById(`pool-items-${star}`);
container.querySelectorAll('input[name="pool_' + star + '_item"]').forEach(e => e.remove());
lines.forEach(line => {
const inp = document.createElement('input');
inp.type = 'hidden';
inp.name = `pool_${star}_item`;
inp.value = line;
container.appendChild(inp);
});
}
function addPoolItem(star, value = '') {
const container = document.getElementById(`pool-items-${star}`);
const row = document.createElement('div');
row.className = 'pool-item';
row.innerHTML = `
<input type="text" class="pool-item-input" value="${value.replace(/"/g, '&quot;')}" placeholder="Текст ответа…">
<button type="button" class="btn-delete-item" title="Удалить">✕</button>`;
row.innerHTML = `<input type="text" class="pool-item-input" value="${value.replace(/"/g,'&quot;')}" placeholder="Текст ответа…"><button type="button" class="btn-delete-item" title="Удалить">✕</button>`;
row.querySelector('.pool-item-input').addEventListener('input', () => syncHidden(star));
row.querySelector('.btn-delete-item').addEventListener('click', () => {
row.remove();
syncHidden(star);
});
row.querySelector('.btn-delete-item').addEventListener('click', () => { row.remove(); syncHidden(star); });
container.appendChild(row);
syncHidden(star);
row.querySelector('input').focus();
}
function updateColumns() {
[1,2,3,4,5].forEach(star => {
const cb = document.querySelector(`input[name="stars"][value="${star}"]`);
const col = document.getElementById(`pool-col-${star}`);
if (col) col.style.display = cb && cb.checked ? '' : 'none';
});
}
function initPool(star) {
const hidden = document.getElementById(`pool-${star}-hidden`);
const lines = hidden.value.split('\n').map(l => l.trim()).filter(Boolean);
lines.forEach(line => addPoolItem(star, line));
if (!hidden) return;
hidden.value.split('\n').map(l => l.trim()).filter(Boolean).forEach(line => addPoolItem(star, line));
}
initPool('5');
initPool('4');
document.getElementById('pools-form').addEventListener('submit', () => {
syncHidden('5');
syncHidden('4');
document.getElementById('settings-form').addEventListener('submit', () => {
[1,2,3,4,5].forEach(syncHidden);
});
[1,2,3,4,5].forEach(initPool);
// ── API polling — reload when new log entry appears ────────────────
(() => {
let lastLogId = null;
+3 -3
View File
@@ -22,7 +22,7 @@
<span class="login-features__icon"></span>
<div>
<strong>Автоответы 24/7</strong>
<span>Отвечает на 4★ и 5★ без текста автоматически, по расписанию</span>
<span>Настраивайте ответы для каждого рейтинга — от 1★ до 5★, с нужными шаблонами</span>
</div>
</li>
<li class="login-features__item">
@@ -49,8 +49,8 @@
</ul>
<div class="login-price">
<div class="login-price__amount">200<span>/мес</span></div>
<div class="login-price__desc">Один платёж — все магазины, без ограничений на количество отзывов</div>
<div class="login-price__amount">7<span>/день</span></div>
<div class="login-price__desc">За один магазин. Неограниченное количество отзывов</div>
</div>
</div>
</div>