0 / app.py
Kgshop's picture
Rename Soola.txt to app.py
0c234ae verified
import os
import io
import base64
import json
import logging
import threading
import time
from datetime import datetime
from uuid import uuid4
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
load_dotenv()
app = Flask(__name__)
app.secret_key = 'metastore_marketplace_secret_key_199823'
DATA_FILE = 'data_metastore.json'
SYNC_FILES = [DATA_FILE]
REPO_ID = "Kgshop/metastorebase"
HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
APP_NAME = "METASTORE"
APP_TAGLINE = "маркетплейс внутри instagram"
DOWNLOAD_RETRIES = 3
DOWNLOAD_DELAY = 5
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
translations = {
'ru': {
'title': f'{APP_NAME} - {APP_TAGLINE}',
'search_placeholder': 'Найти магазины...',
'no_stores_added': 'Магазины пока не добавлены.',
'no_results_found': 'По вашему запросу ничего не найдено.',
'loading': 'Загрузка...',
'error_loading_details': 'Не удалось загрузить информацию о магазине.',
'category': 'Категория',
'no_category': 'Без категории',
'description': 'Описание',
'no_description': 'Описание отсутствует.',
'country': 'Страна',
'city': 'Город',
'no_location_data': 'Нет данных о местоположении',
'go_to_store': 'Перейти в магазин',
'chat_with_eva': 'Чат с EVA',
'chat_placeholder': 'Напишите сообщение...',
'clear_chat_history': 'Очистить чат',
'confirm_clear_chat': 'Вы уверены, что хотите очистить историю чата?',
'chat_history_cleared': 'История чата очищена.',
'admin_panel_title': f'Админ-панель - {APP_NAME}',
'go_to_catalog': 'Перейти в каталог',
'sync_with_dc': 'Синхронизация с Датацентром',
'upload_db': 'Загрузить БД',
'download_db': 'Скачать БД',
'sync_info': 'Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.',
'confirm_force_upload': 'Вы уверены, что хотите принудительно загрузить локальные данные на сервер? Это перезапишет данные на сервере.',
'confirm_force_download': 'Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.',
'category_management': 'Управление категориями',
'add_new_category': 'Добавить новую категорию',
'new_category_name': 'Название новой категории:',
'add': 'Добавить',
'existing_categories': 'Существующие категории:',
'confirm_delete_category': "Вы уверены, что хотите удалить категорию '{}'? Магазины этой категории будут помечены как 'Без категории'.",
'no_categories_yet': 'Категорий пока нет.',
'store_info_management': 'Информация о платформе',
'expand_collapse': 'Развернуть/Свернуть',
'about_us': 'О нас (для ИИ-ассистента):',
'platform_info_saved': 'Информация о платформе успешно обновлена.',
'platform_info_ai_hint': 'Эта информация будет использоваться ИИ-ассистентом для ответов на вопросы о платформе METASTORE.',
'store_management': 'Управление магазинами',
'add_new_store': 'Добавить новый магазин',
'store_name': 'Название магазина *:',
'store_link': 'Ссылка (Instagram, WhatsApp, сайт) *:',
'logo': 'Логотип (1 фото):',
'generate_description': 'Сгенерировать описание',
'generation_language': 'Язык генерации:',
'country_select': 'Страна *:',
'city_select': 'Город *:',
'is_top_store': 'Топ магазин (показывать наверху)',
'add_store': 'Добавить магазин',
'store_list': 'Список магазинов:',
'edit': 'Редактировать',
'delete': 'Удалить',
'confirm_delete_store': "Вы уверены, что хотите удалить магазин '{}'?",
'editing': 'Редактирование:',
'replace_logo': 'Заменить логотип (выберите новый файл):',
'current_logo': 'Текущий логотип:',
'save_changes': 'Сохранить изменения',
'no_stores_yet': 'Магазинов пока нет.',
'category_added': "Категория '{}' успешно добавлена.",
'category_empty': 'Название категории не может быть пустым.',
'category_exists': "Категория '{}' уже существует.",
'category_deleted': "Категория '{}' удалена. {} магазинов обновлено.",
'category_delete_fail': "Не удалось удалить категорию '{}'.",
'store_added': "Магазин '{}' успешно добавлен.",
'store_name_link_required': 'Название магазина и ссылка обязательны.',
'invalid_link_format': 'Неверный формат ссылки. Убедитесь, что она начинается с http:// или https://',
'hf_token_missing_logo': 'HF_TOKEN (write) не настроен. Логотип не был загружен.',
'logo_upload_error': "Ошибка при загрузке логотипа {}.",
'logo_not_image': "Файл {} не является изображением и был пропущен.",
'store_edit_error': "Ошибка редактирования: магазин с ID '{}' не найден.",
'store_updated': "Магазин '{}' успешно обновлен.",
'logo_updated': 'Логотип магазина успешно обновлен.',
'old_logo_delete_fail': 'Не удалось удалить старый логотип с сервера. Новый логотип загружен.',
'new_logo_upload_fail': 'Не удалось загрузить новый логотип.',
'hf_token_missing_logo_update': 'HF_TOKEN (write) не настроен. Логотип не был обновлен.',
'store_delete_error': "Ошибка удаления: магазин с ID '{}' не найден.",
'store_deleted': "Магазин '{}' удален.",
'old_logo_delete_fail_on_store_delete': "Не удалось удалить логотип для магазина '{}' с сервера. Магазин удален локально.",
'store_deleted_no_token': "Магазин '{}' удален локально, но логотип не удален с сервера (токен не задан).",
'unknown_action': "Неизвестное действие: {}",
'internal_error': "Произошла внутренняя ошибка при выполнении действия '{}'. Подробности в логе сервера.",
'force_upload_success': 'Данные успешно загружены на Hugging Face.',
'force_upload_error': 'Ошибка при загрузке на Hugging Face: {}',
'force_download_success': 'Данные успешно скачаны с Hugging Face. Локальные файлы обновлены.',
'force_download_error': 'Не удалось скачать данные с Hugging Face. Проверьте логи.',
'force_download_exception': 'Ошибка при скачивании с Hugging Face: {}',
'error': 'Ошибка',
'success': 'Успешно',
'warning': 'Внимание',
'close': 'Закрыть',
'ai_error_no_image': 'Пожалуйста, сначала загрузите логотип магазина.',
'ai_error_not_image': 'Выбранный файл не является изображением.',
'ai_error_form_elements': 'Ошибка: Не найдены элементы формы для генерации.',
'ai_generating': 'Генерация...',
'ai_generating_placeholder': 'Генерация описания, пожалуйста подождите...',
'ai_generation_error': 'Ошибка генерации: {}',
'ai_file_read_error': 'Ошибка чтения файла изображения.',
},
'en': {
'title': f'{APP_NAME} - {APP_TAGLINE}',
'search_placeholder': 'Find stores...',
'no_stores_added': 'No stores have been added yet.',
'no_results_found': 'No results found for your query.',
'loading': 'Loading...',
'error_loading_details': 'Failed to load store information.',
'category': 'Category',
'no_category': 'Uncategorized',
'description': 'Description',
'no_description': 'No description available.',
'country': 'Country',
'city': 'City',
'no_location_data': 'No location data',
'go_to_store': 'Go to Store',
'chat_with_eva': 'Chat with EVA',
'chat_placeholder': 'Write a message...',
'clear_chat_history': 'Clear Chat History',
'confirm_clear_chat': 'Are you sure you want to clear the chat history?',
'chat_history_cleared': 'Chat history cleared.',
'admin_panel_title': f'Admin Panel - {APP_NAME}',
'go_to_catalog': 'Go to Catalog',
'sync_with_dc': 'Synchronization with Datacenter',
'upload_db': 'Upload DB',
'download_db': 'Download DB',
'sync_info': 'Backup occurs automatically every 30 minutes, and also after each data save. Use these buttons for immediate synchronization.',
'confirm_force_upload': 'Are you sure you want to force upload local data to the server? This will overwrite the data on the server.',
'confirm_force_download': 'Are you sure you want to force download data from the server? This will overwrite your local files.',
'category_management': 'Category Management',
'add_new_category': 'Add New Category',
'new_category_name': 'New category name:',
'add': 'Add',
'existing_categories': 'Existing Categories:',
'confirm_delete_category': "Are you sure you want to delete the category '{}'? Stores in this category will be marked as 'Uncategorized'.",
'no_categories_yet': 'No categories yet.',
'store_info_management': 'Platform Information',
'expand_collapse': 'Expand/Collapse',
'about_us': 'About Us (for AI assistant):',
'platform_info_saved': 'Platform information updated successfully.',
'platform_info_ai_hint': 'This information will be used by the AI assistant to answer questions about the METASTORE platform.',
'store_management': 'Store Management',
'add_new_store': 'Add New Store',
'store_name': 'Store Name *:',
'store_link': 'Link (Instagram, WhatsApp, website) *:',
'logo': 'Logo (1 photo):',
'generate_description': 'Generate Description',
'generation_language': 'Generation Language:',
'country_select': 'Country *:',
'city_select': 'City *:',
'is_top_store': 'Top store (show at the top)',
'add_store': 'Add Store',
'store_list': 'List of Stores:',
'edit': 'Edit',
'delete': 'Delete',
'confirm_delete_store': "Are you sure you want to delete the store '{}'?",
'editing': 'Editing:',
'replace_logo': 'Replace logo (select a new file):',
'current_logo': 'Current logo:',
'save_changes': 'Save Changes',
'no_stores_yet': 'No stores yet.',
'category_added': "Category '{}' added successfully.",
'category_empty': 'Category name cannot be empty.',
'category_exists': "Category '{}' already exists.",
'category_deleted': "Category '{}' deleted. {} stores updated.",
'category_delete_fail': "Failed to delete category '{}'.",
'store_added': "Store '{}' added successfully.",
'store_name_link_required': 'Store name and link are required.',
'invalid_link_format': 'Invalid link format. Make sure it starts with http:// or https://',
'hf_token_missing_logo': 'HF_TOKEN (write) is not configured. The logo was not uploaded.',
'logo_upload_error': "Error uploading logo {}.",
'logo_not_image': "File {} is not an image and was skipped.",
'store_edit_error': "Edit error: store with ID '{}' not found.",
'store_updated': "Store '{}' updated successfully.",
'logo_updated': 'Store logo updated successfully.',
'old_logo_delete_fail': 'Failed to delete the old logo from the server. The new logo has been uploaded.',
'new_logo_upload_fail': 'Failed to upload the new logo.',
'hf_token_missing_logo_update': 'HF_TOKEN (write) is not configured. The logo was not updated.',
'store_delete_error': "Delete error: store with ID '{}' not found.",
'store_deleted': "Store '{}' deleted.",
'old_logo_delete_fail_on_store_delete': "Failed to delete the logo for store '{}' from the server. The store was deleted locally.",
'store_deleted_no_token': "Store '{}' deleted locally, but the logo was not deleted from the server (token not set).",
'unknown_action': "Unknown action: {}",
'internal_error': "An internal error occurred while performing the action '{}'. See server logs for details.",
'force_upload_success': 'Data successfully uploaded to Hugging Face.',
'force_upload_error': 'Error uploading to Hugging Face: {}',
'force_download_success': 'Data successfully downloaded from Hugging Face. Local files have been updated.',
'force_download_error': 'Failed to download data from Hugging Face. Check logs.',
'force_download_exception': 'Error downloading from Hugging Face: {}',
'error': 'Error',
'success': 'Success',
'warning': 'Warning',
'close': 'Close',
'ai_error_no_image': 'Please upload a store logo first.',
'ai_error_not_image': 'The selected file is not an image.',
'ai_error_form_elements': 'Error: Form elements for generation not found.',
'ai_generating': 'Generating...',
'ai_generating_placeholder': 'Generating description, please wait...',
'ai_generation_error': 'Generation error: {}',
'ai_file_read_error': 'Error reading image file.',
}
}
countries_and_cities = {
"Россия": ["Москва", "Санкт-Петербург", "Новосибирск", "Екатеринбург", "Казань"],
"Кыргызстан": ["Бишкек", "Ош", "Джалал-Абад", "Каракол", "Нарын"],
"Казахстан": ["Алматы", "Астана", "Шымкент", "Актобе", "Караганда"],
"Узбекистан": ["Ташкент", "Самарканд", "Бухара", "Наманган", "Андижан"],
"USA": ["New York", "Los Angeles", "Chicago", "Houston", "Phoenix"],
"Turkey": ["Istanbul", "Ankara", "Izmir", "Bursa", "Antalya"]
}
def get_locale():
return request.accept_languages.best_match(['ru', 'en'])
@app.context_processor
def inject_translation():
def _(key, *args):
lang = get_locale()
text = translations.get(lang, translations['en']).get(key, key)
return text.format(*args) if args else text
return dict(_=_)
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({'stores': [], 'categories': [], 'organization_info': {}}, 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():
default_organization_info = {
"about_us": "METASTORE — это инновационный маркетплейс, объединяющий лучшие магазины прямо в Instagram. Мы предоставляем платформу для продавцов, чтобы они могли легко продемонстрировать свои товары широкой аудитории, а покупателям — находить уникальные предложения и совершать покупки у проверенных продавцов со всего мира. Наша миссия — сделать онлайн-шоппинг удобным, безопасным и вдохновляющим."
}
default_data = {'stores': [], 'categories': [], 'organization_info': default_organization_info}
data = default_data
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict):
raise FileNotFoundError
if 'stores' not in data: data['stores'] = data.get('products', [])
if 'products' in data: del data['products']
if 'categories' not in data: data['categories'] = []
if 'organization_info' not in data: data['organization_info'] = default_organization_info
except FileNotFoundError:
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict): data = default_data
if 'stores' not in data: data['stores'] = data.get('products', [])
if 'products' in data: del data['products']
if 'categories' not in data: data['categories'] = []
if 'organization_info' not in data: data['organization_info'] = default_organization_info
except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
data = default_data
else:
data = default_data
except json.JSONDecodeError:
if download_db_from_hf(specific_file=DATA_FILE):
try:
with open(DATA_FILE, 'r', encoding='utf-8') as file:
data = json.load(file)
if not isinstance(data, dict): data = default_data
if 'stores' not in data: data['stores'] = data.get('products', [])
if 'products' in data: del data['products']
if 'categories' not in data: data['categories'] = []
if 'organization_info' not in data: data['organization_info'] = default_organization_info
except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
data = default_data
else:
data = default_data
except Exception as e:
data = default_data
for store in data['stores']:
if 'store_id' not in store:
store['store_id'] = store.get('product_id', uuid4().hex)
if 'product_id' in store: del store['product_id']
if 'link' not in store: store['link'] = '#'
if 'logo' not in store:
store['logo'] = store.get('photos', [None])[0]
if 'photos' in store: del store['photos']
if any('store_id' not in s for s in data['stores']):
save_data(data)
if not os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(default_data, f)
except Exception as create_e:
pass
return data
def save_data(data):
try:
if not isinstance(data, dict):
return
if 'stores' not in data: data['stores'] = []
if 'categories' not in data: data['categories'] = []
if 'organization_info' not in data: data['organization_info'] = {}
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 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, store_name, 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 = f"На основе этого логотипа магазина под названием '{store_name}', напиши большой и красивый, содержательный рекламный пост минимум на 1000 символов со смайликами для Instagram. Включи 25 тематических хэштегов с ключевыми словами, чтобы клиенты могли найти этот магазин в поиске Instagram, Google и т.д. Пост должен быть сфокусирован на продвижении магазина, создавая образ надежного и интересного бренда. Не используй адреса или номера телефонов."
lang_suffix = ""
if language == "Русский":
lang_suffix = " Пиши на русском языке."
elif language == "Кыргызский":
lang_suffix = " Пиши на кыргызском языке."
elif language == "Казахский":
lang_suffix = " Пиши на казахском языке."
elif language == "Узбекский":
lang_suffix = " Пиши на узбекском языке."
elif language == "English":
lang_suffix = " Write in English."
final_prompt = f"{base_prompt}{lang_suffix}"
try:
model = genai.GenerativeModel('learnlm-2.0-flash-experimental')
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):
if not configure_gemini():
return "Извините, сервис чата временно недоступен. Пожалуйста, попробуйте позже."
data = load_data()
stores = data.get('stores', [])
categories = data.get('categories', [])
organization_info = data.get('organization_info', {})
store_info_list = []
for s in stores:
store_info_list.append(f"- Название: {s.get('name', 'Без названия')}, Категория: {s.get('category', 'Без категории')}, Страна: {s.get('country', 'N/A')}, Город: {s.get('city', 'N/A')}, Описание: {s.get('description', '')[:100]}...")
store_list_str = "\n".join(store_info_list) if store_info_list else "В данный момент нет зарегистрированных магазинов."
category_list_str = ", ".join(categories) if categories else "Категорий пока нет."
org_info_str = ""
if organization_info and organization_info.get("about_us"):
org_info_str += f"\n\nИнформация о платформе METASTORE:\nО нас: {organization_info['about_us']}\n"
system_instruction_content = (
"Ты - доброжелательный и очень полезный виртуальный консультант EVA для маркетплейса METASTORE. "
"Твоя задача - помогать пользователям находить магазины по категориям, странам, городам или ключевым словам. "
"Всегда будь вежлив, информативен и стремись помочь пользователю. "
"Никогда не выдумывай магазины или категории, которых нет в предоставленных списках. "
"Когда ты рекомендуешь магазин, всегда указывай его название и по возможности город или страну. "
"Если пользователь ищет магазин или категорию, предлагай несколько наиболее подходящих вариантов.\n\n"
f"Список доступных категорий: {category_list_str}.\n\n"
f"Список доступных магазинов на платформе:\n"
f"{store_list_str}"
f"{org_info_str}\n\n"
"Если пользователь спрашивает про магазины или категории, которых нет в списках, вежливо сообщи, что таких нет и предложи что-то из имеющихся. "
"Если вопрос касается общей информации о платформе (например, 'что такое METASTORE', 'о вас'), используй данные из блока 'Информация о платформе'. "
"Старайся быть кратким, но информативным. Используй эмодзи для дружелюбности. "
"Избегай упоминания Hugging Face или Google."
)
generated_text = ""
response = None
try:
model = genai.GenerativeModel('learnlm-2.0-flash-experimental', system_instruction=system_instruction_content)
model_chat_history_for_gemini = []
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 "Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна."
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}"
CATALOG_TEMPLATE = '''
<!DOCTYPE html>
<html lang="{{ get_locale() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{{ _('title') }}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #121212;
--primary-text: #E0E0E0;
--secondary-text: #BDBDBD;
--card-bg: #1E1E1E;
--header-bg: rgba(18, 18, 18, 0.85);
--accent-gradient: linear-gradient(45deg, #feda75, #fa7e1e, #d62976, #962fbf, #4f5bd5);
--button-bg: #333;
--button-hover: #444;
--input-bg: #2C2C2C;
--border-color: #333;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { -webkit-tap-highlight-color: transparent; }
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--primary-text);
line-height: 1.6;
}
.container {
max-width: 1300px;
margin: 0 auto;
padding: 0 0 100px 0;
}
.top-bar {
display: flex;
align-items: center;
padding: 15px 20px;
gap: 15px;
position: sticky;
top: 0;
background-color: var(--header-bg);
backdrop-filter: blur(10px);
z-index: 999;
border-bottom: 1px solid var(--border-color);
}
.search-wrapper {
flex-grow: 1;
position: relative;
}
#search-input {
width: 100%;
padding: 12px 20px 12px 45px;
font-size: 1rem;
border: 1px solid var(--border-color);
border-radius: 25px;
outline: none;
background-color: var(--input-bg);
color: var(--primary-text);
transition: all 0.3s ease;
}
#search-input:focus {
box-shadow: 0 0 0 2px #962fbf;
border-color: #962fbf;
}
#search-input::placeholder { color: var(--secondary-text); }
.search-wrapper .fa-search {
position: absolute;
top: 50%;
left: 18px;
transform: translateY(-50%);
color: var(--secondary-text);
font-size: 1rem;
}
.top-bar-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
background-color: var(--input-bg);
border: 1px solid var(--border-color);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--primary-text);
font-size: 1.2rem;
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s;
}
.top-bar-icon:hover {
background-color: var(--button-hover);
}
.category-section {
margin-top: 25px;
}
.category-header {
padding: 0 20px;
margin-bottom: 15px;
}
.category-header h2 {
font-size: 1.6rem;
font-weight: 600;
}
.store-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 20px;
padding: 10px 20px;
}
.store-card {
background: var(--card-bg);
border-radius: 16px;
flex-shrink: 0;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
position: relative;
border: 1px solid var(--border-color);
aspect-ratio: 1 / 1.2;
display: flex;
flex-direction: column;
}
.store-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.store-card:active { transform: scale(0.97); }
.store-logo-container {
width: 100%;
flex-grow: 1;
position: relative;
}
.store-logo-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
.store-info {
padding: 12px;
text-align: center;
}
.store-name {
font-size: 1rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.store-location {
font-size: 0.8rem;
color: var(--secondary-text);
margin-top: 4px;
}
.top-store-indicator {
position: absolute;
top: 8px;
right: 8px;
background: var(--accent-gradient);
color: white;
padding: 3px 9px;
font-size: 0.75rem;
border-radius: 12px;
font-weight: bold;
z-index: 10;
}
.no-results-message { padding: 40px 20px; text-align: center; font-size: 1.1rem; color: var(--secondary-text); }
.floating-button {
position: fixed;
bottom: 25px;
right: 25px;
background: var(--accent-gradient);
color: white;
border: none;
border-radius: 50%;
width: 55px;
height: 55px;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 20px rgba(214, 41, 118, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
}
.floating-button:hover {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 6px 25px rgba(214, 41, 118, 0.5);
}
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); backdrop-filter: blur(8px); overflow-y: auto; }
.modal-content { background: var(--card-bg); color: var(--primary-text); margin: 5% auto; padding: 25px; border-radius: 20px; width: 90%; max-width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: slideIn 0.3s ease-out; position: relative; border: 1px solid var(--border-color); }
@keyframes slideIn { from { transform: translateY(-30px) scale(0.95); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }
.close { position: absolute; top: 15px; right: 20px; font-size: 2rem; color: var(--secondary-text); cursor: pointer; transition: color 0.3s; line-height: 1; }
.close:hover { color: var(--primary-text); }
.modal-content h2 { margin-top: 0; margin-bottom: 20px; display: flex; align-items: center; gap: 10px; font-size: 1.8rem; }
#chatModal .modal-content { max-width: 450px; }
#chat-messages { height: 350px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 12px; padding: 15px; margin-bottom: 15px; display: flex; flex-direction: column; gap: 12px; background-color: var(--bg-color); }
.chat-message { padding: 10px 15px; border-radius: 18px; max-width: 85%; word-wrap: break-word; line-height: 1.4; animation: fadeIn 0.3s ease; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.chat-message.user { align-self: flex-end; background: var(--accent-gradient); color: white; border-bottom-right-radius: 4px; }
.chat-message.ai { align-self: flex-start; background-color: var(--input-bg); color: var(--primary-text); border-bottom-left-radius: 4px; }
.chat-input-container { display: flex; gap: 10px; }
#chat-input { flex-grow: 1; padding: 10px 15px; border: 1px solid var(--border-color); border-radius: 20px; font-size: 0.95rem; outline: none; background-color: var(--input-bg); color: var(--primary-text); }
#chat-input:focus { border-color: #962fbf; }
#chat-send-button { background: var(--accent-gradient); color: white; border: none; border-radius: 20px; width: 40px; height: 40px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.2s; }
#chat-send-button:hover { transform: scale(1.1); }
#chat-send-button:disabled { background: #555; cursor: not-allowed; }
.action-button { display: inline-block; width: 100%; padding: 15px; border: none; border-radius: 12px; color: white; font-size: 1.1rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; text-align: center; text-decoration: none; background: var(--accent-gradient); }
.action-button:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(214, 41, 118, 0.4); }
.clear-chat-button { background-color: #555; color: var(--primary-text); }
.clear-chat-button:hover { background-color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="top-bar">
<div class="search-wrapper">
<i class="fas fa-search"></i>
<input type="text" id="search-input" placeholder="{{ _('search_placeholder') }}">
</div>
<a href="#" class="top-bar-icon" onclick="openChatModal()">
<i class="fas fa-comment-dots"></i>
</a>
</div>
<div id="catalog-content">
{% set has_stores = False %}
{% for category_name in ordered_categories %}
{% if stores_by_category[category_name] %}
{% set has_stores = True %}
<div class="category-section" data-category-name="{{ category_name }}">
<div class="category-header">
<h2>{{ category_name }}</h2>
</div>
<div class="store-grid">
{% for store in stores_by_category[category_name] %}
<div class="store-card"
data-store-id="{{ store.get('store_id', '') }}"
data-name="{{ store.name|lower }}"
data-description="{{ store.get('description', '')|lower }}"
data-country="{{ store.get('country', '')|lower }}"
data-city="{{ store.get('city', '')|lower }}"
onclick="openModalById('{{ store.get('store_id', '') }}')">
<div class="store-logo-container">
{% if store.get('is_top', False) %}
<span class="top-store-indicator"><i class="fas fa-star"></i></span>
{% endif %}
{% if store.get('logo') %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/logos/{{ store.logo }}"
alt="{{ store.name }}" loading="lazy">
{% else %}
<img src="https://via.placeholder.com/300x300.png/1E1E1E/E0E0E0?text={{ store.name|replace(' ', '+') }}" alt="No Logo" loading="lazy">
{% endif %}
</div>
<div class="store-info">
<div class="store-name">{{ store.name }}</div>
<div class="store-location">{{ store.get('city', '') }}, {{ store.get('country', '') }}</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
{% if not has_stores %}
<p class="no-results-message">{{ _('no_stores_added') }}</p>
{% endif %}
<p id="no-results-message" class="no-results-message" style="display: none;">{{ _('no_results_found') }}</p>
</div>
</div>
<div id="storeModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('storeModal')" aria-label="{{ _('close') }}">×</span>
<div id="modalContent">{{ _('loading') }}...</div>
</div>
</div>
<div id="chatModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('chatModal')" aria-label="{{ _('close') }}">×</span>
<h2><i class="fas fa-robot"></i> {{ _('chat_with_eva') }}</h2>
<div id="chat-messages"></div>
<div class="chat-input-container">
<input type="text" id="chat-input" placeholder="{{ _('chat_placeholder') }}">
<button id="chat-send-button"><i class="fas fa-paper-plane"></i></button>
</div>
<button id="clear-chat-button" class="action-button clear-chat-button" style="margin-top: 15px; font-size: 0.9rem; padding: 10px;"><i class="fas fa-trash"></i> {{ _('clear_chat_history') }}</button>
</div>
</div>
<button id="chat-open-button" class="floating-button" onclick="openChatModal()" aria-label="{{ _('chat_with_eva') }}">
<i class="fas fa-comment-dots"></i>
</button>
<script>
const allStores = {{ stores_json|safe }};
const repoId = '{{ repo_id }}';
let chatHistory = JSON.parse(localStorage.getItem('evaChatHistory') || '[]');
function getStoreById(storeId) {
return allStores.find(s => s.store_id === storeId);
}
function openModalById(storeId) {
const store = getStoreById(storeId);
if (!store) {
alert("Error: Store not found.");
return;
}
loadStoreDetails(store.store_id);
const modal = document.getElementById('storeModal');
if (modal) {
modal.style.display = "block";
document.body.style.overflow = 'hidden';
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.style.display = "none";
}
const anyModalOpen = document.querySelector('.modal[style*="display: block"]');
if (!anyModalOpen) {
document.body.style.overflow = 'auto';
}
}
function loadStoreDetails(storeId) {
const modalContent = document.getElementById('modalContent');
if (!modalContent) return;
modalContent.innerHTML = `<p style="text-align:center; padding: 40px;">{{ _('loading') }}</p>`;
fetch('/store/' + storeId)
.then(response => {
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
return response.text();
})
.then(data => {
modalContent.innerHTML = data;
})
.catch(error => {
console.error('Error loading store details:', error);
modalContent.innerHTML = `<p style="color: #dc3545; text-align:center; padding: 40px;">{{ _('error_loading_details') }}</p>`;
});
}
function filterStores() {
const searchTerm = document.getElementById('search-input').value.toLowerCase().trim();
const allCategorySections = document.querySelectorAll('.category-section');
const noResultsEl = document.getElementById('no-results-message');
let totalResults = 0;
allCategorySections.forEach(section => {
const storeCards = section.querySelectorAll('.store-card');
let categoryHasVisibleStores = false;
storeCards.forEach(card => {
const name = card.dataset.name || '';
const description = card.dataset.description || '';
const country = card.dataset.country || '';
const city = card.dataset.city || '';
if (searchTerm === '' || name.includes(searchTerm) || description.includes(searchTerm) || country.includes(searchTerm) || city.includes(searchTerm)) {
card.style.display = 'flex';
categoryHasVisibleStores = true;
} else {
card.style.display = 'none';
}
});
const grid = section.querySelector('.store-grid');
if (grid) {
const visibleCards = Array.from(grid.querySelectorAll('.store-card')).filter(c => c.style.display !== 'none');
if(visibleCards.length > 0) {
section.style.display = 'block';
totalResults += visibleCards.length;
} else {
section.style.display = 'none';
}
}
});
if (totalResults === 0 && searchTerm !== '') {
if (noResultsEl) noResultsEl.style.display = 'block';
} else {
if (noResultsEl) noResultsEl.style.display = 'none';
}
}
function openChatModal() {
const modal = document.getElementById('chatModal');
if(modal) {
modal.style.display = "block";
document.body.style.overflow = 'hidden';
displayChatHistory();
document.getElementById('chat-input').focus();
}
}
function displayChatHistory() {
const chatMessagesDiv = document.getElementById('chat-messages');
chatMessagesDiv.innerHTML = '';
chatHistory.forEach(msg => {
addMessageToChat(msg.text, msg.role, false);
});
chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
}
function addMessageToChat(text, role, save = true) {
const chatMessagesDiv = document.getElementById('chat-messages');
const messageElement = document.createElement('div');
messageElement.className = `chat-message ${role}`;
messageElement.innerHTML = text.replace(/\\n/g, '<br>');
chatMessagesDiv.appendChild(messageElement);
chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
if (save) {
chatHistory.push({ text: text, role: role });
localStorage.setItem('evaChatHistory', JSON.stringify(chatHistory));
}
}
async function sendMessage() {
const chatInput = document.getElementById('chat-input');
const chatSendButton = document.getElementById('chat-send-button');
const message = chatInput.value.trim();
if (!message) return;
addMessageToChat(message, 'user');
chatInput.value = '';
chatSendButton.disabled = true;
try {
const response = await fetch('/chat_with_ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: message, history: chatHistory })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'AI response error.');
}
addMessageToChat(result.text, 'ai');
} catch (error) {
console.error("Chat AI Error:", error);
addMessageToChat(`Sorry, an error occurred: ${error.message}`, 'ai', false);
} finally {
chatSendButton.disabled = false;
}
}
function clearChatHistory() {
if (confirm("{{ _('confirm_clear_chat') }}")) {
chatHistory = [];
localStorage.removeItem('evaChatHistory');
displayChatHistory();
}
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('search-input').addEventListener('input', filterStores);
document.getElementById('chat-send-button').addEventListener('click', sendMessage);
document.getElementById('chat-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') { sendMessage(); }
});
document.getElementById('clear-chat-button').addEventListener('click', clearChatHistory);
window.addEventListener('click', function(event) {
if (event.target.classList.contains('modal')) { closeModal(event.target.id); }
});
window.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
document.querySelectorAll('.modal[style*="display: block"]').forEach(modal => {
closeModal(modal.id);
});
}
});
});
</script>
</body>
</html>
'''
STORE_DETAIL_TEMPLATE = '''
<div style="text-align: center; padding: 10px;">
<div style="width: 150px; height: 150px; margin: 0 auto 20px; border-radius: 50%; overflow: hidden; border: 4px solid #333;">
{% if store.get('logo') %}
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/logos/{{ store.logo }}"
alt="{{ store.name }} logo"
style="width: 100%; height: 100%; object-fit: cover;">
{% else %}
<img src="https://via.placeholder.com/150x150.png/1E1E1E/E0E0E0?text={{ store.name[0] }}" alt="No Logo" style="width: 100%; height: 100%; object-fit: cover;">
{% endif %}
</div>
<h2 style="font-size: 2rem; font-weight: 700; margin-bottom: 10px;">{{ store.name }}</h2>
<p style="font-size: 1rem; color: var(--secondary-text); margin-bottom: 20px;"><i class="fas fa-map-marker-alt"></i> {{ store.get('city', '') }}, {{ store.get('country', '') }}</p>
<div style="margin: 30px 0; text-align: left; font-size: 1rem; line-height: 1.7; color: var(--primary-text); max-height: 200px; overflow-y: auto; padding: 0 10px;">
<p><strong>{{ _('description') }}:</strong><br> {{ store.get('description', _('no_description'))|replace('\\n', '<br>')|safe }}</p>
</div>
<div style="margin-top: 30px;">
<a href="{{ store.link }}" target="_blank" rel="noopener noreferrer" class="action-button">
<i class="fas fa-external-link-alt"></i> {{ _('go_to_store') }}
</a>
</div>
</div>
'''
ADMIN_TEMPLATE = '''
<!DOCTYPE html>
<html lang="{{ get_locale() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ _('admin_panel_title') }}</title>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
body { font-family: 'Poppins', sans-serif; background-color: #f4f6f9; color: #333; padding: 20px; line-height: 1.6; }
.container { max-width: 1200px; margin: 0 auto; background-color: #fff; padding: 25px; border-radius: 10px; box-shadow: 0 3px 10px rgba(0,0,0,0.05); }
.header { padding-bottom: 15px; margin-bottom: 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;}
.header .logo-title-container img { height: 50px; width: 50px; border-radius: 50%; object-fit: cover; border: 2px solid #3F51B5;}
h1, h2, h3 { font-weight: 600; color: #3F51B5; margin-bottom: 15px; }
h1 { font-size: 1.8rem; }
h2 { font-size: 1.5rem; margin-top: 30px; display: flex; align-items: center; gap: 8px; }
h3 { font-size: 1.2rem; color: #303F9F; margin-top: 20px; }
.section { margin-bottom: 30px; padding: 20px; background-color: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; }
form { margin-bottom: 20px; }
label { font-weight: 500; margin-top: 10px; display: block; color: #666; font-size: 0.9rem;}
input[type="text"], input[type="url"], input[type="password"], textarea, select { width: 100%; padding: 10px 12px; margin-top: 5px; border: 1px solid #e0e0e0; border-radius: 6px; font-size: 0.95rem; box-sizing: border-box; transition: border-color 0.3s ease; background-color: #fff; }
input:focus, textarea:focus, select:focus { border-color: #3F51B5; outline: none; box-shadow: 0 0 0 2px rgba(63, 81, 181, 0.1); }
textarea { min-height: 80px; resize: vertical; }
input[type="file"] { padding: 8px; background-color: #ffffff; cursor: pointer; border: 1px solid #e0e0e0;}
input[type="file"]::file-selector-button { padding: 5px 10px; border-radius: 4px; background-color: #f0f0f0; border: 1px solid #e0e0e0; cursor: pointer; margin-right: 10px;}
input[type="checkbox"] { margin-right: 5px; vertical-align: middle; }
label.inline-label { display: inline-block; margin-top: 10px; font-weight: normal; }
button, .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: #7986CB; color: white; font-weight: 500; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; margin-top: 15px; font-size: 0.95rem; display: inline-flex; align-items: center; gap: 5px; text-decoration: none; line-height: 1.2;}
button:hover, .button:hover { background-color: #3F51B5; }
button:active, .button:active { transform: scale(0.98); }
button[type="submit"] { min-width: 120px; justify-content: center; }
.delete-button { background-color: #dc3545; }
.delete-button:hover { background-color: #c82333; }
.add-button { background-color: #3F51B5; }
.add-button:hover { background-color: #303F9F; }
.item-list { display: grid; gap: 20px; }
.item { background: #fff; padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.03); border: 1px solid #f0f0f0; }
.item p { margin: 5px 0; font-size: 0.9rem; color: #666; }
.item strong { color: #333; }
.item .description { font-size: 0.85rem; color: #999; max-height: 60px; overflow: hidden; text-overflow: ellipsis; }
.item-actions { margin-top: 15px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.edit-form-container { margin-top: 15px; padding: 20px; background: #edf2ff; border: 1px dashed #e0e0e0; border-radius: 6px; display: none; }
details { background-color: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
details > summary { cursor: pointer; font-weight: 600; color: #303F9F; display: block; padding: 15px; list-style: none; position: relative; }
details > summary::after { content: '\\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; position: absolute; right: 20px; top: 50%; transform: translateY(-50%); transition: transform 0.2s ease; color: #3F51B5; }
details[open] > summary { border-bottom: 1px solid #e0e0e0; }
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
details .form-content { padding: 20px; }
.logo-preview img { max-width: 70px; max-height: 70px; border-radius: 5px; margin: 5px 5px 0 0; border: 1px solid #e0e0e0; object-fit: cover;}
.sync-buttons { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }
.download-hf-button { background-color: #6c757d; }
.download-hf-button:hover { background-color: #5a6268; }
.flex-container { display: flex; flex-wrap: wrap; gap: 20px; }
.flex-item { flex: 1; min-width: 350px; }
.message { padding: 10px 15px; border-radius: 6px; margin-bottom: 15px; font-size: 0.9rem;}
.message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;}
.message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;}
.message.warning { background-color: #fff3cd; color: #856404; border: 1px solid #ffeeba; }
.status-indicator { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 0.8rem; font-weight: 500; margin-left: 10px; vertical-align: middle; }
.status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
.ai-generate-button { background-color: #8D6EC8; margin-top: 5px; margin-bottom: 10px; }
.ai-generate-button:hover { background-color: #7B4DB5; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
<h1><i class="fas fa-tools"></i> {{ _('admin_panel_title') }}</h1>
</div>
<a href="{{ url_for('catalog') }}" class="button" style="background-color: #3F51B5;"><i class="fas fa-store"></i> {{ _('go_to_catalog') }}</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="message {{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<div class="section">
<h2><i class="fas fa-sync-alt"></i> {{ _('sync_with_dc') }}</h2>
<div class="sync-buttons">
<form method="POST" action="{{ url_for('force_upload') }}" style="display: inline;" onsubmit="return confirm('{{ _('confirm_force_upload') }}');">
<button type="submit" class="button" title="Upload local files to Hugging Face"><i class="fas fa-upload"></i> {{ _('upload_db') }}</button>
</form>
<form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('{{ _('confirm_force_download') }}');">
<button type="submit" class="button download-hf-button" title="Download files (will overwrite local)"><i class="fas fa-download"></i> {{ _('download_db') }}</button>
</form>
</div>
<p style="font-size: 0.85rem; color: #999;">{{ _('sync_info') }}</p>
</div>
<div class="flex-container">
<div class="flex-item">
<div class="section">
<h2><i class="fas fa-tags"></i> {{ _('category_management') }}</h2>
<details>
<summary><i class="fas fa-plus-circle"></i> {{ _('add_new_category') }}</summary>
<div class="form-content">
<form method="POST">
<input type="hidden" name="action" value="add_category">
<label for="add_category_name">{{ _('new_category_name') }}</label>
<input type="text" id="add_category_name" name="category_name" required>
<button type="submit" class="add-button"><i class="fas fa-plus"></i> {{ _('add') }}</button>
</form>
</div>
</details>
<h3>{{ _('existing_categories') }}</h3>
{% if categories %}
<div class="item-list">
{% for category in categories %}
<div class="item" style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ category }}</span>
<form method="POST" style="margin: 0;" onsubmit="return confirm('{{ _('confirm_delete_category', category) }}');">
<input type="hidden" name="action" value="delete_category">
<input type="hidden" name="category_name" value="{{ category }}">
<button type="submit" class="delete-button" style="padding: 5px 10px; font-size: 0.8rem; margin: 0;"><i class="fas fa-trash-alt"></i></button>
</form>
</div>
{% endfor %}
</div>
{% else %}
<p>{{ _('no_categories_yet') }}</p>
{% endif %}
</div>
</div>
<div class="flex-item">
<div class="section">
<h2><i class="fas fa-info-circle"></i> {{ _('store_info_management') }}</h2>
<details>
<summary><i class="fas fa-chevron-down"></i> {{ _('expand_collapse') }}</summary>
<div class="form-content">
<form method="POST">
<input type="hidden" name="action" value="update_org_info">
<label for="about_us">{{ _('about_us') }}</label>
<textarea id="about_us" name="about_us" rows="6">{{ organization_info.get('about_us', '') }}</textarea>
<button type="submit" class="add-button"><i class="fas fa-save"></i> {{ _('save_changes') }}</button>
</form>
<p style="font-size: 0.85rem; color: #999;">{{ _('platform_info_ai_hint') }}</p>
</div>
</details>
</div>
</div>
</div>
<div class="section">
<h2><i class="fas fa-store-alt"></i> {{ _('store_management') }}</h2>
<details>
<summary><i class="fas fa-plus-circle"></i> {{ _('add_new_store') }}</summary>
<div class="form-content">
<form id="add-store-form" method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="add_store">
<label for="add_name">{{ _('store_name') }}</label>
<input type="text" id="add_name" name="name" required>
<label for="add_link">{{ _('store_link') }}</label>
<input type="url" id="add_link" name="link" placeholder="https://instagram.com/..." required>
<label for="add_logo">{{ _('logo') }}</label>
<input type="file" id="add_logo" name="logo" accept="image/*">
<label for="add_description">{{ _('description') }}:</label>
<textarea id="add_description" name="description" rows="4"></textarea>
<button type="button" class="button ai-generate-button" onclick="generateDescription('add_logo', 'add_name', 'add_description', 'add_gen_lang')"><i class="fas fa-magic"></i> {{ _('generate_description') }}</button>
<label for="add_gen_lang">{{ _('generation_language') }}</label>
<select id="add_gen_lang" name="gen_lang" style="width: auto; display: inline-block; margin-left: 10px;">
<option value="Русский">Русский</option>
<option value="English">English</option>
<option value="Кыргызский">Кыргызский</option>
<option value="Казахский">Казахский</option>
<option value="Узбекский">Узбекский</option>
</select>
<label for="add_category">{{ _('category') }}:</label>
<select id="add_category" name="category">
<option value="{{ _('no_category') }}">{{ _('no_category') }}</option>
{% for category in categories %}
<option value="{{ category }}">{{ category }}</option>
{% endfor %}
</select>
<label for="add_country">{{ _('country_select') }}</label>
<select id="add_country" name="country" onchange="updateCities('add_country', 'add_city')" required>
{% for country in countries_and_cities.keys() %}
<option value="{{ country }}">{{ country }}</option>
{% endfor %}
</select>
<label for="add_city">{{ _('city_select') }}</label>
<select id="add_city" name="city" required>
</select>
<br>
<div style="margin-top: 15px;">
<input type="checkbox" id="add_is_top" name="is_top">
<label for="add_is_top" class="inline-label">{{ _('is_top_store') }}</label>
</div>
<br>
<button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> {{ _('add_store') }}</button>
</form>
</div>
</details>
<h3>{{ _('store_list') }}</h3>
{% if stores %}
<div class="item-list">
{% for store in stores %}
<div class="item">
<div style="display: flex; gap: 15px; align-items: flex-start;">
<div class="logo-preview" style="flex-shrink: 0;">
{% if store.get('logo') %}
<a href="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/logos/{{ store.logo }}" target="_blank" title="View logo">
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/logos/{{ store.logo }}" alt="Logo">
</a>
{% else %}
<img src="https://via.placeholder.com/70x70.png/E0E0E0/999?text=N/A" alt="No Logo">
{% endif %}
</div>
<div style="flex-grow: 1;">
<h3 style="margin-top: 0; margin-bottom: 5px; color: #333;">
{{ store.name }}
{% if store.get('is_top', False) %}
<span class="status-indicator top-product"><i class="fas fa-star"></i> Top</span>
{% endif %}
</h3>
<p><strong>{{ _('category') }}:</strong> {{ store.get('category', _('no_category')) }}</p>
<p><strong>{{ _('country') }}:</strong> {{ store.get('country', 'N/A') }} / <strong>{{ _('city') }}:</strong> {{ store.get('city', 'N/A') }}</p>
<p><strong>{{ _('store_link') }}:</strong> <a href="{{ store.get('link', '#') }}" target="_blank">{{ store.get('link', 'N/A') }}</a></p>
<p class="description" title="{{ store.get('description', '') }}"><strong>{{ _('description') }}:</strong> {{ store.get('description', 'N/A')[:150] }}{% if store.get('description', '')|length > 150 %}...{% endif %}</p>
</div>
</div>
<div class="item-actions">
<button type="button" class="button" onclick="toggleEditForm('edit-form-{{ loop.index0 }}')"><i class="fas fa-edit"></i> {{ _('edit') }}</button>
<form method="POST" style="margin:0;" onsubmit="return confirm('{{ _('confirm_delete_store', store.name) }}');">
<input type="hidden" name="action" value="delete_store">
<input type="hidden" name="store_id" value="{{ store.get('store_id', '') }}">
<button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> {{ _('delete') }}</button>
</form>
</div>
<div id="edit-form-{{ loop.index0 }}" class="edit-form-container">
<h4><i class="fas fa-edit"></i> {{ _('editing') }} {{ store.name }}</h4>
<form method="POST" enctype="multipart/form-data">
<input type="hidden" name="action" value="edit_store">
<input type="hidden" name="store_id" value="{{ store.get('store_id', '') }}">
<label>{{ _('store_name') }}</label>
<input type="text" name="name" value="{{ store.name }}" required>
<label>{{ _('store_link') }}</label>
<input type="url" name="link" value="{{ store.get('link', '') }}" required>
<label>{{ _('replace_logo') }}</label>
<input type="file" id="edit_logo_{{ loop.index0 }}" name="logo" accept="image/*">
{% if store.get('logo') %}
<p style="font-size: 0.85rem; margin-top: 5px;">{{ _('current_logo') }}</p>
<div class="logo-preview">
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/logos/{{ store.logo }}" alt="Logo">
</div>
{% endif %}
<label>{{ _('description') }}:</label>
<textarea id="edit_description_{{ loop.index0 }}" name="description" rows="4">{{ store.get('description', '') }}</textarea>
<button type="button" class="button ai-generate-button" onclick="generateDescription('edit_logo_{{ loop.index0 }}', 'edit_form-{{ loop.index0 }} input[name=name]', 'edit_description_{{ loop.index0 }}', 'edit_gen_lang_{{ loop.index0 }}')"><i class="fas fa-magic"></i> {{ _('generate_description') }}</button>
<label for="edit_gen_lang_{{ loop.index0 }}">{{ _('generation_language') }}</label>
<select id="edit_gen_lang_{{ loop.index0 }}" name="gen_lang" style="width: auto; display: inline-block; margin-left: 10px;">
<option value="Русский">Русский</option>
<option value="English">English</option>
<option value="Кыргызский">Кыргызский</option>
<option value="Казахский">Казахский</option>
<option value="Узбекский">Узбекский</option>
</select>
<label>{{ _('category') }}:</label>
<select name="category">
<option value="{{ _('no_category') }}" {% if store.get('category', _('no_category')) == _('no_category') %}selected{% endif %}>{{ _('no_category') }}</option>
{% for category in categories %}
<option value="{{ category }}" {% if store.get('category') == category %}selected{% endif %}>{{ category }}</option>
{% endfor %}
</select>
<label>{{ _('country_select') }}</label>
<select id="edit_country_{{ loop.index0 }}" name="country" onchange="updateCities('edit_country_{{ loop.index0 }}', 'edit_city_{{ loop.index0 }}')" required>
{% for country in countries_and_cities.keys() %}
<option value="{{ country }}" {% if store.get('country') == country %}selected{% endif %}>{{ country }}</option>
{% endfor %}
</select>
<label>{{ _('city_select') }}</label>
<select id="edit_city_{{ loop.index0 }}" name="city" required>
{% set selected_country = store.get('country') %}
{% if selected_country in countries_and_cities %}
{% for city in countries_and_cities[selected_country] %}
<option value="{{ city }}" {% if store.get('city') == city %}selected{% endif %}>{{ city }}</option>
{% endfor %}
{% endif %}
</select>
<br>
<div style="margin-top: 15px;">
<input type="checkbox" id="edit_is_top_{{ loop.index0 }}" name="is_top" {% if store.get('is_top', False) %}checked{% endif %}>
<label for="edit_is_top_{{ loop.index0 }}" class="inline-label">{{ _('is_top_store') }}</label>
</div>
<br>
<button type="submit" class="add-button" style="margin-top: 20px;"><i class="fas fa-save"></i> {{ _('save_changes') }}</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p>{{ _('no_stores_yet') }}</p>
{% endif %}
</div>
</div>
<script>
const countriesAndCities = {{ countries_and_cities|tojson }};
function toggleEditForm(formId) {
const formContainer = document.getElementById(formId);
if (formContainer) {
formContainer.style.display = formContainer.style.display === 'none' || formContainer.style.display === '' ? 'block' : 'none';
}
}
function updateCities(countrySelectId, citySelectId) {
const countrySelect = document.getElementById(countrySelectId);
const citySelect = document.getElementById(citySelectId);
const selectedCountry = countrySelect.value;
citySelect.innerHTML = '';
if (countriesAndCities[selectedCountry]) {
countriesAndCities[selectedCountry].forEach(city => {
const option = document.createElement('option');
option.value = city;
option.text = city;
citySelect.appendChild(option);
});
}
}
document.addEventListener('DOMContentLoaded', () => {
updateCities('add_country', 'add_city');
const editForms = document.querySelectorAll('.edit-form-container');
editForms.forEach((form, index) => {
const countrySelectId = `edit_country_${index}`;
const citySelectId = `edit_city_${index}`;
if (document.getElementById(countrySelectId) && document.getElementById(citySelectId)) {
// Cities are pre-filled by Jinja, so no need to call updateCities on load.
}
});
});
async function generateDescription(logoInputId, nameInputSelector, descriptionTextareaId, languageSelectId) {
const logoInput = document.getElementById(logoInputId);
const nameInput = document.querySelector(nameInputSelector);
const descriptionTextarea = document.getElementById(descriptionTextareaId);
const languageSelect = document.getElementById(languageSelectId);
const generateButton = descriptionTextarea.nextElementSibling;
if (!logoInput || !descriptionTextarea || !languageSelect || !generateButton || !nameInput) {
alert("{{ _('ai_error_form_elements') }}");
return;
}
if (!logoInput.files || logoInput.files.length === 0) {
alert("{{ _('ai_error_no_image') }}");
return;
}
const storeName = nameInput.value.trim();
if (!storeName) {
alert("{{ _('store_name') }}");
nameInput.focus();
return;
}
const file = logoInput.files[0];
if (!file.type.startsWith('image/')) {
alert("{{ _('ai_error_not_image') }}");
return;
}
generateButton.disabled = true;
const originalText = generateButton.innerHTML;
generateButton.innerHTML = `<i class="fas fa-sync fa-spin"></i> {{ _('ai_generating') }}`;
descriptionTextarea.value = "{{ _('ai_generating_placeholder') }}";
descriptionTextarea.disabled = true;
const reader = new FileReader();
reader.onload = async (e) => {
const base64Image = e.target.result.split(',')[1];
const language = languageSelect.value;
try {
const response = await fetch('/generate_description_ai', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64Image, name: storeName, language: language })
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || `Server error: ${response.status}`);
}
descriptionTextarea.value = result.text;
} catch (error) {
console.error("AI Generation Error:", error);
const errorMessage = `{{ _('ai_generation_error', '${error.message}') }}`;
descriptionTextarea.value = errorMessage;
alert(errorMessage);
} finally {
generateButton.disabled = false;
generateButton.innerHTML = originalText;
descriptionTextarea.disabled = false;
}
};
reader.onerror = (e) => {
console.error("FileReader error:", e);
descriptionTextarea.value = "{{ _('ai_file_read_error') }}";
generateButton.disabled = false;
generateButton.innerHTML = originalText;
descriptionTextarea.disabled = false;
};
reader.readAsDataURL(file);
}
</script>
</body>
</html>
'''
@app.route('/')
def catalog():
data = load_data()
all_stores_raw = data.get('stores', [])
store_categories = set(p.get('category', _('no_category')) for p in all_stores_raw)
admin_categories = set(data.get('categories', []))
all_cat_names = sorted(list(store_categories.union(admin_categories)))
stores_sorted_for_js = sorted(all_stores_raw, key=lambda s: (not s.get('is_top', False), s.get('name', '').lower()))
stores_by_category = {cat: [] for cat in all_cat_names}
for store in all_stores_raw:
stores_by_category[store.get('category', _('no_category'))].append(store)
for category in stores_by_category:
stores_by_category[category].sort(key=lambda s: (not s.get('is_top', False), s.get('name', '').lower()))
ordered_categories = [cat for cat in all_cat_names if stores_by_category.get(cat)]
return render_template_string(
CATALOG_TEMPLATE,
stores_by_category=stores_by_category,
ordered_categories=ordered_categories,
stores_json=json.dumps(stores_sorted_for_js),
repo_id=REPO_ID
)
@app.route('/store/<store_id>')
def store_detail(store_id):
data = load_data()
all_stores_raw = data.get('stores', [])
store = next((s for s in all_stores_raw if s.get('store_id') == store_id), None)
if not store:
return _('error_loading_details'), 404
return render_template_string(
STORE_DETAIL_TEMPLATE,
store=store,
repo_id=REPO_ID
)
@app.route('/admin', methods=['GET', 'POST'])
def admin():
lang = get_locale()
_ = lambda key, *args: translations.get(lang, translations['en']).get(key, key).format(*args) if args else translations.get(lang, translations['en']).get(key, key)
data = load_data()
stores = data.get('stores', [])
categories = data.get('categories', [])
organization_info = data.get('organization_info', {})
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_data(data)
flash(_('category_added', category_name), 'success')
elif not category_name:
flash(_('category_empty'), 'error')
else:
flash(_('category_exists', 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 store in stores:
if store.get('category') == category_to_delete:
store['category'] = _('no_category')
updated_count += 1
data['categories'] = categories
data['stores'] = stores
save_data(data)
flash(_('category_deleted', category_to_delete, updated_count), 'success')
else:
flash(_('category_delete_fail', category_to_delete), 'error')
elif action == 'update_org_info':
organization_info['about_us'] = request.form.get('about_us', '').strip()
data['organization_info'] = organization_info
save_data(data)
flash(_('platform_info_saved'), 'success')
elif action == 'add_store':
name = request.form.get('name', '').strip()
link = request.form.get('link', '').strip()
description = request.form.get('description', '').strip()
category = request.form.get('category')
country = request.form.get('country')
city = request.form.get('city')
logo_file = request.files.get('logo')
is_top = 'is_top' in request.form
if not name or not link:
flash(_('store_name_link_required'), 'error')
return redirect(url_for('admin'))
if not (link.startswith('http://') or link.startswith('https://')):
flash(_('invalid_link_format'), 'error')
return redirect(url_for('admin'))
logo_filename = None
if logo_file and logo_file.filename and HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
try:
ext = os.path.splitext(logo_file.filename)[1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
flash(_('logo_not_image', logo_file.filename), "warning")
else:
safe_name = secure_filename(name.replace(' ', '_'))[:50]
logo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
temp_path = os.path.join(uploads_dir, logo_filename)
logo_file.save(temp_path)
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=f"logos/{logo_filename}",
repo_id=REPO_ID,
repo_type="dataset",
token=HF_TOKEN_WRITE,
commit_message=f"Add logo for store {name}"
)
os.remove(temp_path)
except Exception as e:
flash(_('logo_upload_error', logo_file.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
elif logo_file and logo_file.filename and not HF_TOKEN_WRITE:
flash(_('hf_token_missing_logo'), "warning")
new_store = {
'store_id': uuid4().hex, 'name': name, 'link': link,
'description': description, 'category': category if category in categories else _('no_category'),
'country': country, 'city': city, 'logo': logo_filename, 'is_top': is_top
}
stores.append(new_store)
data['stores'] = stores
save_data(data)
flash(_('store_added', name), 'success')
elif action == 'edit_store':
store_id = request.form.get('store_id')
store_to_edit = next((s for s in stores if s.get('store_id') == store_id), None)
if store_to_edit is None:
flash(_('store_edit_error', store_id), 'error')
return redirect(url_for('admin'))
original_name = store_to_edit.get('name', 'N/A')
store_to_edit['name'] = request.form.get('name', store_to_edit['name']).strip()
store_to_edit['link'] = request.form.get('link', store_to_edit['link']).strip()
store_to_edit['description'] = request.form.get('description', store_to_edit.get('description', '')).strip()
store_to_edit['category'] = request.form.get('category')
store_to_edit['country'] = request.form.get('country')
store_to_edit['city'] = request.form.get('city')
store_to_edit['is_top'] = 'is_top' in request.form
if not (store_to_edit['link'].startswith('http://') or store_to_edit['link'].startswith('https://')):
flash(_('invalid_link_format'), 'error')
return redirect(url_for('admin'))
logo_file = request.files.get('logo')
if logo_file and logo_file.filename and HF_TOKEN_WRITE:
uploads_dir = 'uploads_temp'
os.makedirs(uploads_dir, exist_ok=True)
api = HfApi()
new_logo_filename = None
try:
ext = os.path.splitext(logo_file.filename)[1].lower()
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
flash(_('logo_not_image', logo_file.filename), "warning")
else:
safe_name = secure_filename(store_to_edit['name'].replace(' ', '_'))[:50]
new_logo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
temp_path = os.path.join(uploads_dir, new_logo_filename)
logo_file.save(temp_path)
api.upload_file(path_or_fileobj=temp_path, path_in_repo=f"logos/{new_logo_filename}",
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Update logo for store {store_to_edit['name']}")
os.remove(temp_path)
except Exception as e:
flash(_('logo_upload_error', logo_file.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_logo_filename:
old_logo = store_to_edit.get('logo')
if old_logo:
try:
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"logos/{old_logo}"],
repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Delete old logo for store {store_to_edit['name']}")
except Exception as e:
flash(_('old_logo_delete_fail'), "warning")
store_to_edit['logo'] = new_logo_filename
flash(_('logo_updated'), "success")
elif logo_file and logo_file.filename:
flash(_('new_logo_upload_fail'), "error")
elif logo_file and logo_file.filename and not HF_TOKEN_WRITE:
flash(_('hf_token_missing_logo_update'), "warning")
save_data(data)
flash(_('store_updated', store_to_edit['name']), 'success')
elif action == 'delete_store':
store_id = request.form.get('store_id')
store_index = next((i for i, s in enumerate(stores) if s.get('store_id') == store_id), -1)
if store_index == -1:
flash(_('store_delete_error', store_id), 'error')
return redirect(url_for('admin'))
deleted_store = stores.pop(store_index)
store_name = deleted_store.get('name', 'N/A')
logo_to_delete = deleted_store.get('logo')
if logo_to_delete and HF_TOKEN_WRITE:
try:
api = HfApi()
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"logos/{logo_to_delete}"],
repo_type="dataset", token=HF_TOKEN_WRITE,
commit_message=f"Delete logo for deleted store {store_name}")
except Exception as e:
flash(_('old_logo_delete_fail_on_store_delete', store_name), "warning")
elif logo_to_delete and not HF_TOKEN_WRITE:
flash(_('store_deleted_no_token', store_name), "warning")
data['stores'] = stores
save_data(data)
flash(_('store_deleted', store_name), 'success')
else:
flash(_('unknown_action', action), 'warning')
return redirect(url_for('admin'))
except Exception as e:
flash(_('internal_error', action), 'error')
return redirect(url_for('admin'))
current_data = load_data()
display_stores = sorted(current_data.get('stores', []), key=lambda s: s.get('name', '').lower())
display_categories = sorted(current_data.get('categories', []))
display_organization_info = current_data.get('organization_info', {})
return render_template_string(
ADMIN_TEMPLATE,
stores=display_stores,
categories=display_categories,
organization_info=display_organization_info,
repo_id=REPO_ID,
countries_and_cities=countries_and_cities
)
@app.route('/generate_description_ai', methods=['POST'])
def handle_generate_description_ai():
lang = get_locale()
_ = lambda key, *args: translations.get(lang, translations['en']).get(key, key).format(*args) if args else translations.get(lang, translations['en']).get(key, key)
request_data = request.get_json()
base64_image = request_data.get('image')
store_name = request_data.get('name')
language = request_data.get('language', 'Русский')
if not base64_image:
return jsonify({"error": _('ai_error_no_image')}), 400
if not store_name:
return jsonify({"error": _('store_name_link_required')}), 400
try:
image_data = base64.b64decode(base64_image)
result_text = generate_ai_description_from_image(image_data, store_name, language)
return jsonify({"text": result_text})
except ValueError as ve:
return jsonify({"error": str(ve)}), 400
except Exception as e:
return jsonify({"error": f"Internal server error: {e}"}), 500
@app.route('/chat_with_ai', methods=['POST'])
def handle_chat_with_ai():
request_data = request.get_json()
user_message = request_data.get('message')
chat_history_from_client = request_data.get('history', [])
if not user_message:
return jsonify({"error": "Message cannot be empty."}), 400
try:
ai_response_text = generate_chat_response(user_message, chat_history_from_client)
return jsonify({"text": ai_response_text})
except Exception as e:
return jsonify({"error": f"Chat error: {e}"}), 500
@app.route('/force_upload', methods=['POST'])
def force_upload():
lang = get_locale()
_ = lambda key, *args: translations.get(lang, translations['en']).get(key, key).format(*args) if args else translations.get(lang, translations['en']).get(key, key)
try:
upload_db_to_hf()
flash(_('force_upload_success'), 'success')
except Exception as e:
flash(_('force_upload_error', e), 'error')
return redirect(url_for('admin'))
@app.route('/force_download', methods=['POST'])
def force_download():
lang = get_locale()
_ = lambda key, *args: translations.get(lang, translations['en']).get(key, key).format(*args) if args else translations.get(lang, translations['en']).get(key, key)
try:
if download_db_from_hf():
flash(_('force_download_success'), 'success')
load_data()
else:
flash(_('force_download_error'), 'error')
except Exception as e:
flash(_('force_download_exception', e), 'error')
return redirect(url_for('admin'))
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()
port = int(os.environ.get('PORT', 7860))
app.run(debug=False, host='0.0.0.0', port=port)