diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3 +1,4 @@ + import os import io import base64 @@ -8,7 +9,7 @@ import time from datetime import datetime from uuid import uuid4 import random -import shutil +import string from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify, Response from PIL import Image @@ -23,9 +24,10 @@ import requests load_dotenv() app = Flask(__name__) -app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login_multienv' -ENVIRONMENTS_FILE = 'environments.json' -DATA_DIR = 'data' +app.secret_key = 'your_unique_secret_key_gippo_312_shop_54321_no_login' +DATA_FILE = 'data.json' + +SYNC_FILES = [DATA_FILE] REPO_ID = "Kgshop/metastorebase1" HF_TOKEN_WRITE = os.getenv("HF_TOKEN") @@ -43,33 +45,20 @@ DOWNLOAD_DELAY = 5 logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -def get_env_file_path(env_id): - return os.path.join(DATA_DIR, f"{env_id}.json") - 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 [ENVIRONMENTS_FILE] + files_to_download = [specific_file] if specific_file else SYNC_FILES all_successful = True - if not specific_file: - try: - with open(ENVIRONMENTS_FILE, 'r', encoding='utf-8') as f: - environments = json.load(f) - for env_id in environments: - files_to_download.append(get_env_file_path(env_id).replace('\\', '/')) - except (FileNotFoundError, json.JSONDecodeError): - pass - - os.makedirs(DATA_DIR, exist_ok=True) - for file_name in files_to_download: success = False for attempt in range(retries + 1): try: - hf_hub_download( + local_path = hf_hub_download( repo_id=REPO_ID, filename=file_name, repo_type="dataset", @@ -87,166 +76,285 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN if e.response.status_code == 404: if attempt == 0 and not os.path.exists(file_name): try: - if file_name == ENVIRONMENTS_FILE: - with open(file_name, 'w', encoding='utf-8') as f: json.dump([], f) - elif file_name.startswith(DATA_DIR): - with open(file_name, 'w', encoding='utf-8') as f: json.dump({'products': [], 'categories': [], 'orders': {}, 'organization_info': {}, 'chats': {}}, f) - except Exception: pass - success = True + 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 - except Exception: pass - if attempt < retries: time.sleep(delay) - if not success: all_successful = False + 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_to_hf(specific_file): - if not HF_TOKEN_WRITE: return +def upload_db_to_hf(specific_file=None): + if not HF_TOKEN_WRITE: + return try: api = HfApi() - if os.path.exists(specific_file): - api.upload_file( - path_or_fileobj=specific_file, - path_in_repo=specific_file.replace('\\', '/'), - repo_id=REPO_ID, - repo_type="dataset", - token=HF_TOKEN_WRITE, - commit_message=f"Sync {specific_file} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - ) - except Exception: pass + 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) - try: - with open(ENVIRONMENTS_FILE, 'r', encoding='utf-8') as f: environments = json.load(f) - upload_to_hf(ENVIRONMENTS_FILE) - for env_id in environments: upload_to_hf(get_env_file_path(env_id)) - except (FileNotFoundError, json.JSONDecodeError): pass + 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 load_data(env_id): - file_path = get_env_file_path(env_id) +def get_env_data(env_id): + all_data = load_data() default_organization_info = { "about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров. Мы предлагаем широкий ассортимент продукции, от электроники до товаров для дома, всегда стремясь к качеству и доступности. Наша миссия — сделать ваш шопинг приятным и удобным, предлагая только лучшие товары, тщательно отобранные для вас.", "shipping": "Доставка осуществляется по всему Кыргызстану. Стоимость и сроки доставки зависят от региона и веса товара. По Бишкеку доставка возможна в течение 1-2 рабочих дней, в регионы — от 3 до 7 дней. Для уточнения деталей свяжитесь с нами.", "returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.", "contact": f"Наш магазин находится по адресу: {STORE_ADDRESS}. Связаться с нами можно по телефону: {WHATSAPP_NUMBER} или через WhatsApp по этому же номеру. Мы работаем ежедневно с 9:00 до 18:00." } - default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_organization_info, 'chats': {}} - - def read_and_validate(path): - with open(path, 'r', encoding='utf-8') as f: data = json.load(f) - if not isinstance(data, dict): return default_data - data.setdefault('products', []) - data.setdefault('categories', []) - data.setdefault('orders', {}) - data.setdefault('organization_info', default_organization_info) - data.setdefault('chats', {}) - return data + env_data = all_data.get(env_id, { + 'products': [], + 'categories': [], + 'orders': {}, + 'organization_info': default_organization_info, + 'chats': {} + }) - try: - data = read_and_validate(file_path) - except (FileNotFoundError, json.JSONDecodeError): - if download_db_from_hf(specific_file=file_path.replace('\\', '/')): - try: data = read_and_validate(file_path) - except Exception: data = default_data - else: data = default_data + 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 not os.path.exists(file_path): - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(default_data, f, ensure_ascii=False, indent=4) + products_changed = False + for product in env_data['products']: + if 'product_id' not in product: + product['product_id'] = uuid4().hex + products_changed = True - return data + if products_changed: + save_env_data(env_id, env_data) -def save_data(data, env_id): - file_path = get_env_file_path(env_id) - os.makedirs(DATA_DIR, exist_ok=True) - try: - if not isinstance(data, dict): return - data.setdefault('products', []) - data.setdefault('categories', []) - data.setdefault('orders', {}) - data.setdefault('organization_info', {}) - data.setdefault('chats', {}) - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=4) - upload_to_hf(file_path) - except Exception: pass + 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 + if not GOOGLE_API_KEY: + return False try: genai.configure(api_key=GOOGLE_API_KEY) return True - except Exception: return False + except Exception as e: + return False def generate_ai_description_from_image(image_data, language): - if not configure_gemini(): raise ValueError("Google AI API не настроен.") + if not configure_gemini(): + raise ValueError("Google AI API не настроен.") + try: - image = Image.open(io.BytesIO(image_data)).convert('RGB') - except Exception: raise ValueError("Не удалось обработать изображение.") + 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_map = {"Русский": " Пиши на русском языке.", "Кыргызский": " Пиши на кыргызском языке.", "Казахский": " Пиши на казахском языке.", "Узбекский": " Пиши на узбекском языке."} - final_prompt = f"{base_prompt}{lang_map.get(language, '')}" + + 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 - if response.parts: return "".join(part.text for part in response.parts if hasattr(part, 'text')) - response.resolve() - return response.text + + 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("Модель AI не найдена или недоступна.") - elif "resource has been exhausted" in str(e).lower(): raise ValueError("Квота запросов исчерпана.") - elif "content has been blocked" in str(e).lower(): raise ValueError("Генерация контента заблокирована из-за политики безопасности.") - else: raise ValueError(f"Ошибка при генерации контента: {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 configure_gemini(): return "Извините, сервис чата временно недоступен." - data = load_data(env_id) + 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', {}) - product_info_list = [f"- [ID_ТОВАРА: {p.get('product_id', 'N/A')} Название: {p.get('name', 'Без названия')}], Категория: {p.get('category', 'Без категории')}, Цена: {p.get('price', 0):.0f} {CURRENCY_CODE}, Описание: {p.get('description', '')[:100]}..." for p in products if p.get('in_stock', True)] + + product_info_list = [] + for p in products: + if p.get('in_stock', True): + price_display = f"{p.get('price', 0):.2f}".replace('.00', '') + product_info_list.append(f"- [ID_ТОВАРА: {p.get('product_id', 'N/A')} Название: {p.get('name', 'Без названия')}], Категория: {p.get('category', 'Без категории')}, Цена: {price_display} {CURRENCY_CODE}, Описание: {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 = "\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" + + 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" + + system_instruction_content = ( "Ты - доброжелательный и очень полезный виртуальный консультант для магазина Gippo312. " "Твоя задача - помогать пользователям находить товары, отвечать на вопросы о них, предлагать варианты, а также предоставлять информацию о магазине. " "Всегда будь вежлив, информативен и стремись решить проблему пользователя. " "Никогда не выдумывай товары или категории, которых нет в предоставленных списках. " "Когда ты предлагаешь товар, всегда указывай его название и ID, и��пользуя *точный формат*: [ID_ТОВАРА: Название: ]. Это *очень важно* для клиента. " - f"Список доступных категорий: {category_list_str}.\n" - f"Список доступных товаров в магазине:\n{product_list_str}{org_info_str}\n" - "Если пользователь спрашивает про товары или категории, которых нет в списках, вежливо сообщи, что таких товаров/категорий нет и предложи что-то из имеющихся. " - "Если вопрос касается общей информации о магазине, используй данные из блока 'Информация о магазине'. " - "Старайся быть кратким, но информативным. Используй эмодзи для дружелюбности." + "Если пользователь ищет товар или категорию, предлагай несколько наиболее подходящих вариантов или перечисляй доступные из этой категории.\n\n" + f"Список доступных категорий: {category_list_str}.\n\n" + f"Список доступных товаров в магазине:\n" + f"{product_list_str}" + f"{org_info_str}\n\n" + "Если пользователь спрашивает про товары или категории, которых нет в списках, вежливо сообщи, что таких товаров/категорий нет и предложи что-то из имеющихся, или перечисли доступные категории. " + "Если вопрос касается общей информации о магазине (например, 'о нас', 'доставка', 'возврат', 'контакты'), используй данные из блока 'Информация о магазине'. " + "Старайся быть кратким, но информативным. Используй эмодзи для дружелюбности. " + "Избегай упоминания Hugging Face или Hugging Face Hub." ) + + generated_text = "" + response = None + try: model = genai.GenerativeModel('gemma-3-27b-it') - model_chat_history = [{'role': 'user', 'parts': [{'text': system_instruction_content}]}] + + model_chat_history_for_gemini = [ + {'role': 'user', 'parts': [{'text': system_instruction_content}]} + ] for entry in chat_history_from_client: - model_chat_history.append({'role': 'model' if entry['role'] == 'ai' else 'user', 'parts': [{'text': entry['text']}]}) - chat = model.start_chat(history=model_chat_history) + 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'): return response.text - if response.parts: return "".join(part.text for part in response.parts if hasattr(part, 'text')) - response.resolve() - return response.text + + 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(): return "Извините, Ваш запрос был заблокирован из-за политики безопасности." - else: return f"Извините, произошла ошибка: {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}" ADMHOSTO_TEMPLATE = ''' @@ -254,35 +362,40 @@ ADMHOSTO_TEMPLATE = ''' - Главная панель управления - + Главная Админ-панель +
-

Главная панель управления

+

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

+ {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} @@ -290,33 +403,34 @@ ADMHOSTO_TEMPLATE = ''' {% endfor %} {% endif %} {% endwith %} -
-
- - + +
+ +
-

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

- {% if environments %} + +
+

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

+ {% if environments %} - {% else %} -

Сред пока не создано. Нажмите кнопку выше, чтобы создать первую.

- {% endif %} + {% else %} +

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

+ {% endif %} +
@@ -333,50 +447,243 @@ CATALOG_TEMPLATE = '''
-
@@ -384,210 +691,446 @@ CATALOG_TEMPLATE = '''
+
{% set has_products = False %} {% for category_name in ordered_categories %} {% if products_by_category[category_name] %} {% set has_products = True %}
-

{{ category_name }}

+
+

{{ category_name }}

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

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

{% endif %} + + {% if not has_products %} +

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

+ {% endif %}
- - - + + + + + + +
- - + + + +
+
+ @@ -603,32 +1146,162 @@ CHAT_TEMPLATE = ''' Gippo312 - Чат с EVA + @@ -644,27 +1317,238 @@ CHAT_TEMPLATE = '''
+ +
+ +
+ + + + +
+ {% else %} -

Ошибка

Заказ не найден.

+

Ошибка

+

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

+ ← Вернуться в каталог {% endif %} @@ -843,264 +1862,1087 @@ ADMIN_TEMPLATE = ''' Админ-панель - Gippo312 - +
-

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

Каталог Назад
- {% with messages = get_flashed_messages(with_categories=true) %}{% if messages %}{% for category, message in messages %}
{{ message }}
{% endfor %}{% endif %}{% endwith %} -

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

Добавить новый товар
-

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

-
{% for product in products %}
{% if product.get('photos') %}{% endif %}

{{ product['name'] }}

Цена: {{ product.price }} {{ currency_code }}

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

-
{% endfor %}
+
+
+ Gippo312 Logo +

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

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

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

+
+
+ +
+
+ +
+
+

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

+
+ +
+

Диалоги с EVA

+
+ {% 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 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', 'Без категории') }}

+

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

+

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

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

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

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

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

+ {% endif %} +
+
+ +
+ +
+ + + +
+
+ +
+

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

+
+ + + + + + + + + {% 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 %} +
+ +
+
+ + +
+
+ + +
+
+ +
+
+
+ {% endfor %} +
+ {% else %} +

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

+ {% endif %} +
+ + ''' @app.route('/') -def root(): +def index(): return redirect(url_for('admhosto')) -@app.route('/admhosto', methods=['GET', 'POST']) +@app.route('/admhosto', methods=['GET']) def admhosto(): - try: - with open(ENVIRONMENTS_FILE, 'r', encoding='utf-8') as f: - environments = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - environments = [] + data = load_data() + environments = sorted(data.keys()) + return render_template_string(ADMHOSTO_TEMPLATE, environments=environments) - if request.method == 'POST': - action = request.form.get('action') - if action == 'create_env': - new_id = str(random.randint(100000, 999999)) - while new_id in environments: - new_id = str(random.randint(100000, 999999)) - environments.append(new_id) - with open(ENVIRONMENTS_FILE, 'w', encoding='utf-8') as f: - json.dump(environments, f) - upload_to_hf(ENVIRONMENTS_FILE) - load_data(new_id) # Creates the default file - flash(f"Новая среда с ID {new_id} успешно создана.", 'success') - elif action == 'delete_env': - env_id_to_delete = request.form.get('env_id') - if env_id_to_delete in environments: - environments.remove(env_id_to_delete) - with open(ENVIRONMENTS_FILE, 'w', encoding='utf-8') as f: - json.dump(environments, f) - upload_to_hf(ENVIRONMENTS_FILE) - - env_file_path = get_env_file_path(env_id_to_delete) - if os.path.exists(env_file_path): - os.remove(env_file_path) - - if HF_TOKEN_WRITE: - try: - api = HfApi() - api.delete_folder(repo_id=REPO_ID, folder_path=f"data", repo_type="dataset", token=HF_TOKEN_WRITE) - api.delete_folder(repo_id=REPO_ID, folder_path=f"photos/{env_id_to_delete}", repo_type="dataset", token=HF_TOKEN_WRITE) - except Exception as e: - flash(f"Не удалось удалить папки среды {env_id_to_delete} с сервера: {e}", 'warning') +@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": f"Наш магазин находится по адресу: {STORE_ADDRESS}. Связаться с нами можно по телефону: {WHATSAPP_NUMBER}." + }, + 'chats': {} + } + save_data(all_data) + flash(f'Новая среда с ID {new_id} успешно создана.', 'success') + return redirect(url_for('admhosto')) - flash(f"Среда с ID {env_id_to_delete} удалена.", 'success') +@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')) - return redirect(url_for('admhosto')) +@app.route('//catalog') +def catalog(env_id): + data = get_env_data(env_id) + all_products_raw = data.get('products', []) + + 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))) - return render_template_string(ADMHOSTO_TEMPLATE, environments=environments) + 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())) -@app.route('/catalog/') -def catalog(env_id): - data = load_data(env_id) - products_in_stock = [p for p in data.get('products', []) 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())) + 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())) - categories = sorted(list(set(p.get('category', 'Без категории') for p in products_in_stock).union(data.get('categories', [])))) - products_by_category = {cat: [p for p in products_sorted if p.get('category', 'Без категории') == cat] for cat in categories} - ordered_categories = [cat for cat in categories if products_by_category.get(cat)] + ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)] return render_template_string( CATALOG_TEMPLATE, products_by_category=products_by_category, ordered_categories=ordered_categories, - products_json=json.dumps(products_sorted), + products_json=json.dumps(products_sorted_for_js), repo_id=REPO_ID, - env_id=env_id, - currency_code=CURRENCY_CODE + store_address=STORE_ADDRESS, + currency_code=CURRENCY_CODE, + env_id=env_id ) -@app.route('/product//') +@app.route('//product/') def product_detail(env_id, index): - data = load_data(env_id) - products_in_stock = [p for p in data.get('products', []) if p.get('in_stock', True)] + data = get_env_data(env_id) + all_products_raw = data.get('products', []) + 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, env_id=env_id, currency_code=CURRENCY_CODE) -@app.route('/create_order/', methods=['POST']) + try: + product = products_sorted[index] + except IndexError: + return "Товар не найден или отсутствует в наличии.", 404 + + return render_template_string( + PRODUCT_DETAIL_TEMPLATE, + product=product, + repo_id=REPO_ID, + currency_code=CURRENCY_CODE, + 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: return jsonify({"error": "Корзина пуста."}), 400 - total_price = sum(float(item['price']) * int(item['quantity']) for item in order_data['cart']) + + 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'), + "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": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - "cart": [{ - **item, - "photo_url": f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/photos/{env_id}/{item['photo']}" if item.get('photo') else "" - } for item in order_data['cart']], - "total_price": round(total_price, 2) + "created_at": order_timestamp, + "cart": processed_cart, + "total_price": round(total_price, 2), + "user_info": None, + "status": "new" } - data = load_data(env_id) - data['orders'][order_id] = new_order - save_data(data, env_id) - return jsonify({"order_id": order_id}), 201 -@app.route('/order//') + 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 = load_data(env_id) + data = get_env_data(env_id) order = data.get('orders', {}).get(order_id) - return render_template_string(ORDER_TEMPLATE, order=order, repo_id=REPO_ID, env_id=env_id, currency_code=CURRENCY_CODE, whatsapp_number=WHATSAPP_NUMBER) -@app.route('/admin/', methods=['GET', 'POST']) + return render_template_string(ORDER_TEMPLATE, + order=order, + repo_id=REPO_ID, + currency_code=CURRENCY_CODE, + whatsapp_number=WHATSAPP_NUMBER, + env_id=env_id) + + +@app.route('//admin', methods=['GET', 'POST']) def admin(env_id): - data = load_data(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', {}) + + if 'orders' not in data or not isinstance(data.get('orders'), dict): + data['orders'] = {} + if request.method == 'POST': action = request.form.get('action') - if action == 'add_product': - name = request.form.get('name', '').strip() - price = round(float(request.form.get('price', '0').replace(',', '.')), 2) - if not name or price <= 0: - flash("Название и цена обязательны.", 'error') - return redirect(url_for('admin', env_id=env_id)) - - photos_list = [] - if HF_TOKEN_WRITE: - api = HfApi() - for photo in request.files.getlist('photos'): - if photo and photo.filename: - safe_name = secure_filename(name)[:50] - photo_filename = f"{safe_name}_{uuid4().hex[:8]}{os.path.splitext(photo.filename)[1]}" - api.upload_file( - path_or_fileobj=photo.stream, - path_in_repo=f"photos/{env_id}/{photo_filename}", - repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE - ) - photos_list.append(photo_filename) - - new_product = { - 'product_id': uuid4().hex, 'name': name, 'price': price, - 'description': request.form.get('description', ''), - 'category': request.form.get('category', 'Без категории'), - 'photos': photos_list, 'colors': [c for c in request.form.getlist('colors') if c], - 'in_stock': 'in_stock' in request.form, 'is_top': 'is_top' in request.form - } - data['products'].append(new_product) - save_data(data, env_id) - flash(f"Товар '{name}' добавлен.", 'success') - - elif action == 'delete_product': - product_id = request.form.get('product_id') - product_to_delete = next((p for p in data['products'] if p.get('product_id') == product_id), None) - if product_to_delete: - data['products'] = [p for p in data['products'] if p.get('product_id') != product_id] - if HF_TOKEN_WRITE and product_to_delete.get('photos'): + + 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 == 'add_product': + name = request.form.get('name', '').strip() + price_str = request.form.get('price', '').replace(',', '.') + description = request.form.get('description', '').strip() + category = request.form.get('category') + photos_files = request.files.getlist('photos') + colors = [c.strip() for c in request.form.getlist('colors') if c.strip()] + in_stock = 'in_stock' in request.form + is_top = 'is_top' in request.form + + if not name or not price_str: + flash("Название и цена товара обязательны.", 'error') + return redirect(url_for('admin', env_id=env_id)) + + try: + price = round(float(price_str), 2) + if price < 0: price = 0 + except ValueError: + flash("Неверный формат цены.", 'error') + return redirect(url_for('admin', env_id=env_id)) + + photos_list = [] + if photos_files and HF_TOKEN_WRITE: + uploads_dir = 'uploads_temp' + os.makedirs(uploads_dir, exist_ok=True) api = HfApi() - api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"photos/{env_id}/{p}" for p in product_to_delete['photos']], repo_type="dataset", token=HF_TOKEN_WRITE, not_found_ok=True) - save_data(data, env_id) - flash(f"Товар '{product_to_delete['name']}' удален.", 'success') - - return redirect(url_for('admin', env_id=env_id)) + 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(name.replace(' ', '_'))[:50] + photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{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, + commit_message=f"Add photo for product {name}" + ) + photos_list.append(photo_filename) + os.remove(temp_path) + uploaded_count += 1 + except Exception as e: + flash(f"Ошибка при загрузке фото {photo.filename}.", 'error') + if os.path.exists(temp_path): + try: os.remove(temp_path) + except OSError: pass + elif photo and not photo.filename: + pass + try: + if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): + os.rmdir(uploads_dir) + except OSError as e: + pass + elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files): + flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning") + + + new_product = { + 'product_id': uuid4().hex, + 'name': name, 'price': price, 'description': description, + 'category': category if category in categories else 'Без категории', + 'photos': photos_list, 'colors': colors, + 'in_stock': in_stock, 'is_top': is_top + } + products.append(new_product) + data['products'] = products + save_env_data(env_id, data) + flash(f"Товар '{name}' успешно добавлен.", 'success') + + elif action == 'edit_product': + product_id = request.form.get('product_id') + product_to_edit = next((p for p in products if p.get('product_id') == product_id), None) + + if product_to_edit is None: + flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error') + return redirect(url_for('admin', env_id=env_id)) + + original_name = product_to_edit.get('name', 'N/A') + + product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip() + price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.') + product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip() + category = request.form.get('category') + product_to_edit['category'] = category if category in categories else 'Без категории' + product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()] + product_to_edit['in_stock'] = 'in_stock' in request.form + product_to_edit['is_top'] = 'is_top' in request.form + + try: + price = round(float(price_str), 2) + if price < 0: price = 0 + product_to_edit['price'] = price + except ValueError: + flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning') + + photos_files = request.files.getlist('photos') + if photos_files and any(f.filename for f in photos_files) and 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_to_edit['name'].replace(' ', '_'))[:50] + photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{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, + commit_message=f"Update photo for product {product_to_edit['name']}") + new_photos_list.append(photo_filename) + os.remove(temp_path) + uploaded_count += 1 + except Exception as e: + flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error') + if os.path.exists(temp_path): + try: os.remove(temp_path) + except OSError: pass + try: + if os.path.exists(uploads_dir) and not os.listdir(uploads_dir): + os.rmdir(uploads_dir) + except OSError as e: + pass + + if new_photos_list: + old_photos = product_to_edit.get('photos', []) + if old_photos: + try: + api = HfApi() + api.delete_files( + repo_id=REPO_ID, + paths_in_repo=[f"photos/{p}" for p in old_photos], + repo_type="dataset", + token=HF_TOKEN_WRITE, + commit_message=f"Delete old photos for product {product_to_edit['name']}" + ) + except Exception as e: + flash("Не удалось удалить старые фотографии с сервера. Новые фото загружены.", "warning") + product_to_edit['photos'] = new_photos_list + flash("Фотографии товара успешно обновлены.", "success") + elif uploaded_count == 0 and any(f.filename for f in photos_files): + flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error") + elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files): + flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning") + + save_env_data(env_id, data) + flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success') + + 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', {}) return render_template_string( ADMIN_TEMPLATE, - products=sorted(data.get('products', []), key=lambda p: p.get('name', '').lower()), - categories=sorted(data.get('categories', [])), - organization_info=data.get('organization_info', {}), - chats=data.get('chats', {}), + products=display_products, + categories=display_categories, + organization_info=display_organization_info, + chats=display_chats, repo_id=REPO_ID, - env_id=env_id, - currency_code=CURRENCY_CODE + currency_code=CURRENCY_CODE, + env_id=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(request_data['image']) - result_text = generate_ai_description_from_image(image_data, request_data.get('language', 'Русский')) + 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 + 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']) +@app.route('//chat_with_ai', methods=['POST']) def handle_chat_with_ai(env_id): 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 or not chat_id: return jsonify({"error": "Неверный запрос."}), 400 + + 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, request_data.get('history', []), env_id) - data = load_data(env_id) - data.setdefault('chats', {}).setdefault(chat_id, []).extend([ - {'role': 'user', 'text': user_message, 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')}, - {'role': 'ai', 'text': ai_response_text, 'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - ]) - save_data(data, env_id) + 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 + except Exception as e: + return jsonify({"error": f"Ошибка чата: {e}"}), 500 -@app.route('/chat/') +@app.route('//chat') def chat_page(env_id): - data = load_data(env_id) - products_in_stock = [p for p in data.get('products', []) 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())) - return render_template_string(CHAT_TEMPLATE, products_json=json.dumps(products_sorted), repo_id=REPO_ID, env_id=env_id, currency_code=CURRENCY_CODE) + data = get_env_data(env_id) + all_products_raw = data.get('products', []) + 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())) -@app.route('/get_chat//') + return render_template_string( + CHAT_TEMPLATE, + products_json=json.dumps(products_sorted_for_js), + repo_id=REPO_ID, + currency_code=CURRENCY_CODE, + env_id=env_id + ) + +@app.route('//get_chat/') def get_chat_history(env_id, chat_id): - data = load_data(env_id) + data = get_env_data(env_id) chat_history = data.get('chats', {}).get(chat_id) - return jsonify(chat_history) if chat_history else (jsonify({"error": "Chat not found"}), 404) + 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)) if __name__ == '__main__': configure_gemini() - os.makedirs(DATA_DIR, exist_ok=True) download_db_from_hf() + load_data() if HF_TOKEN_WRITE: - threading.Thread(target=periodic_backup, daemon=True).start() + 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) \ No newline at end of file + app.run(debug=False, host='0.0.0.0', port=port)