| 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 и индексирование") |
|
|
| pdf_file = gr.File(label="Загрузить PDF") |
| add_btn = gr.Button("Добавить в базу", variant="primary") |
| status = gr.Markdown() |
|
|
| def index_pdf(file): |
| if file is None: |
| return "❌ Файл не выбран" |
|
|
| |
| return f"✅ Файл {file.name} успешно добавлен в базу" |
|
|
| add_btn.click( |
| fn=index_pdf, |
| inputs=pdf_file, |
| outputs=status |
| ) |
| with gr.Tab("💬 2MOOD Messenger"): |
| |
| chat = gr.Chatbot(height=420) |
| 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) |
|
|