| | import os |
| | import sys |
| | import csv |
| | from datetime import datetime |
| | from pathlib import Path |
| | from typing import Optional, Tuple, List, Dict |
| |
|
| | import gradio as gr |
| | import requests |
| |
|
| | |
| | try: |
| | from dotenv import load_dotenv |
| | except Exception: |
| | load_dotenv = None |
| |
|
| |
|
| | |
| | PROJECT_ROOT = Path(__file__).resolve().parents[1] |
| | if str(PROJECT_ROOT) not in sys.path: |
| | sys.path.insert(0, str(PROJECT_ROOT)) |
| |
|
| | if load_dotenv: |
| | load_dotenv(PROJECT_ROOT / ".env") |
| |
|
| |
|
| | |
| | DATA_DIR = PROJECT_ROOT / "data" |
| | FEEDBACK_DIR = DATA_DIR / "feedback" |
| | FEEDBACK_CSV = FEEDBACK_DIR / "feedback.csv" |
| |
|
| |
|
| | |
| | def send_telegram_message(text: str) -> Tuple[bool, str]: |
| | """ |
| | Отправляет сообщение в Telegram (если TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID заданы). |
| | Возвращает (ok, details). |
| | """ |
| | token = os.getenv("TELEGRAM_BOT_TOKEN", "").strip() |
| | chat_id = os.getenv("TELEGRAM_CHAT_ID", "").strip() |
| |
|
| | if not token or not chat_id: |
| | return False, "Telegram: пропущено (нет TELEGRAM_BOT_TOKEN или TELEGRAM_CHAT_ID)" |
| |
|
| | url = f"https://api.telegram.org/bot{token}/sendMessage" |
| | try: |
| | r = requests.post(url, data={"chat_id": chat_id, "text": text}, timeout=10) |
| | if r.status_code == 200: |
| | return True, "Telegram: отправлено ✅" |
| | return False, f"Telegram: ошибка {r.status_code}: {r.text[:200]}" |
| | except Exception as e: |
| | return False, f"Telegram: исключение: {e}" |
| |
|
| |
|
| | |
| | def ensure_feedback_csv_exists() -> None: |
| | FEEDBACK_DIR.mkdir(parents=True, exist_ok=True) |
| | if not FEEDBACK_CSV.exists(): |
| | with FEEDBACK_CSV.open("w", newline="", encoding="utf-8") as f: |
| | w = csv.writer(f) |
| | w.writerow(["timestamp", "category", "rating", "contact", "feedback_text"]) |
| |
|
| |
|
| | def append_feedback_row(category: str, rating: int, contact: str, feedback_text: str) -> None: |
| | ensure_feedback_csv_exists() |
| | ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
| | with FEEDBACK_CSV.open("a", newline="", encoding="utf-8") as f: |
| | w = csv.writer(f) |
| | w.writerow([ts, category, rating, contact.strip(), feedback_text.strip()]) |
| |
|
| |
|
| | |
| | def save_feedback(category: str, rating: int, contact: str, feedback_text: str): |
| | if not feedback_text or not feedback_text.strip(): |
| | return "⚠️ Введите текст обратной связи.", gr.update(value=feedback_text), None |
| |
|
| | if rating is None: |
| | return "⚠️ Выберите оценку (1–5).", gr.update(value=feedback_text), None |
| |
|
| | try: |
| | append_feedback_row(category, int(rating), contact or "", feedback_text) |
| | except Exception as e: |
| | return f"❌ Не удалось сохранить в CSV: {e}", gr.update(value=feedback_text), None |
| |
|
| | |
| | tg_text = ( |
| | "📝 2MOOD Feedback\n" |
| | f"Категория: {category}\n" |
| | f"Оценка: {rating}\n" |
| | f"Контакт: {contact or '-'}\n" |
| | f"Текст: {feedback_text.strip()}" |
| | ) |
| | tg_ok, tg_details = send_telegram_message(tg_text) |
| |
|
| | status = "✅ Отзыв сохранён в CSV.\n\n" |
| | status += f"Файл: `{FEEDBACK_CSV.as_posix()}`\n\n" |
| | status += ("✅ " if tg_ok else "⚠️ ") + tg_details |
| |
|
| | |
| | return status, gr.update(value=""), str(FEEDBACK_CSV) |
| |
|
| |
|
| | def get_feedback_csv() -> Tuple[Optional[str], str]: |
| | if FEEDBACK_CSV.exists() and FEEDBACK_CSV.stat().st_size > 0: |
| | return str(FEEDBACK_CSV), "✅ Готово: файл доступен для скачивания." |
| | return None, "⚠️ Файл ещё не создан или пуст." |
| |
|
| |
|
| | |
| | def messenger_reply(user_message: str, history: Optional[List[Dict[str, str]]]): |
| | """ |
| | Gradio Chatbot с type="messages" ожидает: |
| | [{"role":"user","content":"..."}, {"role":"assistant","content":"..."} ...] |
| | """ |
| | history = history or [] |
| |
|
| | msg = (user_message or "").strip() |
| | if not msg: |
| | return "", history |
| |
|
| | history.append({"role": "user", "content": msg}) |
| | history.append({"role": "assistant", "content": f"Echo: {msg}"}) |
| |
|
| | return "", history |
| |
|
| |
|
| | def build_interface(): |
| | ensure_feedback_csv_exists() |
| |
|
| | with gr.Blocks(title="2MOOD AI Workspace (Pilot)") as demo: |
| | gr.Markdown("# 2MOOD AI Workspace") |
| | gr.Markdown("База знаний + Messenger + Обратная связь") |
| |
|
| | with gr.Tab("📚 База знаний"): |
| | gr.Markdown("Загрузка PDF и индексирование (пока заглушка)") |
| | gr.File(label="Загрузить PDF") |
| | gr.Button("Добавить в базу") |
| |
|
| | with gr.Tab("💬 2MOOD Messenger"): |
| | |
| | chat = gr.Chatbot(height=420, type="messages") |
| | msg = gr.Textbox(label="Сообщение", placeholder="Напишите сообщение…") |
| | send = gr.Button("Отправить", variant="primary") |
| |
|
| | send.click(messenger_reply, inputs=[msg, chat], outputs=[msg, chat]) |
| |
|
| | with gr.Tab("📝 Обратная связь"): |
| | category = gr.Dropdown( |
| | label="Категория", |
| | choices=["Другое", "Messenger", "База знаний", "UI/UX", "Ошибка/Баг", "Идея/Feature"], |
| | value="Другое", |
| | ) |
| |
|
| | rating = gr.Radio( |
| | label="Оценка", |
| | choices=[1, 2, 3, 4, 5], |
| | value=5, |
| | ) |
| |
|
| | contact = gr.Textbox(label="Контакт (опционально): Telegram/почта") |
| | feedback_text = gr.Textbox( |
| | label="Текст обратной связи", |
| | lines=6, |
| | placeholder="Например: не хватает кнопки…, ошибка…, хочу экспорт в Word…", |
| | ) |
| |
|
| | submit = gr.Button("Отправить", variant="primary") |
| | status = gr.Markdown() |
| |
|
| | |
| | download_btn = gr.Button("📥 Скачать все отзывы") |
| | download_file = gr.File(label="Файл с отзывами", interactive=False) |
| | download_status = gr.Markdown() |
| |
|
| | |
| | gr.Markdown(f"Файл логов: `{FEEDBACK_CSV.as_posix()}`") |
| |
|
| | submit.click( |
| | save_feedback, |
| | inputs=[category, rating, contact, feedback_text], |
| | outputs=[status, feedback_text, download_file], |
| | ) |
| |
|
| | download_btn.click( |
| | get_feedback_csv, |
| | inputs=None, |
| | outputs=[download_file, download_status], |
| | ) |
| |
|
| | return demo |
| |
|
| |
|
| | def launch_with_port_fallback(demo: gr.Blocks): |
| | """ |
| | Устойчивый запуск: |
| | - сначала локально (127.0.0.1, share=False) |
| | - если Gradio считает, что localhost недоступен (proxy/настройки) -> fallback share=True |
| | """ |
| | |
| | os.environ.setdefault("NO_PROXY", "localhost,127.0.0.1") |
| | os.environ.setdefault("no_proxy", "localhost,127.0.0.1") |
| |
|
| | env_port = os.getenv("GRADIO_SERVER_PORT") or os.getenv("PORT") or "" |
| | ports: List[int] = [] |
| | if str(env_port).isdigit(): |
| | ports.append(int(env_port)) |
| |
|
| | ports.extend([7860] + list(range(7861, 7871))) |
| |
|
| | last_error: Optional[Exception] = None |
| |
|
| | for port in ports: |
| | |
| | try: |
| | demo.launch( |
| | server_name="127.0.0.1", |
| | server_port=port, |
| | share=False, |
| | inbrowser=True, |
| | ) |
| | return |
| | except Exception as e: |
| | last_error = e |
| | msg = str(e) |
| |
|
| | |
| | if "localhost is not accessible" in msg or "shareable link must be created" in msg: |
| | try: |
| | demo.launch( |
| | server_name="0.0.0.0", |
| | server_port=port, |
| | share=True, |
| | inbrowser=False, |
| | ) |
| | return |
| | except Exception as e2: |
| | last_error = e2 |
| | continue |
| |
|
| | continue |
| |
|
| | raise RuntimeError(f"Не удалось запустить Gradio ни на одном порту. Последняя ошибка: {last_error}") |
| |
|
| |
|
| | if __name__ == "__main__": |
| | app = build_interface() |
| | launch_with_port_fallback(app) |
| |
|