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
+64 -4
View File
@@ -28,6 +28,11 @@ AUTO_REPLY_SETTING_KEY = "auto_reply_enabled"
AUTO_REPLY_LAST_RUN_KEY = "auto_reply_last_run_ts" AUTO_REPLY_LAST_RUN_KEY = "auto_reply_last_run_ts"
AUTO_REPLY_POOL_5_KEY = "auto_reply_pool_5" AUTO_REPLY_POOL_5_KEY = "auto_reply_pool_5"
AUTO_REPLY_POOL_4_KEY = "auto_reply_pool_4" AUTO_REPLY_POOL_4_KEY = "auto_reply_pool_4"
AUTO_REPLY_POOL_3_KEY = "auto_reply_pool_3"
AUTO_REPLY_POOL_2_KEY = "auto_reply_pool_2"
AUTO_REPLY_POOL_1_KEY = "auto_reply_pool_1"
AUTO_REPLY_STARS_KEY = "auto_reply_stars_json"
AUTO_REPLY_FILTER_KEY = "auto_reply_filter"
AUTO_REPLY_QUEUE_KEY = "auto_reply_queue_json" AUTO_REPLY_QUEUE_KEY = "auto_reply_queue_json"
AUTO_REPLY_LAST_FETCH_KEY = "auto_reply_last_fetch_ts" AUTO_REPLY_LAST_FETCH_KEY = "auto_reply_last_fetch_ts"
AUTO_REPLY_INTERVAL_MINUTES = int(os.getenv("AUTO_REPLY_INTERVAL_MINUTES", "10")) AUTO_REPLY_INTERVAL_MINUTES = int(os.getenv("AUTO_REPLY_INTERVAL_MINUTES", "10"))
@@ -815,8 +820,9 @@ def _save_auto_reply_queue(queue: List[dict]) -> None:
def _build_auto_reply_queue(reviews: List[Review]) -> List[dict]: def _build_auto_reply_queue(reviews: List[Review]) -> List[dict]:
queue: List[dict] = [] queue: List[dict] = []
filter_mode = _load_filter_mode()
for review in reviews: for review in reviews:
if _has_review_content(review): if _has_review_content(review, filter_mode):
logger.info("Auto-reply skip review_id=%s reason=has_review_text", review.id) logger.info("Auto-reply skip review_id=%s reason=has_review_text", review.id)
db.add_auto_reply_log( db.add_auto_reply_log(
review_id=review.id, review_id=review.id,
@@ -863,9 +869,32 @@ def _load_reply_pool(star: int) -> List[str]:
if star == 4: if star == 4:
db_value = db.get_setting(AUTO_REPLY_POOL_4_KEY) db_value = db.get_setting(AUTO_REPLY_POOL_4_KEY)
return _parse_pool(db_value or ENV_REPLY_POOL_4, DEFAULT_REPLY_POOL_4) return _parse_pool(db_value or ENV_REPLY_POOL_4, DEFAULT_REPLY_POOL_4)
if star in (1, 2, 3):
return _parse_pool(db.get_setting(f"auto_reply_pool_{star}") or "", [])
return [] return []
def _load_enabled_stars() -> Set[int]:
raw = db.get_setting(AUTO_REPLY_STARS_KEY)
if raw:
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
result = {int(s) for s in parsed if isinstance(s, (int, float, str)) and str(s).isdigit() and 1 <= int(s) <= 5}
if result:
return result
except (TypeError, ValueError):
pass
return AUTO_REPLY_STARS
def _load_filter_mode() -> str:
raw = db.get_setting(AUTO_REPLY_FILTER_KEY)
if raw in ("no_text", "empty", "all"):
return raw
return "no_text"
def _pool_to_multiline_text(pool: List[str]) -> str: def _pool_to_multiline_text(pool: List[str]) -> str:
return "\n".join(pool) return "\n".join(pool)
@@ -877,8 +906,17 @@ def _pick_auto_reply(star: int) -> Optional[str]:
return None return None
def _has_review_content(review: Review) -> bool: def _has_review_content(review: Review, filter_mode: str = "no_text") -> bool:
return bool((review.text or "").strip()) if filter_mode == "no_text":
return bool((review.text or "").strip())
if filter_mode == "empty":
return bool(
(review.text or "").strip()
or (review.pros or "").strip()
or (review.cons or "").strip()
)
# filter_mode == "all": reply to everything, treat as no content to skip
return False
def process_auto_replies() -> int: def process_auto_replies() -> int:
@@ -906,7 +944,7 @@ def process_auto_replies() -> int:
reviews = client.fetch_reviews( reviews = client.fetch_reviews(
limit=100, limit=100,
unanswered_only=True, unanswered_only=True,
allowed_ratings=AUTO_REPLY_STARS, allowed_ratings=_load_enabled_stars(),
) )
except FeedbackApiError: except FeedbackApiError:
db.set_setting(AUTO_REPLY_LAST_FETCH_KEY, str(time.time())) db.set_setting(AUTO_REPLY_LAST_FETCH_KEY, str(time.time()))
@@ -1265,8 +1303,13 @@ def index():
next_auto_reply_at=next_auto_reply_at, next_auto_reply_at=next_auto_reply_at,
next_auto_reply_in_seconds=next_auto_reply_in_seconds, next_auto_reply_in_seconds=next_auto_reply_in_seconds,
api_cooldown_seconds_left=api_cooldown_seconds_left, api_cooldown_seconds_left=api_cooldown_seconds_left,
enabled_stars=list(_load_enabled_stars()),
filter_mode=_load_filter_mode(),
reply_pool_5_text=_pool_to_multiline_text(_load_reply_pool(5)), reply_pool_5_text=_pool_to_multiline_text(_load_reply_pool(5)),
reply_pool_4_text=_pool_to_multiline_text(_load_reply_pool(4)), reply_pool_4_text=_pool_to_multiline_text(_load_reply_pool(4)),
reply_pool_3_text=_pool_to_multiline_text(_load_reply_pool(3)),
reply_pool_2_text=_pool_to_multiline_text(_load_reply_pool(2)),
reply_pool_1_text=_pool_to_multiline_text(_load_reply_pool(1)),
reply_pool_5_list=_load_reply_pool(5), reply_pool_5_list=_load_reply_pool(5),
reply_pool_4_list=_load_reply_pool(4), reply_pool_4_list=_load_reply_pool(4),
auto_reply_queue=_load_auto_reply_queue(), auto_reply_queue=_load_auto_reply_queue(),
@@ -1322,6 +1365,23 @@ def auto_reply_pools():
return redirect(url_for("index", status="pools_saved", action="unanswered", stars=[5, 4])) return redirect(url_for("index", status="pools_saved", action="unanswered", stars=[5, 4]))
@app.route("/auto-reply-settings", methods=["POST"])
@login_required
def auto_reply_settings():
stars = [int(s) for s in request.form.getlist("stars") if s.isdigit() and 1 <= int(s) <= 5]
filter_mode = request.form.get("filter_mode", "no_text")
if filter_mode not in ("no_text", "empty", "all"):
filter_mode = "no_text"
db.set_setting(AUTO_REPLY_STARS_KEY, json.dumps(stars))
db.set_setting(AUTO_REPLY_FILTER_KEY, filter_mode)
# Save pools for each enabled star
for star in range(1, 6):
items = [i.strip() for i in request.form.getlist(f"pool_{star}_item") if i.strip()]
if items:
db.set_setting(f"auto_reply_pool_{star}", _pool_to_multiline_text(items))
return redirect(url_for("index", status="pools_saved"))
@app.route("/reply", methods=["POST"]) @app.route("/reply", methods=["POST"])
@login_required @login_required
def reply_all(): def reply_all():
+85 -3
View File
@@ -897,7 +897,7 @@ textarea:focus {
} }
.login-promo { .login-promo {
flex: 1 1 55%; flex: 0 0 300px;
background: linear-gradient(145deg, #3D0066 0%, #6B0FA8 45%, #CB11AB 100%); background: linear-gradient(145deg, #3D0066 0%, #6B0FA8 45%, #CB11AB 100%);
display: flex; display: flex;
align-items: center; align-items: center;
@@ -930,8 +930,8 @@ textarea:focus {
} }
.login-promo__logo { .login-promo__logo {
width: 96px; width: 140px;
height: 96px; height: 140px;
object-fit: contain; object-fit: contain;
border-radius: 20px; border-radius: 20px;
background: white; background: white;
@@ -1443,3 +1443,85 @@ textarea:focus {
color: #6B7280; color: #6B7280;
white-space: nowrap; white-space: nowrap;
} }
/* ─── Auto-reply settings ────────────────────────────────────────── */
.settings-section {
margin-bottom: 24px;
}
.settings-label {
font-size: 13px;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 12px;
}
.star-toggles {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.star-toggle {
cursor: pointer;
user-select: none;
}
.star-toggle input { display: none; }
.star-toggle span {
display: inline-flex;
align-items: center;
padding: 8px 18px;
border-radius: 99px;
font-weight: 700;
font-size: 15px;
border: 2px solid currentColor;
opacity: 0.35;
transition: opacity .15s, transform .15s;
}
.star-toggle input:checked + span { opacity: 1; transform: scale(1.05); }
.star-toggle--1 span { color: #EF4444; }
.star-toggle--2 span { color: #F97316; }
.star-toggle--3 span { color: #EAB308; }
.star-toggle--4 span { color: #3B82F6; }
.star-toggle--5 span { color: #22C55E; }
.filter-options {
display: flex;
flex-direction: column;
gap: 10px;
}
.filter-option {
display: flex;
align-items: flex-start;
gap: 10px;
cursor: pointer;
font-size: 14px;
}
.filter-option input { margin-top: 3px; }
.filter-option small { display: block; color: var(--muted); font-size: 12px; }
.pool-columns {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.pool-col {
flex: 1 1 180px;
min-width: 160px;
border-radius: var(--r);
padding: 16px;
border: 2px solid;
}
.pool-col--1 { border-color: #FEE2E2; background: #FFF5F5; }
.pool-col--2 { border-color: #FFEDD5; background: #FFFAF5; }
.pool-col--3 { border-color: #FEF9C3; background: #FFFEF0; }
.pool-col--4 { border-color: #DBEAFE; background: #F0F7FF; }
.pool-col--5 { border-color: #DCFCE7; background: #F0FFF4; }
.pool-col__header {
margin-bottom: 12px;
font-weight: 700;
}
/* star-chip colors 1-3 */
.star-chip--1 { background: #FEE2E2; color: #EF4444; }
.star-chip--2 { background: #FFEDD5; color: #F97316; }
.star-chip--3 { background: #FEF9C3; color: #CA8A04; }
+81 -176
View File
@@ -40,29 +40,6 @@
<div class="alert alert-success">{{ success_message }}</div> <div class="alert alert-success">{{ success_message }}</div>
{% endif %} {% 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="card auto-reply">
<div class="auto-reply__info"> <div class="auto-reply__info">
@@ -92,33 +69,66 @@
</form> </form>
</div> </div>
<!-- Пулы автоответов --> <!-- Настройки автоответа -->
<div class="card"> <div class="card">
<div class="section-header"> <div class="section-header">
<h2>Пулы автоответов</h2> <h2>Настройки автоответа</h2>
</div> </div>
<p style="margin-bottom:16px;font-size:13px">Для каждого нового отзыва текст выбирается случайно из пула.</p> <form method="post" action="{{ url_for('auto_reply_settings') }}" id="settings-form">
<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="settings-section">
<div class="pool-grid"> <div class="settings-label">На какие оценки отвечать</div>
<div class="pool-block"> <div class="star-toggles">
<div class="pool-label" style="margin-bottom:10px"> {% for star in [1,2,3,4,5] %}
Ответы для <span class="star-chip star-chip--5">5★</span> <label class="star-toggle star-toggle--{{ star }}">
</div> <input type="checkbox" name="stars" value="{{ star }}"
<div class="pool-items" id="pool-items-5"></div> {% if star in enabled_stars %}checked{% endif %}
<button type="button" class="btn-add-item" onclick="addPoolItem('5')">+ Добавить ответ</button> onchange="updateColumns()">
</div> <span>{{ star }}★</span>
<div class="pool-block"> </label>
<div class="pool-label" style="margin-bottom:10px"> {% endfor %}
Ответы для <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>
</div> </div>
</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> </div>
</form> </form>
</div> </div>
@@ -225,126 +235,12 @@
{% endif %} {% endif %}
</div> </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> </div>
<!-- Нижняя навигация (мобайл) --> <!-- Нижняя навигация (мобайл) -->
<nav class="bottom-nav"> <nav class="bottom-nav">
<a href="{{ url_for('index') }}" class="bottom-nav__item active"> <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"> <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"/> <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"/> <rect x="9" y="3" width="6" height="4" rx="1"/>
@@ -387,40 +283,49 @@
function syncHidden(star) { function syncHidden(star) {
const items = document.querySelectorAll(`#pool-items-${star} .pool-item-input`); const items = document.querySelectorAll(`#pool-items-${star} .pool-item-input`);
const lines = [...items].map(i => i.value.trim()).filter(Boolean); 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 = '') { function addPoolItem(star, value = '') {
const container = document.getElementById(`pool-items-${star}`); const container = document.getElementById(`pool-items-${star}`);
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'pool-item'; row.className = 'pool-item';
row.innerHTML = ` row.innerHTML = `<input type="text" class="pool-item-input" value="${value.replace(/"/g,'&quot;')}" placeholder="Текст ответа…"><button type="button" class="btn-delete-item" title="Удалить">✕</button>`;
<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('.pool-item-input').addEventListener('input', () => syncHidden(star));
row.querySelector('.btn-delete-item').addEventListener('click', () => { row.querySelector('.btn-delete-item').addEventListener('click', () => { row.remove(); syncHidden(star); });
row.remove();
syncHidden(star);
});
container.appendChild(row); container.appendChild(row);
syncHidden(star); 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) { function initPool(star) {
const hidden = document.getElementById(`pool-${star}-hidden`); const hidden = document.getElementById(`pool-${star}-hidden`);
const lines = hidden.value.split('\n').map(l => l.trim()).filter(Boolean); if (!hidden) return;
lines.forEach(line => addPoolItem(star, line)); hidden.value.split('\n').map(l => l.trim()).filter(Boolean).forEach(line => addPoolItem(star, line));
} }
initPool('5'); document.getElementById('settings-form').addEventListener('submit', () => {
initPool('4'); [1,2,3,4,5].forEach(syncHidden);
document.getElementById('pools-form').addEventListener('submit', () => {
syncHidden('5');
syncHidden('4');
}); });
[1,2,3,4,5].forEach(initPool);
// ── API polling — reload when new log entry appears ──────────────── // ── API polling — reload when new log entry appears ────────────────
(() => { (() => {
let lastLogId = null; let lastLogId = null;
+3 -3
View File
@@ -22,7 +22,7 @@
<span class="login-features__icon"></span> <span class="login-features__icon"></span>
<div> <div>
<strong>Автоответы 24/7</strong> <strong>Автоответы 24/7</strong>
<span>Отвечает на 4★ и 5★ без текста автоматически, по расписанию</span> <span>Настраивайте ответы для каждого рейтинга — от 1★ до 5★, с нужными шаблонами</span>
</div> </div>
</li> </li>
<li class="login-features__item"> <li class="login-features__item">
@@ -49,8 +49,8 @@
</ul> </ul>
<div class="login-price"> <div class="login-price">
<div class="login-price__amount">200<span>/мес</span></div> <div class="login-price__amount">7<span>/день</span></div>
<div class="login-price__desc">Один платёж — все магазины, без ограничений на количество отзывов</div> <div class="login-price__desc">За один магазин. Неограниченное количество отзывов</div>
</div> </div>
</div> </div>
</div> </div>