|
|
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) |