diff --git a/app.py b/app.py index 111044f..24c9e93 100644 --- a/app.py +++ b/app.py @@ -28,6 +28,11 @@ AUTO_REPLY_SETTING_KEY = "auto_reply_enabled" AUTO_REPLY_LAST_RUN_KEY = "auto_reply_last_run_ts" AUTO_REPLY_POOL_5_KEY = "auto_reply_pool_5" 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_LAST_FETCH_KEY = "auto_reply_last_fetch_ts" 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]: queue: List[dict] = [] + filter_mode = _load_filter_mode() 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) db.add_auto_reply_log( review_id=review.id, @@ -863,9 +869,32 @@ def _load_reply_pool(star: int) -> List[str]: if star == 4: db_value = db.get_setting(AUTO_REPLY_POOL_4_KEY) 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 [] +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: return "\n".join(pool) @@ -877,8 +906,17 @@ def _pick_auto_reply(star: int) -> Optional[str]: return None -def _has_review_content(review: Review) -> bool: - return bool((review.text or "").strip()) +def _has_review_content(review: Review, filter_mode: str = "no_text") -> bool: + 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: @@ -906,7 +944,7 @@ def process_auto_replies() -> int: reviews = client.fetch_reviews( limit=100, unanswered_only=True, - allowed_ratings=AUTO_REPLY_STARS, + allowed_ratings=_load_enabled_stars(), ) except FeedbackApiError: 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_in_seconds=next_auto_reply_in_seconds, 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_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_4_list=_load_reply_pool(4), 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])) +@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"]) @login_required def reply_all(): diff --git a/static/styles.css b/static/styles.css index 8783034..b430407 100644 --- a/static/styles.css +++ b/static/styles.css @@ -897,7 +897,7 @@ textarea:focus { } .login-promo { - flex: 1 1 55%; + flex: 0 0 300px; background: linear-gradient(145deg, #3D0066 0%, #6B0FA8 45%, #CB11AB 100%); display: flex; align-items: center; @@ -930,8 +930,8 @@ textarea:focus { } .login-promo__logo { - width: 96px; - height: 96px; + width: 140px; + height: 140px; object-fit: contain; border-radius: 20px; background: white; @@ -1443,3 +1443,85 @@ textarea:focus { color: #6B7280; 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; } diff --git a/templates/index.html b/templates/index.html index 8bf9d73..f4999c3 100644 --- a/templates/index.html +++ b/templates/index.html @@ -40,29 +40,6 @@
Для каждого нового отзыва текст выбирается случайно из пула.
-