import os import io import base64 import json import logging import threading import time from datetime import datetime, timedelta, timezone from uuid import uuid4 import random import string import queue from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, Response from PIL import Image import google.generativeai as genai import numpy as np from huggingface_hub import HfApi, hf_hub_download from huggingface_hub.utils import RepositoryNotFoundError, HfHubHTTPError from werkzeug.utils import secure_filename from dotenv import load_dotenv import requests from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.chrome.options import Options as ChromeOptions from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.keys import Keys from selenium.common.exceptions import TimeoutException, NoSuchElementException from webdriver_manager.chrome import ChromeDriverManager load_dotenv() app = Flask(__name__) app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login' DATA_FILE = 'data.json' SYNC_FILES = [DATA_FILE] REPO_ID = "Kgshop/metastoretest" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") HF_TOKEN_READ = os.getenv("HF_TOKEN_READ") GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") DOWNLOAD_RETRIES = 3 DOWNLOAD_DELAY = 5 ALMATY_TZ = timezone(timedelta(hours=6)) CURRENCIES = { 'KGS': 'Кыргызский сом', 'KZT': 'Казахстанский тенге', 'UAH': 'Украинская гривна', 'RUB': 'Российский рубль', 'USD': 'Доллар США', 'EUR': 'Евро' } COLOR_SCHEMES = { 'default': 'Бирюзовый (по умолч.)', 'forest': 'Лесной зеленый', 'ocean': 'Глубокий синий', 'sunset': 'Закатный оранжевый', 'lavender': 'Лавандовый', 'vintage': 'Винтажный', 'dark': 'Полночь (тёмная)', 'cosmic': 'Космическая ночь', 'minty': 'Свежая мята', 'mocha': 'Кофейный мокко', 'crimson': 'Багровый рассвет', 'solar': 'Солнечная вспышка', 'cyberpunk': 'Киберпанк неон' } logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') driver = None whatsapp_thread = None whatsapp_env_id = None whatsapp_queues = { 'incoming': queue.Queue(), 'outgoing': queue.Queue() } whatsapp_active = threading.Event() def whatsapp_bot_thread(env_id): global driver, whatsapp_env_id whatsapp_env_id = env_id try: chrome_options = ChromeOptions() user_data_dir = os.path.join(os.getcwd(), "whatsapp_profile") os.makedirs(user_data_dir, exist_ok=True) chrome_options.add_argument(f"--user-data-dir={user_data_dir}") chrome_options.add_argument("--headless=new") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") chrome_options.add_argument("--disable-gpu") chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") try: service = ChromeService(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=chrome_options) except Exception as e: logging.error(f"Could not start WebDriver with manager: {e}. Falling back to default.") driver = webdriver.Chrome(options=chrome_options) driver.get("https://web.whatsapp.com") logging.info("WhatsApp Web page opened. Please scan the QR code if required.") wait = WebDriverWait(driver, 60) wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="side"]/div[1]/div/div[2]/div[2]/div/div[1]/p'))) # Wait for search bar logging.info("WhatsApp Web is logged in and ready.") whatsapp_active.set() processor = threading.Thread(target=process_whatsapp_queues, daemon=True) processor.start() last_processed_messages = {} while whatsapp_active.is_set(): try: unread_chats = driver.find_elements(By.XPATH, '//span[@data-testid="icon-unread-count"]') for chat in unread_chats: try: parent_element = chat.find_element(By.XPATH, './ancestor::div[contains(@class, "_aou8")]') parent_element.click() time.sleep(1) chat_title_element = driver.find_element(By.XPATH, '//header//span[@dir="auto"]') chat_name = chat_title_element.text messages = driver.find_elements(By.XPATH, '//div[contains(@class, "message-in")]') if messages: last_message = messages[-1] message_text_element = last_message.find_element(By.XPATH, './/span[contains(@class, "selectable-text")]/span') message_text = message_text_element.text if last_processed_messages.get(chat_name) != message_text: logging.info(f"New message from '{chat_name}': {message_text}") whatsapp_queues['incoming'].put({'sender': chat_name, 'message': message_text}) last_processed_messages[chat_name] = message_text except (NoSuchElementException, StaleElementReferenceException): continue time.sleep(5) except Exception as e: logging.error(f"Error in WhatsApp listener loop: {e}") time.sleep(10) except TimeoutException: logging.error("Timed out waiting for WhatsApp Web to load. Please scan the QR code faster or check your connection.") except Exception as e: logging.error(f"An error occurred in the WhatsApp bot thread: {e}") finally: if driver: driver.quit() driver = None whatsapp_active.clear() logging.info("WhatsApp bot thread has stopped.") def process_whatsapp_queues(): while whatsapp_active.is_set(): try: # Process incoming messages if not whatsapp_queues['incoming'].empty(): data = whatsapp_queues['incoming'].get() sender = data['sender'] message = data['message'] # Simple chat history management (in-memory for this example) # For a real app, this should be persisted in data.json if 'whatsapp_chats' not in app.config: app.config['whatsapp_chats'] = {} if sender not in app.config['whatsapp_chats']: app.config['whatsapp_chats'][sender] = [] history = app.config['whatsapp_chats'][sender] ai_response = generate_chat_response(message, history, whatsapp_env_id) history.append({'role': 'user', 'text': message}) history.append({'role': 'ai', 'text': ai_response}) # Trim history to keep it from growing too large app.config['whatsapp_chats'][sender] = history[-20:] whatsapp_queues['outgoing'].put({'recipient': sender, 'message': ai_response}) # Process outgoing messages if not whatsapp_queues['outgoing'].empty(): data = whatsapp_queues['outgoing'].get() send_whatsapp_message(data['recipient'], data['message']) time.sleep(1) except Exception as e: logging.error(f"Error processing WhatsApp queues: {e}") def send_whatsapp_message(recipient, message): if not driver or not whatsapp_active.is_set(): logging.warning("WhatsApp driver not ready, cannot send message.") return try: search_box = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, '//*[@id="side"]/div[1]/div/div[2]/div[2]/div/div[1]/p')) ) search_box.clear() search_box.send_keys(recipient) time.sleep(2) chat = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, f'//span[@title="{recipient}"]')) ) chat.click() time.sleep(1) message_box = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, '//*[@id="main"]/footer/div[1]/div/span[2]/div/div[2]/div[1]/div/div[1]/p')) ) for line in message.split('\n'): message_box.send_keys(line) message_box.send_keys(Keys.SHIFT, Keys.ENTER) message_box.send_keys(Keys.ENTER) logging.info(f"Message sent to '{recipient}'.") except TimeoutException: logging.error(f"Could not find chat or message box for '{recipient}'.") except Exception as e: logging.error(f"Failed to send WhatsApp message to '{recipient}': {e}") def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY): if not HF_TOKEN_READ and not HF_TOKEN_WRITE: logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.") token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE files_to_download = [specific_file] if specific_file else SYNC_FILES all_successful = True for file_name in files_to_download: success = False for attempt in range(retries + 1): try: local_path = hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", token=token_to_use, local_dir=".", local_dir_use_symlinks=False, force_download=True, resume_download=False ) success = True break except RepositoryNotFoundError: return False except HfHubHTTPError as e: if e.response.status_code == 404: if attempt == 0 and not os.path.exists(file_name): try: if file_name == DATA_FILE: with open(file_name, 'w', encoding='utf-8') as f: json.dump({}, f) except Exception as create_e: pass success = False break else: pass except requests.exceptions.RequestException as e: pass except Exception as e: pass if attempt < retries: time.sleep(delay) if not success: all_successful = False return all_successful def upload_db_to_hf(specific_file=None): if not HF_TOKEN_WRITE: return try: api = HfApi() files_to_upload = [specific_file] if specific_file else SYNC_FILES for file_name in files_to_upload: if os.path.exists(file_name): try: api.upload_file( path_or_fileobj=file_name, path_in_repo=file_name, repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) except Exception as e: pass else: pass except Exception as e: pass def periodic_backup(): backup_interval = 1800 while True: time.sleep(backup_interval) upload_db_to_hf() def load_data(): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, dict): data = {} except (FileNotFoundError, json.JSONDecodeError): if download_db_from_hf(specific_file=DATA_FILE): try: with open(DATA_FILE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, dict): data = {} except (FileNotFoundError, json.JSONDecodeError): data = {} else: data = {} return data def save_data(data): try: with open(DATA_FILE, 'w', encoding='utf-8') as file: json.dump(data, file, ensure_ascii=False, indent=4) upload_db_to_hf(specific_file=DATA_FILE) except Exception as e: pass def is_chat_active(env_id): data = get_env_data(env_id) settings = data.get('settings', {}) if not settings.get('chat_activated', False): return False expires_str = settings.get('chat_activation_expires') if not expires_str: return False try: expires_dt = datetime.fromisoformat(expires_str) if expires_dt > datetime.now(ALMATY_TZ): return True except (ValueError, TypeError): return False return False def get_env_data(env_id): all_data = load_data() default_organization_info = { "about_us": "Мы — надежный партнер в мире уникальных товаров. Мы предлагаем широкий ассортимент продукции, от электроники до товаров для дома, всегда стремясь к качеству и доступности. Наша миссия — сделать ваш шопинг приятным и удобным, предлагая только лучшие товары, тщательно отобранные для вас.", "shipping": "Доставка осуществляется по всему Кыргызстану. Стоимость и сроки доставки зависят от региона и веса товара. По Бишкеку доставка возможна в течение 1-2 рабочих дней, в регионы — от 3 до 7 дней. Для уточнения деталей свяжитесь с нами.", "returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.", "contact": f"Наш магазин находится по адресу: Рынок Кербен, 6 ряд , 43 контейнер. Связаться с нами можно по телефону или через WhatsApp. Мы работаем ежедневно с 9:00 до 18:00." } default_settings = { "organization_name": "Gippo312", "whatsapp_number": "+996701202013", "currency_code": "KGS", "chat_name": "EVA", "chat_avatar": None, "color_scheme": "default", "chat_activated": False, "chat_activation_expires": None, "business_type": "combined" } env_data = all_data.get(env_id, {}) if not env_data: env_data = { 'products': [], 'categories': [], 'orders': {}, 'chats': {}, 'organization_info': default_organization_info, 'settings': default_settings } if 'products' not in env_data: env_data['products'] = [] if 'categories' not in env_data: env_data['categories'] = [] if 'orders' not in env_data: env_data['orders'] = {} if 'organization_info' not in env_data: env_data['organization_info'] = default_organization_info if 'chats' not in env_data: env_data['chats'] = {} if 'settings' not in env_data: env_data['settings'] = default_settings settings_changed = False for key, value in default_settings.items(): if key not in env_data['settings']: env_data['settings'][key] = value settings_changed = True products_changed = False for product in env_data['products']: if 'product_id' not in product: product['product_id'] = uuid4().hex products_changed = True if 'sizes' not in product: product['sizes'] = [] products_changed = True if 'variant_prices' not in product: product['variant_prices'] = {} products_changed = True if 'wholesale_price' not in product: product['wholesale_price'] = None products_changed = True if 'min_wholesale_quantity' not in product: product['min_wholesale_quantity'] = None products_changed = True if products_changed or settings_changed: save_env_data(env_id, env_data) return env_data def save_env_data(env_id, env_data): all_data = load_data() all_data[env_id] = env_data save_data(all_data) def configure_gemini(): if not GOOGLE_API_KEY: return False try: genai.configure(api_key=GOOGLE_API_KEY) return True except Exception as e: return False def generate_ai_description_from_image(image_data, language): if not configure_gemini(): raise ValueError("Google AI API не настроен.") try: if not image_data: raise ValueError("Файл изображения не найден.") image_stream = io.BytesIO(image_data) image = Image.open(image_stream).convert('RGB') except Exception as e: raise ValueError(f"Не удалось обработать изображение. Убедитесь, что это действительный файл изображения.") base_prompt = "Напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами и 25 тематических хэштегов с ключевыми словами разных вариантов, чтобы мои клиенты могли найти меня в поиске Instagram, Google и т.д. по ключевым словам. Пост пиши исключительно под товар, который на фото, без адресов и номеров телефона." lang_suffix = "" if language == "Русский": lang_suffix = " Пиши на русском языке." elif language == "Кыргызский": lang_suffix = " Пиши на кыргызском языке." elif language == "Казахский": lang_suffix = " Пиши на казахском языке." elif language == "Узбекский": lang_suffix = " Пиши на узбекском языке." final_prompt = f"{base_prompt}{lang_suffix}" try: model = genai.GenerativeModel('gemma-3-27b-it') response = model.generate_content([final_prompt, image]) if hasattr(response, 'text'): return response.text else: if response.parts: return "".join(part.text for part in response.parts if hasattr(part, 'text')) else: response.resolve() return response.text except Exception as e: if "API key not valid" in str(e): raise ValueError("Внутренняя ошибка конфигурации API.") elif " Billing account not found" in str(e): raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.") elif "Could not find model" in str(e): raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.") elif "resource has been exhausted" in str(e).lower(): raise ValueError("Квота запросов исчерпана. Попробуйте позже.") elif "content has been blocked" in str(e).lower(): reason = "неизвестна" if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback') and e.response.prompt_feedback.block_reason: reason = e.response.prompt_feedback.block_reason raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение или запрос.)") else: raise ValueError(f"Ошибка при генерации контента: {e}") def generate_chat_response(message, chat_history_from_client, env_id): if not is_chat_active(env_id): return "Извините, чат в данный момент неактивен. Пожалуйста, свяжитесь с нами другим способом." if not configure_gemini(): return "Извините, сервис чата временно недоступен. Пожалуйста, попробуйте позже." data = get_env_data(env_id) products = data.get('products', []) categories = data.get('categories', []) organization_info = data.get('organization_info', {}) settings = data.get('settings', {}) currency_code = settings.get('currency_code', 'KGS') chat_name = settings.get('chat_name', 'EVA') org_name = settings.get('organization_name', 'Gippo312') business_type = settings.get('business_type', 'combined') product_info_list = [] for p in products: if p.get('in_stock', True): price_display = f"{p.get('price', 0):.2f}".replace('.00', '') wholesale_info = "" if business_type in ['wholesale', 'combined'] and p.get('wholesale_price') and p.get('min_wholesale_quantity'): wholesale_price_display = f"{p['wholesale_price']:.2f}".replace('.00', '') wholesale_info = (f", Оптовая цена: {wholesale_price_display} {currency_code} " f"(от {p['min_wholesale_quantity']} шт.)") colors_str = f"Цвета: {', '.join(p.get('colors', []))}" if p.get('colors') else "" sizes_str = f"Размеры: {', '.join(p.get('sizes', []))}" if p.get('sizes') else "" options_str = "" if colors_str or sizes_str: options_str = f", Варианты: ({' '.join(filter(None, [colors_str, sizes_str]))})" variant_prices_str = "" if p.get('variant_prices'): variant_prices_list = [f"{k.replace('-', ' ')} - {v} {currency_code}" for k, v in p['variant_prices'].items()] if variant_prices_list: variant_prices_str = f", Особые цены: [{'; '.join(variant_prices_list)}]" product_info_list.append( f"- [ID_ТОВАРА: {p.get('product_id', 'N/A')} Название: {p.get('name', 'Без названия')}], " f"Категория: {p.get('category', 'Без категории')}, " f"Розничная цена: {price_display} {currency_code}" f"{wholesale_info}" f"{options_str}{variant_prices_str}, " f"Описание: {p.get('description', '')[:100]}..." ) product_list_str = "\n".join(product_info_list) if product_info_list else "В данный момент нет товаров в наличии." category_list_str = ", ".join(categories) if categories else "Категорий пока нет." org_info_str = "" if organization_info: org_info_str += "\n\nИнформация о магазине:\n" if organization_info.get("about_us"): org_info_str += f"О нас: {organization_info['about_us']}\n" if organization_info.get("shipping"): org_info_str += f"Доставка: {organization_info['shipping']}\n" if organization_info.get("returns"): org_info_str += f"Возврат и обмен: {organization_info['returns']}\n" if organization_info.get("contact"): org_info_str += f"Контактная информация: {organization_info['contact']}\n" base_prompt = ( f"Ты — первоклассный виртуальный консультант-продажник по имени {chat_name} для магазина {org_name}. " "Говори на любом языке, на котором к тебе обращается клиент. Будь энергичным, убедительным и проактивным. " "Твоя речь должна быть живой, с использованием эмодзи, чтобы располагать к себе клиента. " "Никогда не выдумывай товары, категории или характеристики, которых нет в предоставленных списках. " "Когда ты предлагаешь товар, всегда указывай его название и ID, используя *точный формат*: [ID_ТОВАРА: Название: ]. Это *критически важно* для клиента. " "Если пользователь спрашивает цену на конкретный вариант (цвет или размер), найди ее в 'Особые цены'. Если там нет, используй базовую цену. " "Создавай ощущение срочности: 'Эта модель сейчас очень популярна, осталось всего несколько штук!'. " "Работай с возражениями: если клиент говорит 'дорого', расскажи о качестве, гарантии и уникальных особенностях товара." ) if business_type == 'retail': business_specific_prompt = ( "Твоя главная цель — продавать розничным клиентам. Помогай пользователям находить товары, мастерски отвечай на вопросы о них, предлагай лучшие варианты, создавай ценность и закрывай сделку. " "Активно предлагай сопутствующие товары или более дорогие аналоги (апсейл). Например: 'Отличный выбор! К этому телефону идеально подойдут наши новые беспроводные наушники. Хотите взглянуть?'. " ) elif business_type == 'wholesale': business_specific_prompt = ( "Твоя главная цель — продавать товары оптом для бизнес-клиентов. " "Всегда уточняй, какой объем интересует клиента. Если клиент спрашивает цену, всегда предлагай оптовую цену и условия (минимальное количество). " "Подчеркивай выгоду больших партий и возможность долгосрочного сотрудничества. Например: 'При покупке от 50 единиц цена будет значительно ниже. Какой объем вас интересует для вашего бизнеса?'. " "Твоя задача — находить крупных клиентов и заключать большие сделки." ) else: business_specific_prompt = ( "Твоя главная цель — продавать товары как в розницу, так и оптом. В начале диалога вежливо уточни у клиента, интересует ли его розничная или оптовая покупка, чтобы сделать наилучшее предложение. " "Для розничных клиентов: предлагай сопутствующие товары, рассказывай о преимуществах. " "Для оптовых клиентов: сообщай об оптовых ценах и минимальном количестве для заказа. Подчеркивай выгоду крупных партий. " "Четко разделяй предложения для разных типов клиентов. Например: 'Вас интересует покупка для себя или для бизнеса? У нас есть отличные условия для оптовиков!'. " ) system_instruction_content = ( f"{base_prompt}\n{business_specific_prompt}\n\n" f"Список доступных категорий: {category_list_str}.\n\n" f"Список доступных товаров в магазине (используй эту информацию для ответов):\n" f"{product_list_str}" f"{org_info_str}\n\n" "Если пользователь спрашивает про товары, которых нет, вежливо сообщи об этом и немедленно предложи лучшую альтернативу из имеющихся. " "Если вопрос касается общей информации о магазине (доставка, возврат), используй данные из блока 'Информация о магазине' и сразу после ответа возвращай разговор к покупкам: 'Кстати, я могу помочь вам подобрать что-нибудь еще?'. " "Твоя конечная цель — довольный клиент, который совершил покупку. Действуй!" ) generated_text = "" response = None try: model = genai.GenerativeModel('gemma-3-27b-it') model_chat_history_for_gemini = [ {'role': 'user', 'parts': [{'text': system_instruction_content}]} ] for entry in chat_history_from_client: gemini_role = 'model' if entry['role'] == 'ai' else 'user' model_chat_history_for_gemini.append({ 'role': gemini_role, 'parts': [{'text': entry['text']}] }) chat = model.start_chat(history=model_chat_history_for_gemini) response = chat.send_message(message, generation_config={'max_output_tokens': 1000}) if hasattr(response, 'text'): generated_text = response.text elif response.parts: generated_text = "".join(part.text for part in response.parts if hasattr(part, 'text')) else: response.resolve() if hasattr(response, 'text'): generated_text = response.text else: raise ValueError("AI did not return a valid text response.") return generated_text except Exception as e: if "API key not valid" in str(e): return "Внутренняя ошибка конфигурации API." elif " Billing account not found" in str(e): return "Проблема с биллингом аккаунта Google Cloud." elif "Could not find model" in str(e): return "Модель AI не найдена или недоступна." elif "resource has been exhausted" in str(e).lower(): return "Квота запросов исчерпана. Попробуйте позже." elif "content has been blocked" in str(e).lower() or (response is not None and hasattr(response, 'prompt_feedback') and response.prompt_feedback.block_reason): reason = response.prompt_feedback.block_reason if (response and hasattr(response, 'prompt_feedback')) else "неизвестна" return f"Извините, Ваш запрос был заблокирован из-за политики безопасности (причина: {reason}). Пожалуйста, переформулируйте его." else: return f"Извините, произошла ошибка: {e}" LANDING_PAGE_TEMPLATE = ''' MetaStore - AI система для Вашего Бизнеса ''' ADMHOSTO_TEMPLATE = ''' Главная Админ-панель

Управление Средами

{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

Существующие среды

{% if environments %}
    {% for env in environments %}
  • {{ env.id }}
    {% if env.chat_active %} Активирован до: {{ env.expires_date }} {% else %} Чат не активирован {% endif %}
    Админ Каталог
  • {% endfor %}
{% else %}

Пока не создано ни одной среды.

{% endif %}
''' CATALOG_TEMPLATE = ''' {{ settings.organization_name }} - Каталог
{% set has_products = False %} {% for category_name in ordered_categories %} {% if products_by_category[category_name] %} {% set has_products = True %}

{{ category_name }}

>
{% endif %} {% endfor %} {% if not has_products %}

Товары пока не добавлены.

{% endif %}
{% if chat_is_active %} {% endif %}
''' CHAT_TEMPLATE = ''' {{ settings.organization_name }} - Чат с {{ settings.chat_name }}

Чат с {{ settings.chat_name }}

''' PRODUCT_DETAIL_TEMPLATE = '''

{{ product['name'] }}

{% if product.get('photos') and product['photos']|length > 0 %} {% for photo in product['photos'] %}
{{ product['name'] }} - фото {{ loop.index }}
{% endfor %} {% else %}
Изображение отсутствует
{% endif %}
{% if product.get('photos') and product['photos']|length > 1 %}
{% endif %}
{% if product.get('colors') and product.colors|select('ne', '')|list|length > 0 %}
{% endif %} {% if product.get('sizes') and product.sizes|select('ne', '')|list|length > 0 %}
{% endif %}
{% if settings.business_type == 'wholesale' %}

Цена: {{ "%.0f"|format(product.wholesale_price) }} {{ currency_code }}

Минимальный заказ: {{ product.min_wholesale_quantity }} шт.

{% else %}

Цена: {{ "%.0f"|format(product.price) }} {{ currency_code }}

{% if product.wholesale_price and product.min_wholesale_quantity and settings.business_type == 'combined' %}

Опт: {{ "%.0f"|format(product.wholesale_price) }} {{ currency_code }} от {{ product.min_wholesale_quantity }} шт.

{% endif %} {% endif %}

Категория: {{ product.get('category', 'Без категории') }}

Описание:
{{ product.get('description', 'Описание отсутствует.')|replace('\\n', '
')|safe }}

''' ORDER_TEMPLATE = ''' Заказ №{{ order.id }} - {{ settings.organization_name }}
{% if order %}

Ваш Заказ №{{ order.id }}

Дата создания: {{ order.created_at }}

Товары в заказе

ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}

Статус заказа

Этот заказ был оформлен без входа в систему. Вы можете скорректировать его и отправить в WhatsApp.

Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.

← Вернуться в каталог {% else %}

Ошибка

Заказ с таким ID не найден.

← Вернуться в каталог {% endif %}
''' ADMIN_TEMPLATE = ''' Админ-панель - {{ settings.organization_name }}
Logo

Админ-панель {{ settings.organization_name }} (Среда: {{ env_id }})

Перейти в каталог
{% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %}
{{ message }}
{% endfor %} {% endif %} {% endwith %}

Статус активации чата

{% if chat_status.active %}

Чат активен. Срок действия истекает: {{ chat_status.expires_date }}

{% else %}

Чат неактивен. {% if chat_status.expires_date %}Срок действия истек: {{ chat_status.expires_date }}{% endif %}

Для активации чат-бота, пожалуйста, свяжитесь с администратором.

Написать администратору {% endif %}

Интеграция с WhatsApp

{% if whatsapp_is_active %}

WhatsApp бот активен для среды {{ whatsapp_env_id }}.

Остановить WhatsApp Бота {% else %}

Запустите ИИ-консультанта в WhatsApp. После запуска, откроется браузер. Если потребуется, отсканируйте QR-код.

Запустить WhatsApp Бота {% endif %}

Синхронизация с Датацентром

Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.

Настройки магазина и чата
{% if settings.chat_avatar %}

Текущий аватар:

{% endif %}
Диалоги с {{ settings.chat_name }}
{% if chats %} {% for chat_id, chat_data in chats.items()|sort(reverse=True) %}
ID Диалога: {{ chat_id }}
Сообщений: {{ chat_data|length }} | Последнее сообщение: {{ chat_data[-1].timestamp if chat_data and 'timestamp' in chat_data[-1] else 'N/A' }}
{% endfor %} {% else %}

Пока не было ни одного диалога.

{% endif %}

Управление категориями

Добавить новую категорию

Существующие категории:

{% if categories %}
{% for category in categories %}
{{ category }}
{% endfor %}
{% else %}

Категорий пока нет.

{% endif %}

Информация о магазине

Развернуть/Свернуть

Эта информация будет использоваться ИИ-ассистентом для ответов на вопросы о вашем магазине.

Управление товарами

Добавить новый товар
{% if settings.get('business_type') != 'wholesale' %} {% endif %} {% if settings.get('business_type') != 'retail' %} {% endif %}

Особые цены для вариантов

Здесь можно указать цену для конкретного сочетания цвета и размера. Если цена не указана, будет использоваться базовая.



Список товаров:

{% if products %}
{% for product in products %}
{% if product.get('photos') %} Фото {% else %} Нет фото {% endif %}

{{ product['name'] }} {% if product.get('in_stock', True) %} В наличии {% else %} Нет в наличии {% endif %} {% if product.get('is_top', False) %} Топ {% endif %}

Категория: {{ product.get('category', 'Без категории') }}

{% if settings.get('business_type') != 'wholesale' %}

Цена: {{ "%.2f"|format(product.price) }} {{ currency_code }}

{% endif %} {% if settings.get('business_type') != 'retail' and product.get('wholesale_price') and product.get('min_wholesale_quantity') %}

Опт. цена: {{ "%.2f"|format(product.wholesale_price) }} {{ currency_code }} (от {{ product.min_wholesale_quantity }} шт.)

{% endif %}

Описание: {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}

{% set colors = product.get('colors', []) %} {% set sizes = product.get('sizes', []) %}

Цвета/Вар-ты: {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}

Размеры/Объем: {{ sizes|select('ne', '')|join(', ') if sizes|select('ne', '')|list|length > 0 else 'Нет' }}

{% if product.get('photos') and product['photos']|length > 1 %}

(Всего фото: {{ product['photos']|length }})

{% endif %}

Редактирование: {{ product['name'] }}

{% if settings.get('business_type') != 'wholesale' %} {% endif %} {% if settings.get('business_type') != 'retail' %} {% endif %} {% if product.get('photos') %}

Текущие фото:

{% for photo in product['photos'] %} Фото {{ loop.index }} {% endfor %}
{% endif %}
{% set current_colors = product.get('colors', []) %} {% if current_colors and current_colors|select('ne', '')|list|length > 0 %} {% for color in current_colors %} {% if color.strip() %}
{% endif %} {% endfor %} {% else %}
{% endif %}
{% set current_sizes = product.get('sizes', []) %} {% if current_sizes and current_sizes|select('ne', '')|list|length > 0 %} {% for size in current_sizes %} {% if size.strip() %}
{% endif %} {% endfor %} {% else %}
{% endif %}

Особые цены для вариантов



{% endfor %}
{% else %}

Товаров пока нет.

{% endif %}
''' @app.route('/') def index(): return render_template_string(LANDING_PAGE_TEMPLATE) @app.route('/admhosto', methods=['GET']) def admhosto(): data = load_data() environments_data = [] for env_id, env_data in data.items(): settings = env_data.get('settings', {}) is_active = False expires_soon = False expires_date_str = "N/A" if settings.get('chat_activated', False): expires_str = settings.get('chat_activation_expires') if expires_str: try: expires_dt = datetime.fromisoformat(expires_str) now_almaty = datetime.now(ALMATY_TZ) if expires_dt > now_almaty: is_active = True expires_date_str = expires_dt.strftime('%Y-%m-%d') if (expires_dt - now_almaty).days <= 4: expires_soon = True except (ValueError, TypeError): pass environments_data.append({ "id": env_id, "chat_active": is_active, "expires_soon": expires_soon, "expires_date": expires_date_str }) environments_data.sort(key=lambda x: x['id']) return render_template_string(ADMHOSTO_TEMPLATE, environments=environments_data) @app.route('/admhosto/create', methods=['POST']) def create_environment(): all_data = load_data() while True: new_id = ''.join(random.choices(string.digits, k=6)) if new_id not in all_data: break all_data[new_id] = { 'products': [], 'categories': [], 'orders': {}, 'organization_info': { "about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров.", "shipping": "Доставка осуществляется по всему Кыргызстану.", "returns": "Возврат и обмен товара возможен в течение 14 дней.", "contact": "Наш магазин находится по адресу: ... Связаться с нами можно по телефону ..." }, 'settings': { "organization_name": "Gippo312", "whatsapp_number": "+996701202013", "currency_code": "KGS", "chat_name": "EVA", "chat_avatar": None, "color_scheme": "default", "chat_activated": False, "chat_activation_expires": None, "business_type": "combined" }, 'chats': {} } save_data(all_data) flash(f'Новая среда с ID {new_id} успешно создана.', 'success') return redirect(url_for('admhosto')) @app.route('/admhosto/delete/', methods=['POST']) def delete_environment(env_id): all_data = load_data() if env_id in all_data: del all_data[env_id] save_data(all_data) flash(f'Среда {env_id} была удалена.', 'success') else: flash(f'Среда {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) @app.route('/admhosto/activate/', methods=['POST']) def activate_chat(env_id): period = request.form.get('period') if not period: flash('Не выбран период активации.', 'error') return redirect(url_for('admhosto')) delta = None if period == 'month': delta = timedelta(days=30) elif period == 'half_year': delta = timedelta(days=182) elif period == 'year': delta = timedelta(days=365) if not delta: flash('Неверный период активации.', 'error') return redirect(url_for('admhosto')) data = get_env_data(env_id) if not data: flash(f'Среда {env_id} не найдена.', 'error') return redirect(url_for('admhosto')) settings = data.get('settings', {}) now_almaty = datetime.now(ALMATY_TZ) start_date = now_almaty expires_str = settings.get('chat_activation_expires') if expires_str: try: current_expires_dt = datetime.fromisoformat(expires_str) if current_expires_dt > now_almaty: start_date = current_expires_dt except (ValueError, TypeError): pass new_expires_date = start_date + delta settings['chat_activated'] = True settings['chat_activation_expires'] = new_expires_date.isoformat() data['settings'] = settings save_env_data(env_id, data) flash(f'Чат для среды {env_id} активирован/продлен до {new_expires_date.strftime("%Y-%m-%d")}.', 'success') return redirect(url_for('admhosto')) @app.route('//catalog') def catalog(env_id): data = get_env_data(env_id) all_products_raw = data.get('products', []) settings = data.get('settings', {}) product_categories = set(p.get('category', 'Без категории') for p in all_products_raw) admin_categories = set(data.get('categories', [])) all_cat_names = sorted(list(product_categories.union(admin_categories))) products_in_stock = [p for p in all_products_raw if p.get('in_stock', True)] products_sorted_for_js = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) products_by_category = {cat: [] for cat in all_cat_names} for product in products_in_stock: products_by_category[product.get('category', 'Без категории')].append(product) for category in products_by_category: products_by_category[category].sort(key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)] chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" chat_active = is_chat_active(env_id) return render_template_string( CATALOG_TEMPLATE, products_by_category=products_by_category, ordered_categories=ordered_categories, products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id, chat_is_active=chat_active ) @app.route('//product/') def product_detail(env_id, index): data = get_env_data(env_id) all_products_raw = data.get('products', []) settings = data.get('settings', {}) products_in_stock = [p for p in all_products_raw if p.get('in_stock', True)] products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) try: product = products_sorted[index] except IndexError: return "Товар не найден или отсутствует в наличии.", 404 return render_template_string( PRODUCT_DETAIL_TEMPLATE, product=product, repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, env_id=env_id ) @app.route('//create_order', methods=['POST']) def create_order(env_id): order_data = request.get_json() if not order_data or 'cart' not in order_data or not order_data['cart']: return jsonify({"error": "Корзина пуста или не передана."}), 400 cart_items = order_data['cart'] total_price = 0 processed_cart = [] for item in cart_items: if not all(k in item for k in ('name', 'price', 'quantity')): return jsonify({"error": "Неверный формат товара в корзине."}), 400 try: price = float(item['price']) quantity = int(item['quantity']) if price < 0 or quantity <= 0: raise ValueError("Invalid price or quantity") processed_cart.append({ "product_id": item.get('product_id', 'N/A'), "name": item['name'], "price": price, "quantity": quantity, "color": item.get('color', 'N/A'), "size": item.get('size', 'N/A'), "photo": item.get('photo'), "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{item['photo']}" if item.get('photo') else "https://via.placeholder.com/60x60.png?text=N/A" }) total_price += price * quantity except (ValueError, TypeError) as e: return jsonify({"error": "Неверная цена или количество в товаре."}), 400 order_id = f"{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}" order_timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') new_order = { "id": order_id, "created_at": order_timestamp, "cart": processed_cart, "total_price": round(total_price, 2), "user_info": None, "status": "new" } try: data = get_env_data(env_id) if 'orders' not in data or not isinstance(data.get('orders'), dict): data['orders'] = {} data['orders'][order_id] = new_order save_env_data(env_id, data) return jsonify({"order_id": order_id}), 201 except Exception as e: return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500 @app.route('//order/') def view_order(env_id, order_id): data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) settings = data.get('settings', {}) return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), whatsapp_number=settings.get('whatsapp_number', ''), settings=settings, env_id=env_id) @app.route('//admin', methods=['GET', 'POST']) def admin(env_id): data = get_env_data(env_id) products = data.get('products', []) categories = data.get('categories', []) organization_info = data.get('organization_info', {}) chats = data.get('chats', {}) settings = data.get('settings', {}) if 'orders' not in data or not isinstance(data.get('orders'), dict): data['orders'] = {} if request.method == 'POST': action = request.form.get('action') try: if action == 'add_category': category_name = request.form.get('category_name', '').strip() if category_name and category_name not in categories: categories.append(category_name) data['categories'] = categories save_env_data(env_id, data) flash(f"Категория '{category_name}' успешно добавлена.", 'success') elif not category_name: flash("Название категории не может быть пустым.", 'error') else: flash(f"Категория '{category_name}' уже существует.", 'error') elif action == 'delete_category': category_to_delete = request.form.get('category_name') if category_to_delete and category_to_delete in categories: categories.remove(category_to_delete) updated_count = 0 for product in products: if product.get('category') == category_to_delete: product['category'] = 'Без категории' updated_count += 1 data['categories'] = categories data['products'] = products save_env_data(env_id, data) flash(f"Категория '{category_to_delete}' удалена. {updated_count} товаров обновлено.", 'success') else: flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error') elif action == 'update_org_info': organization_info['about_us'] = request.form.get('about_us', '').strip() organization_info['shipping'] = request.form.get('shipping', '').strip() organization_info['returns'] = request.form.get('returns', '').strip() organization_info['contact'] = request.form.get('contact', '').strip() data['organization_info'] = organization_info save_env_data(env_id, data) flash("Информация о магазине успешно обновлена.", 'success') elif action == 'update_settings': settings['organization_name'] = request.form.get('organization_name', 'Gippo312').strip() settings['whatsapp_number'] = request.form.get('whatsapp_number', '').strip() settings['currency_code'] = request.form.get('currency_code', 'KGS') settings['business_type'] = request.form.get('business_type', 'combined') settings['chat_name'] = request.form.get('chat_name', 'EVA').strip() settings['color_scheme'] = request.form.get('color_scheme', 'default') avatar_file = request.files.get('chat_avatar') if avatar_file and avatar_file.filename: if HF_TOKEN_WRITE: try: api = HfApi() old_avatar = settings.get('chat_avatar') if old_avatar: try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass ext = os.path.splitext(avatar_file.filename)[1].lower() avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}" uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) temp_path = os.path.join(uploads_dir, avatar_filename) avatar_file.save(temp_path) api.upload_file( path_or_fileobj=temp_path, path_in_repo=f"avatars/{avatar_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE ) settings['chat_avatar'] = avatar_filename os.remove(temp_path) flash("Аватар чата успешно обновлен.", 'success') except Exception as e: flash(f"Ошибка при загрузке аватара: {e}", 'error') else: flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning") data['settings'] = settings save_env_data(env_id, data) flash("Настройки магазина и чата успешно обновлены.", 'success') elif action == 'add_product' or action == 'edit_product': product_id = request.form.get('product_id') product_data = {} is_edit = action == 'edit_product' if is_edit: product_data = next((p for p in products if p.get('product_id') == product_id), None) if not product_data: flash(f"Ошибка: товар с ID {product_id} не найден.", 'error') return redirect(url_for('admin', env_id=env_id)) product_data['name'] = request.form.get('name', '').strip() price_str = request.form.get('price', '0').replace(',', '.') product_data['description'] = request.form.get('description', '').strip() category = request.form.get('category') product_data['category'] = category if category in categories else 'Без категории' product_data['colors'] = sorted(list(set(c.strip() for c in request.form.getlist('colors') if c.strip()))) product_data['sizes'] = sorted(list(set(s.strip() for s in request.form.getlist('sizes') if s.strip()))) product_data['in_stock'] = 'in_stock' in request.form product_data['is_top'] = 'is_top' in request.form w_price_str = request.form.get('wholesale_price', '').replace(',', '.') w_quant_str = request.form.get('min_wholesale_quantity', '') if settings.get('business_type') == 'wholesale': if not w_price_str or not w_quant_str: flash("Для оптового типа бизнеса, оптовая цена и минимальное количество обязательны.", 'error') return redirect(url_for('admin', env_id=env_id)) product_data['price'] = 0 product_data['wholesale_price'] = float(w_price_str) if w_price_str else None product_data['min_wholesale_quantity'] = int(w_quant_str) if w_quant_str else None if not product_data['name']: flash("Название товара обязательно.", 'error') return redirect(url_for('admin', env_id=env_id)) try: price = round(float(price_str), 2) if price < 0: price = 0 product_data['price'] = price except ValueError: flash("Неверный формат цены.", 'error') return redirect(url_for('admin', env_id=env_id)) variant_prices = {} for key, value in request.form.items(): if key.startswith('variant_price_') and value: variant_key = key.replace('variant_price_', '') try: variant_prices[variant_key] = round(float(value), 2) except ValueError: pass product_data['variant_prices'] = variant_prices photos_files = request.files.getlist('photos') if photos_files and any(f.filename for f in photos_files): if HF_TOKEN_WRITE: uploads_dir = 'uploads_temp' os.makedirs(uploads_dir, exist_ok=True) api = HfApi() new_photos_list = [] photo_limit = 10 uploaded_count = 0 for photo in photos_files: if uploaded_count >= photo_limit: flash(f"Загружено только первые {photo_limit} фото.", "warning") break if photo and photo.filename: try: ext = os.path.splitext(photo.filename)[1].lower() if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: flash(f"Файл {photo.filename} пропущен (не изображение).", "warning") continue safe_name = secure_filename(product_data['name'].replace(' ', '_'))[:50] photo_filename = f"{safe_name}_{uuid4().hex[:8]}{ext}" temp_path = os.path.join(uploads_dir, photo_filename) photo.save(temp_path) api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"photos/{photo_filename}", repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE) new_photos_list.append(photo_filename) os.remove(temp_path) uploaded_count += 1 except Exception as e: flash(f"Ошибка при загрузке фото {photo.filename}: {e}", 'error') if new_photos_list and is_edit and product_data.get('photos'): try: api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in product_data['photos']], repo_type="dataset", token=HF_TOKEN_WRITE) except Exception: pass if new_photos_list: product_data['photos'] = new_photos_list else: flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning") if is_edit: product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) if product_index != -1: products[product_index] = product_data flash(f"Товар '{product_data['name']}' успешно обновлен.", 'success') else: product_data['product_id'] = uuid4().hex products.append(product_data) flash(f"Товар '{product_data['name']}' успешно добавлен.", 'success') data['products'] = products save_env_data(env_id, data) elif action == 'delete_product': product_id = request.form.get('product_id') product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1) if product_index == -1: flash(f"Ошибка удаления: товар с ID '{product_id}' не найден.", 'error') return redirect(url_for('admin', env_id=env_id)) deleted_product = products.pop(product_index) product_name = deleted_product.get('name', 'N/A') photos_to_delete = deleted_product.get('photos', []) if photos_to_delete and HF_TOKEN_WRITE: try: api = HfApi() api.delete_files( repo_id=REPO_ID, paths_in_repo=[f"photos/{p}" for p in photos_to_delete], repo_type="dataset", token=HF_TOKEN_WRITE, commit_message=f"Delete photos for deleted product {product_name}" ) except Exception as e: flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning") elif photos_to_delete and not HF_TOKEN_WRITE: flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning") data['products'] = products save_env_data(env_id, data) flash(f"Товар '{product_name}' удален.", 'success') else: flash(f"Неизвестное действие: {action}", 'warning') return redirect(url_for('admin', env_id=env_id)) except Exception as e: flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error') return redirect(url_for('admin', env_id=env_id)) display_products = sorted(data.get('products', []), key=lambda p: p.get('name', '').lower()) display_categories = sorted(data.get('categories', [])) display_organization_info = data.get('organization_info', {}) display_chats = data.get('chats', {}) display_settings = data.get('settings', {}) chat_status = { "active": False, "expires_soon": False, "expires_date": "N/A" } if display_settings.get('chat_activated'): expires_str = display_settings.get('chat_activation_expires') if expires_str: try: expires_dt = datetime.fromisoformat(expires_str) now_almaty = datetime.now(ALMATY_TZ) if expires_dt > now_almaty: chat_status["active"] = True chat_status["expires_date"] = expires_dt.strftime('%Y-%m-%d %H:%M:%S') if (expires_dt - now_almaty).days <= 4: chat_status["expires_soon"] = True else: chat_status["expires_date"] = expires_dt.strftime('%Y-%m-%d %H:%M:%S') except (ValueError, TypeError): pass chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" return render_template_string( ADMIN_TEMPLATE, products=display_products, categories=display_categories, organization_info=display_organization_info, chats=display_chats, settings=display_settings, repo_id=REPO_ID, currency_code=display_settings.get('currency_code', 'KGS'), chat_avatar_url=chat_avatar_url, currencies=CURRENCIES, color_schemes=COLOR_SCHEMES, env_id=env_id, chat_status=chat_status, whatsapp_is_active=whatsapp_active.is_set(), whatsapp_env_id=whatsapp_env_id ) @app.route('/generate_description_ai', methods=['POST']) def handle_generate_description_ai(): request_data = request.get_json() base64_image = request_data.get('image') language = request_data.get('language', 'Русский') if not base64_image: return jsonify({"error": "Изображение не найдено в запросе."}), 400 try: image_data = base64.b64decode(base64_image) result_text = generate_ai_description_from_image(image_data, language) return jsonify({"text": result_text}) except ValueError as ve: return jsonify({"error": str(ve)}), 400 except Exception as e: return jsonify({"error": f"Внутренняя ошибка сервера: {e}"}), 500 @app.route('//chat_with_ai', methods=['POST']) def handle_chat_with_ai(env_id): if not is_chat_active(env_id): return jsonify({"error": "Чат неактивен."}), 403 request_data = request.get_json() user_message = request_data.get('message') chat_history_from_client = request_data.get('history', []) chat_id = request_data.get('chat_id') if not user_message: return jsonify({"error": "Сообщение не может быть пустым."}), 400 if not chat_id: return jsonify({"error": "ID чата не предоставлен."}), 400 try: ai_response_text = generate_chat_response(user_message, chat_history_from_client, env_id) data = get_env_data(env_id) if 'chats' not in data: data['chats'] = {} if chat_id not in data['chats']: data['chats'][chat_id] = [] timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') data['chats'][chat_id].append({'role': 'user', 'text': user_message, 'timestamp': timestamp}) data['chats'][chat_id].append({'role': 'ai', 'text': ai_response_text, 'timestamp': timestamp}) save_env_data(env_id, data) return jsonify({"text": ai_response_text}) except Exception as e: return jsonify({"error": f"Ошибка чата: {e}"}), 500 @app.route('//chat') def chat_page(env_id): if not is_chat_active(env_id): return "Чат для этой среды неактивен. Обратитесь к администратору.", 403 data = get_env_data(env_id) all_products_raw = data.get('products', []) settings = data.get('settings', {}) products_in_stock = [p for p in all_products_raw if p.get('in_stock', True)] products_sorted_for_js = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower())) chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" return render_template_string( CHAT_TEMPLATE, products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, currency_code=settings.get('currency_code', 'KGS'), settings=settings, chat_avatar_url=chat_avatar_url, env_id=env_id ) @app.route('//get_chat/') def get_chat_history(env_id, chat_id): data = get_env_data(env_id) chat_history = data.get('chats', {}).get(chat_id) if chat_history: return jsonify(chat_history) else: return jsonify({"error": "Chat not found"}), 404 @app.route('//force_upload', methods=['POST']) def force_upload(env_id): try: upload_db_to_hf() flash("Данные успешно загружены на Hugging Face.", 'success') except Exception as e: flash(f"Ошибка при загрузке на Hugging Face: {e}", 'error') return redirect(url_for('admin', env_id=env_id)) @app.route('//force_download', methods=['POST']) def force_download(env_id): try: if download_db_from_hf(): flash("Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.", 'success') else: flash("Не удалось скачать данные с Hugging Face после нескольких попыток. Проверьте логи.", 'error') except Exception as e: flash(f"Ошибка при скачивании с Hugging Face: {e}", 'error') return redirect(url_for('admin', env_id=env_id)) @app.route('//whatsapp/start') def start_whatsapp_bot(env_id): global whatsapp_thread if whatsapp_thread and whatsapp_thread.is_alive(): flash('WhatsApp бот уже запущен.', 'warning') else: whatsapp_thread = threading.Thread(target=whatsapp_bot_thread, args=(env_id,), daemon=True) whatsapp_thread.start() flash('Запуск WhatsApp бота... Пожалуйста, будьте готовы отсканировать QR-код в новом окне браузера.', 'success') return redirect(url_for('admin', env_id=env_id)) @app.route('//whatsapp/stop') def stop_whatsapp_bot(env_id): global driver, whatsapp_thread if not (whatsapp_thread and whatsapp_thread.is_alive()): flash('WhatsApp бот не был запущен.', 'warning') else: whatsapp_active.clear() if driver: try: driver.quit() except: pass flash('WhatsApp бот остановлен.', 'success') return redirect(url_for('admin', env_id=env_id)) if __name__ == '__main__': configure_gemini() download_db_from_hf() load_data() if HF_TOKEN_WRITE: backup_thread = threading.Thread(target=periodic_backup, daemon=True) backup_thread.start() else: pass port = int(os.environ.get('PORT', 7860)) app.run(debug=False, host='0.0.0.0', port=port)