Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -34,8 +34,8 @@ HF_TOKEN_WRITE = os.getenv("HF_TOKEN")
|
|
| 34 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 35 |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 36 |
|
| 37 |
-
|
| 38 |
-
|
| 39 |
|
| 40 |
CURRENCIES = {
|
| 41 |
'KGS': 'Кыргызский сом',
|
|
@@ -46,8 +46,13 @@ CURRENCIES = {
|
|
| 46 |
'EUR': 'Евро'
|
| 47 |
}
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 53 |
|
|
@@ -168,30 +173,36 @@ def get_env_data(env_id):
|
|
| 168 |
"about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров. Мы предлагаем широкий ассортимент продукции, от электроники до товаров для дома, всегда стремясь к качеству и доступности. Наша миссия — сделать ваш шопинг приятным и удобным, предлагая только лучшие товары, тщательно отобранные для вас.",
|
| 169 |
"shipping": "Доставка осуществляется по всему Кыргызстану. Стоимость и сроки доставки зависят от региона и веса товара. По Бишкеку доставка возможна в течение 1-2 рабочих дней, в регионы — от 3 до 7 дней. Для уточнения деталей свяжитесь с нами.",
|
| 170 |
"returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.",
|
| 171 |
-
"contact": f"Наш магазин находится по адресу:
|
| 172 |
-
|
|
|
|
|
|
|
| 173 |
"currency_code": "KGS",
|
| 174 |
-
"chat_name": "
|
| 175 |
"chat_avatar": None,
|
| 176 |
-
"
|
| 177 |
}
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
| 185 |
|
| 186 |
if 'products' not in env_data: env_data['products'] = []
|
| 187 |
if 'categories' not in env_data: env_data['categories'] = []
|
| 188 |
if 'orders' not in env_data: env_data['orders'] = {}
|
| 189 |
-
if 'organization_info' not in env_data:
|
| 190 |
-
env_data['organization_info'] = default_organization_info
|
| 191 |
-
else:
|
| 192 |
-
for key, value in default_organization_info.items():
|
| 193 |
-
env_data['organization_info'].setdefault(key, value)
|
| 194 |
if 'chats' not in env_data: env_data['chats'] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
|
| 196 |
products_changed = False
|
| 197 |
for product in env_data['products']:
|
|
@@ -199,11 +210,12 @@ def get_env_data(env_id):
|
|
| 199 |
product['product_id'] = uuid4().hex
|
| 200 |
products_changed = True
|
| 201 |
|
| 202 |
-
if products_changed:
|
| 203 |
save_env_data(env_id, env_data)
|
| 204 |
|
| 205 |
return env_data
|
| 206 |
|
|
|
|
| 207 |
def save_env_data(env_id, env_data):
|
| 208 |
all_data = load_data()
|
| 209 |
all_data[env_id] = env_data
|
|
@@ -283,10 +295,10 @@ def generate_chat_response(message, chat_history_from_client, env_id):
|
|
| 283 |
products = data.get('products', [])
|
| 284 |
categories = data.get('categories', [])
|
| 285 |
organization_info = data.get('organization_info', {})
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
product_info_list = []
|
| 291 |
for p in products:
|
| 292 |
if p.get('in_stock', True):
|
|
@@ -465,6 +477,40 @@ CATALOG_TEMPLATE = '''
|
|
| 465 |
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 466 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
| 467 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
:root {
|
| 469 |
--bg-dark: #003C43;
|
| 470 |
--bg-medium: #135D66;
|
|
@@ -475,6 +521,8 @@ CATALOG_TEMPLATE = '''
|
|
| 475 |
--danger: #E57373;
|
| 476 |
--danger-hover: #EF5350;
|
| 477 |
}
|
|
|
|
|
|
|
| 478 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 479 |
html { -webkit-tap-highlight-color: transparent; }
|
| 480 |
body {
|
|
@@ -694,7 +742,6 @@ CATALOG_TEMPLATE = '''
|
|
| 694 |
.chat-product-card-actions { display: flex; flex-direction: column; gap: 5px; }
|
| 695 |
.chat-product-link, .chat-add-to-cart { display: inline-block; background-color: #E0F2F1; color: var(--bg-medium); padding: 5px 10px; border-radius: 15px; cursor: pointer; font-size: 0.85rem; text-decoration: none; transition: background-color 0.2s; font-weight: 500; text-align: center; width: 100%; }
|
| 696 |
.chat-product-link:hover, .chat-add-to-cart:hover { background-color: #B2DFDB; }
|
| 697 |
-
.chat-product-card-actions .fa-cart-plus { font-size: 0.9em; }
|
| 698 |
|
| 699 |
</style>
|
| 700 |
</head>
|
|
@@ -702,7 +749,7 @@ CATALOG_TEMPLATE = '''
|
|
| 702 |
<div class="container">
|
| 703 |
<div class="top-bar">
|
| 704 |
<a href="{{ url_for('catalog', env_id=env_id) }}" class="logo">
|
| 705 |
-
<img src="
|
| 706 |
</a>
|
| 707 |
<div class="search-wrapper">
|
| 708 |
<i class="fas fa-search"></i>
|
|
@@ -1161,22 +1208,61 @@ CHAT_TEMPLATE = '''
|
|
| 1161 |
<head>
|
| 1162 |
<meta charset="UTF-8">
|
| 1163 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 1164 |
-
<title>{{ chat_name }}</title>
|
| 1165 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1166 |
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 1167 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
| 1168 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
:root {
|
| 1170 |
-
--bg-dark:
|
| 1171 |
-
--bg-medium:
|
| 1172 |
-
--accent:
|
| 1173 |
-
--accent-hover:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1174 |
--text-light: #E3FEF7;
|
| 1175 |
--text-dark: #333;
|
| 1176 |
--danger: #E57373;
|
| 1177 |
--danger-hover: #EF5350;
|
| 1178 |
--chat-bg: #f0f2f5;
|
| 1179 |
}
|
|
|
|
|
|
|
| 1180 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 1181 |
html { -webkit-tap-highlight-color: transparent; height: 100%; }
|
| 1182 |
body {
|
|
@@ -1207,7 +1293,7 @@ CHAT_TEMPLATE = '''
|
|
| 1207 |
flex-shrink: 0;
|
| 1208 |
}
|
| 1209 |
.chat-header a { color: var(--text-light); font-size: 1.2rem; text-decoration: none; }
|
| 1210 |
-
.chat-header .logo { width: 40px; height: 40px; border-radius: 50%; margin: 0 15px; border: 2px solid var(--accent);
|
| 1211 |
.chat-header h1 { font-size: 1.2rem; font-weight: 600; }
|
| 1212 |
#chat-messages {
|
| 1213 |
flex-grow: 1;
|
|
@@ -1326,12 +1412,8 @@ CHAT_TEMPLATE = '''
|
|
| 1326 |
<div class="chat-container">
|
| 1327 |
<div class="chat-header">
|
| 1328 |
<a href="{{ url_for('catalog', env_id=env_id) }}"><i class="fas fa-arrow-left"></i></a>
|
| 1329 |
-
{
|
| 1330 |
-
|
| 1331 |
-
{% else %}
|
| 1332 |
-
<img src="https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png" alt="Logo" class="logo">
|
| 1333 |
-
{% endif %}
|
| 1334 |
-
<h1>{{ chat_name }}</h1>
|
| 1335 |
</div>
|
| 1336 |
<div id="chat-messages"></div>
|
| 1337 |
<div class="chat-input-container">
|
|
@@ -1654,9 +1736,8 @@ CHAT_TEMPLATE = '''
|
|
| 1654 |
window.addEventListener('click', e => { if (e.target.classList.contains('modal')) closeModal(e.target.id); });
|
| 1655 |
window.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelectorAll('.modal').forEach(m => closeModal(m.id)); });
|
| 1656 |
|
| 1657 |
-
|
| 1658 |
-
|
| 1659 |
-
chatHistory.push({ role: 'ai', text: `Здравствуйте! Я ваш виртуальный помощник ${chatAssistantName}. Чем могу помочь?` });
|
| 1660 |
});
|
| 1661 |
</script>
|
| 1662 |
</body>
|
|
@@ -1849,7 +1930,7 @@ ORDER_TEMPLATE = '''
|
|
| 1849 |
return;
|
| 1850 |
}
|
| 1851 |
const orderId = document.getElementById('orderId').textContent;
|
| 1852 |
-
const whatsappNumber = "{{ whatsapp_number }}".replace(
|
| 1853 |
let message = `Здравствуйте! Хочу подтвердить или изменить свой заказ на Gippo312:%0A%0A`;
|
| 1854 |
message += `*Номер заказа:* ${orderId}%0A%0A`;
|
| 1855 |
|
|
@@ -1969,13 +2050,14 @@ ADMIN_TEMPLATE = '''
|
|
| 1969 |
.chat-message .bubble { display: inline-block; padding: 8px 12px; border-radius: 15px; max-width: 80%; }
|
| 1970 |
.chat-message.user .bubble { background-color: #dcf8c6; }
|
| 1971 |
.chat-message.ai .bubble { background-color: #f1f1f1; }
|
|
|
|
| 1972 |
</style>
|
| 1973 |
</head>
|
| 1974 |
<body>
|
| 1975 |
<div class="container">
|
| 1976 |
<div class="header">
|
| 1977 |
<div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
|
| 1978 |
-
<img src="
|
| 1979 |
<h1><i class="fas fa-tools"></i> Админ-панель Gippo312 (Среда: {{ env_id }})</h1>
|
| 1980 |
</div>
|
| 1981 |
<a href="{{ url_for('catalog', env_id=env_id) }}" class="button" style="background-color: var(--bg-medium); color: white;"><i class="fas fa-store"></i> Перейти в каталог</a>
|
|
@@ -2001,9 +2083,50 @@ ADMIN_TEMPLATE = '''
|
|
| 2001 |
</div>
|
| 2002 |
<p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
| 2003 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2004 |
|
| 2005 |
<div class="section">
|
| 2006 |
-
<h2><i class="fas fa-comments"></i> Диалоги с
|
| 2007 |
<div class="item-list">
|
| 2008 |
{% if chats %}
|
| 2009 |
{% for chat_id, chat_data in chats.items()|sort(reverse=True) %}
|
|
@@ -2057,52 +2180,23 @@ ADMIN_TEMPLATE = '''
|
|
| 2057 |
|
| 2058 |
<div class="flex-item">
|
| 2059 |
<div class="section">
|
| 2060 |
-
<h2><i class="fas fa-info-circle"></i>
|
| 2061 |
-
<details
|
| 2062 |
<summary><i class="fas fa-chevron-down"></i> Развернуть/Свернуть</summary>
|
| 2063 |
<div class="form-content">
|
| 2064 |
-
<form method="POST"
|
| 2065 |
<input type="hidden" name="action" value="update_org_info">
|
| 2066 |
-
<
|
| 2067 |
-
<label for="whatsapp_number">Номер WhatsApp для заказов:</label>
|
| 2068 |
-
<input type="tel" id="whatsapp_number" name="whatsapp_number" value="{{ organization_info.get('whatsapp_number', '') }}" placeholder="+996701202013">
|
| 2069 |
-
<label for="currency_code">Валюта магазина:</label>
|
| 2070 |
-
<select id="currency_code" name="currency_code">
|
| 2071 |
-
{% for code, name in currencies.items() %}
|
| 2072 |
-
<option value="{{ code }}" {% if organization_info.get('currency_code') == code %}selected{% endif %}>{{ name }} ({{ code }})</option>
|
| 2073 |
-
{% endfor %}
|
| 2074 |
-
</select>
|
| 2075 |
-
|
| 2076 |
-
<h3 style="margin-top:20px;">Настройки чата</h3>
|
| 2077 |
-
<label for="chat_name">Имя чата:</label>
|
| 2078 |
-
<input type="text" id="chat_name" name="chat_name" value="{{ organization_info.get('chat_name', '') }}">
|
| 2079 |
-
<label for="chat_avatar">Аватар чата (загрузка заменит текущий):</label>
|
| 2080 |
-
<input type="file" id="chat_avatar" name="chat_avatar" accept="image/*">
|
| 2081 |
-
{% if organization_info.get('chat_avatar') %}
|
| 2082 |
-
<div class="photo-preview" style="margin-top:5px;">
|
| 2083 |
-
<p style="font-size:0.8rem; margin-bottom:5px;">Текущий аватар:</p>
|
| 2084 |
-
<img src="https://huggingface.co/datasets/{{ repo_id }}/resolve/main/avatars/{{ organization_info.get('chat_avatar') }}" alt="Аватар чата">
|
| 2085 |
-
</div>
|
| 2086 |
-
{% endif %}
|
| 2087 |
-
<label for="chat_color_theme">Цветовая схема чата:</label>
|
| 2088 |
-
<select id="chat_color_theme" name="chat_color_theme">
|
| 2089 |
-
<option value="teal" {% if organization_info.get('chat_color_theme') == 'teal' %}selected{% endif %}>Бирюзовая (по умолч.)</option>
|
| 2090 |
-
<option value="blue" {% if organization_info.get('chat_color_theme') == 'blue' %}selected{% endif %}>Синяя</option>
|
| 2091 |
-
<option value="green" {% if organization_info.get('chat_color_theme') == 'green' %}selected{% endif %}>Зеленая</option>
|
| 2092 |
-
<option value="purple" {% if organization_info.get('chat_color_theme') == 'purple' %}selected{% endif %}>Фиолетовая</option>
|
| 2093 |
-
</select>
|
| 2094 |
-
|
| 2095 |
-
<h3 style="margin-top:20px;">Информация для ИИ-ассистента</h3>
|
| 2096 |
-
<label for="about_us">О нас:</label>
|
| 2097 |
<textarea id="about_us" name="about_us" rows="4">{{ organization_info.get('about_us', '') }}</textarea>
|
| 2098 |
-
<label for="shipping">Информация о
|
| 2099 |
<textarea id="shipping" name="shipping" rows="4">{{ organization_info.get('shipping', '') }}</textarea>
|
| 2100 |
-
<label for="returns">Информация о
|
| 2101 |
<textarea id="returns" name="returns" rows="4">{{ organization_info.get('returns', '') }}</textarea>
|
| 2102 |
-
<label for="contact">Контактная
|
| 2103 |
<textarea id="contact" name="contact" rows="4">{{ organization_info.get('contact', '') }}</textarea>
|
| 2104 |
-
<button type="submit" class="add-button"><i class="fas fa-save"></i>
|
| 2105 |
</form>
|
|
|
|
| 2106 |
</div>
|
| 2107 |
</details>
|
| 2108 |
</div>
|
|
@@ -2118,7 +2212,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2118 |
<input type="hidden" name="action" value="add_product">
|
| 2119 |
<label for="add_name">Название товара *:</label>
|
| 2120 |
<input type="text" id="add_name" name="name" required>
|
| 2121 |
-
<label for="add_price">Цена ({{
|
| 2122 |
<input type="number" id="add_price" name="price" step="0.01" min="0" required>
|
| 2123 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 2124 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
|
@@ -2190,7 +2284,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2190 |
{% endif %}
|
| 2191 |
</h3>
|
| 2192 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 2193 |
-
<p><strong>Цена:</strong> {{ "%.2f"|format(product.price) }} {{
|
| 2194 |
<p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
|
| 2195 |
{% set colors = product.get('colors', []) %}
|
| 2196 |
<p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
|
|
@@ -2216,7 +2310,7 @@ ADMIN_TEMPLATE = '''
|
|
| 2216 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 2217 |
<label>Название *:</label>
|
| 2218 |
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 2219 |
-
<label>Цена ({{
|
| 2220 |
<input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
|
| 2221 |
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
| 2222 |
<input type="file" id="edit_photos_{{ loop.index0 }}" name="photos" accept="image/*" multiple>
|
|
@@ -2464,7 +2558,26 @@ def create_environment():
|
|
| 2464 |
if new_id not in all_data:
|
| 2465 |
break
|
| 2466 |
|
| 2467 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2468 |
flash(f'Новая среда с ID {new_id} успешно создана.', 'success')
|
| 2469 |
return redirect(url_for('admhosto'))
|
| 2470 |
|
|
@@ -2483,8 +2596,7 @@ def delete_environment(env_id):
|
|
| 2483 |
def catalog(env_id):
|
| 2484 |
data = get_env_data(env_id)
|
| 2485 |
all_products_raw = data.get('products', [])
|
| 2486 |
-
|
| 2487 |
-
currency_code = org_info.get('currency_code', 'KGS')
|
| 2488 |
|
| 2489 |
product_categories = set(p.get('category', 'Без категории') for p in all_products_raw)
|
| 2490 |
admin_categories = set(data.get('categories', []))
|
|
@@ -2503,13 +2615,17 @@ def catalog(env_id):
|
|
| 2503 |
|
| 2504 |
ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)]
|
| 2505 |
|
|
|
|
|
|
|
| 2506 |
return render_template_string(
|
| 2507 |
CATALOG_TEMPLATE,
|
| 2508 |
products_by_category=products_by_category,
|
| 2509 |
ordered_categories=ordered_categories,
|
| 2510 |
products_json=json.dumps(products_sorted_for_js),
|
| 2511 |
repo_id=REPO_ID,
|
| 2512 |
-
currency_code=currency_code,
|
|
|
|
|
|
|
| 2513 |
env_id=env_id
|
| 2514 |
)
|
| 2515 |
|
|
@@ -2517,8 +2633,7 @@ def catalog(env_id):
|
|
| 2517 |
def product_detail(env_id, index):
|
| 2518 |
data = get_env_data(env_id)
|
| 2519 |
all_products_raw = data.get('products', [])
|
| 2520 |
-
|
| 2521 |
-
currency_code = org_info.get('currency_code', 'KGS')
|
| 2522 |
products_in_stock = [p for p in all_products_raw if p.get('in_stock', True)]
|
| 2523 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
| 2524 |
|
|
@@ -2531,7 +2646,7 @@ def product_detail(env_id, index):
|
|
| 2531 |
PRODUCT_DETAIL_TEMPLATE,
|
| 2532 |
product=product,
|
| 2533 |
repo_id=REPO_ID,
|
| 2534 |
-
currency_code=currency_code,
|
| 2535 |
env_id=env_id
|
| 2536 |
)
|
| 2537 |
|
|
@@ -2595,16 +2710,13 @@ def create_order(env_id):
|
|
| 2595 |
def view_order(env_id, order_id):
|
| 2596 |
data = get_env_data(env_id)
|
| 2597 |
order = data.get('orders', {}).get(order_id)
|
| 2598 |
-
|
| 2599 |
-
|
| 2600 |
-
whatsapp_number = org_info.get('whatsapp_number', DEFAULT_WHATSAPP_NUMBER)
|
| 2601 |
-
|
| 2602 |
-
|
| 2603 |
return render_template_string(ORDER_TEMPLATE,
|
| 2604 |
order=order,
|
| 2605 |
repo_id=REPO_ID,
|
| 2606 |
-
currency_code=currency_code,
|
| 2607 |
-
whatsapp_number=whatsapp_number,
|
| 2608 |
env_id=env_id)
|
| 2609 |
|
| 2610 |
|
|
@@ -2615,6 +2727,7 @@ def admin(env_id):
|
|
| 2615 |
categories = data.get('categories', [])
|
| 2616 |
organization_info = data.get('organization_info', {})
|
| 2617 |
chats = data.get('chats', {})
|
|
|
|
| 2618 |
|
| 2619 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 2620 |
data['orders'] = {}
|
|
@@ -2652,48 +2765,55 @@ def admin(env_id):
|
|
| 2652 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 2653 |
|
| 2654 |
elif action == 'update_org_info':
|
| 2655 |
-
organization_info['whatsapp_number'] = request.form.get('whatsapp_number', '').strip()
|
| 2656 |
-
organization_info['currency_code'] = request.form.get('currency_code', 'KGS')
|
| 2657 |
-
organization_info['chat_name'] = request.form.get('chat_name', '').strip()
|
| 2658 |
-
organization_info['chat_color_theme'] = request.form.get('chat_color_theme', 'teal')
|
| 2659 |
organization_info['about_us'] = request.form.get('about_us', '').strip()
|
| 2660 |
organization_info['shipping'] = request.form.get('shipping', '').strip()
|
| 2661 |
organization_info['returns'] = request.form.get('returns', '').strip()
|
| 2662 |
organization_info['contact'] = request.form.get('contact', '').strip()
|
| 2663 |
-
|
| 2664 |
-
|
| 2665 |
-
|
| 2666 |
-
|
| 2667 |
-
|
| 2668 |
-
|
| 2669 |
-
|
| 2670 |
-
|
| 2671 |
-
|
| 2672 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2673 |
|
| 2674 |
-
old_avatar =
|
| 2675 |
if old_avatar:
|
| 2676 |
try:
|
| 2677 |
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 2678 |
-
except
|
| 2679 |
pass
|
| 2680 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2681 |
api.upload_file(
|
| 2682 |
-
path_or_fileobj=
|
| 2683 |
path_in_repo=f"avatars/{avatar_filename}",
|
| 2684 |
-
repo_id=REPO_ID,
|
| 2685 |
-
repo_type="dataset",
|
| 2686 |
-
token=HF_TOKEN_WRITE,
|
| 2687 |
-
commit_message=f"Update chat avatar for env {env_id}"
|
| 2688 |
)
|
| 2689 |
-
|
| 2690 |
-
|
| 2691 |
-
|
| 2692 |
-
|
| 2693 |
-
|
| 2694 |
-
|
| 2695 |
-
|
| 2696 |
-
|
|
|
|
| 2697 |
save_env_data(env_id, data)
|
| 2698 |
flash("Настройки магазина и чата успешно обновлены.", 'success')
|
| 2699 |
|
|
@@ -2913,6 +3033,9 @@ def admin(env_id):
|
|
| 2913 |
display_categories = sorted(data.get('categories', []))
|
| 2914 |
display_organization_info = data.get('organization_info', {})
|
| 2915 |
display_chats = data.get('chats', {})
|
|
|
|
|
|
|
|
|
|
| 2916 |
|
| 2917 |
return render_template_string(
|
| 2918 |
ADMIN_TEMPLATE,
|
|
@@ -2920,8 +3043,12 @@ def admin(env_id):
|
|
| 2920 |
categories=display_categories,
|
| 2921 |
organization_info=display_organization_info,
|
| 2922 |
chats=display_chats,
|
|
|
|
| 2923 |
repo_id=REPO_ID,
|
|
|
|
|
|
|
| 2924 |
currencies=CURRENCIES,
|
|
|
|
| 2925 |
env_id=env_id
|
| 2926 |
)
|
| 2927 |
|
|
@@ -2977,31 +3104,20 @@ def handle_chat_with_ai(env_id):
|
|
| 2977 |
def chat_page(env_id):
|
| 2978 |
data = get_env_data(env_id)
|
| 2979 |
all_products_raw = data.get('products', [])
|
| 2980 |
-
|
| 2981 |
-
currency_code = org_info.get('currency_code', 'KGS')
|
| 2982 |
-
chat_name = org_info.get('chat_name', 'Чат с EVA')
|
| 2983 |
-
chat_avatar = org_info.get('chat_avatar')
|
| 2984 |
-
chat_color_theme = org_info.get('chat_color_theme', 'teal')
|
| 2985 |
-
|
| 2986 |
-
color_schemes = {
|
| 2987 |
-
"teal": {'bg-dark': '#003C43', 'bg-medium': '#135D66', 'accent': '#48D1CC', 'accent-hover': '#77E4D8'},
|
| 2988 |
-
"blue": {'bg-dark': '#012A4A', 'bg-medium': '#013A63', 'accent': '#2A6F97', 'accent-hover': '#468FAF'},
|
| 2989 |
-
"green": {'bg-dark': '#14452F', 'bg-medium': '#1A593E', 'accent': '#2A9161', 'accent-hover': '#3BB87B'},
|
| 2990 |
-
"purple": {'bg-dark': '#3C096C', 'bg-medium': '#5A189A', 'accent': '#9D4EDD', 'accent-hover': '#C77DFF'}
|
| 2991 |
-
}
|
| 2992 |
-
|
| 2993 |
products_in_stock = [p for p in all_products_raw if p.get('in_stock', True)]
|
| 2994 |
products_sorted_for_js = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
| 2995 |
|
|
|
|
|
|
|
| 2996 |
return render_template_string(
|
| 2997 |
CHAT_TEMPLATE,
|
| 2998 |
products_json=json.dumps(products_sorted_for_js),
|
| 2999 |
repo_id=REPO_ID,
|
| 3000 |
-
currency_code=currency_code,
|
| 3001 |
-
|
| 3002 |
-
|
| 3003 |
-
|
| 3004 |
-
chat_colors=color_schemes.get(chat_color_theme, color_schemes['teal'])
|
| 3005 |
)
|
| 3006 |
|
| 3007 |
@app.route('/<env_id>/get_chat/<chat_id>')
|
|
|
|
| 34 |
HF_TOKEN_READ = os.getenv("HF_TOKEN_READ")
|
| 35 |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY")
|
| 36 |
|
| 37 |
+
DOWNLOAD_RETRIES = 3
|
| 38 |
+
DOWNLOAD_DELAY = 5
|
| 39 |
|
| 40 |
CURRENCIES = {
|
| 41 |
'KGS': 'Кыргызский сом',
|
|
|
|
| 46 |
'EUR': 'Евро'
|
| 47 |
}
|
| 48 |
|
| 49 |
+
COLOR_SCHEMES = {
|
| 50 |
+
'default': 'Бирюзовый (по умолч.)',
|
| 51 |
+
'forest': 'Лесной зеленый',
|
| 52 |
+
'ocean': 'Глубокий синий',
|
| 53 |
+
'sunset': 'Закатный оранжевый'
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
|
| 57 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 58 |
|
|
|
|
| 173 |
"about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров. Мы предлагаем широкий ассортимент продукции, от электроники до товаров для дома, всегда стремясь к качеству и доступности. Наша миссия — сделать ваш шопинг приятным и удобным, предлагая только лучшие товары, тщательно отобранные для вас.",
|
| 174 |
"shipping": "Доставка осуществляется по всему Кыргызстану. Стоимость и сроки доставки зависят от региона и веса товара. По Бишкеку доставка возможна в течение 1-2 рабочих дней, в регионы — от 3 до 7 дней. Для уточнения деталей свяжитесь с нами.",
|
| 175 |
"returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.",
|
| 176 |
+
"contact": f"Наш магазин находится по адресу: Рынок Кербен, 6 ряд , 43 контейнер. Связаться с нами можно по телефону или через WhatsApp. Мы работаем ежедневно с 9:00 до 18:00."
|
| 177 |
+
}
|
| 178 |
+
default_settings = {
|
| 179 |
+
"whatsapp_number": "+996701202013",
|
| 180 |
"currency_code": "KGS",
|
| 181 |
+
"chat_name": "EVA",
|
| 182 |
"chat_avatar": None,
|
| 183 |
+
"color_scheme": "default"
|
| 184 |
}
|
| 185 |
+
|
| 186 |
+
env_data = all_data.get(env_id, {})
|
| 187 |
+
if not env_data:
|
| 188 |
+
env_data = {
|
| 189 |
+
'products': [], 'categories': [], 'orders': {}, 'chats': {},
|
| 190 |
+
'organization_info': default_organization_info,
|
| 191 |
+
'settings': default_settings
|
| 192 |
+
}
|
| 193 |
|
| 194 |
if 'products' not in env_data: env_data['products'] = []
|
| 195 |
if 'categories' not in env_data: env_data['categories'] = []
|
| 196 |
if 'orders' not in env_data: env_data['orders'] = {}
|
| 197 |
+
if 'organization_info' not in env_data: env_data['organization_info'] = default_organization_info
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
if 'chats' not in env_data: env_data['chats'] = {}
|
| 199 |
+
if 'settings' not in env_data: env_data['settings'] = default_settings
|
| 200 |
+
|
| 201 |
+
settings_changed = False
|
| 202 |
+
for key, value in default_settings.items():
|
| 203 |
+
if key not in env_data['settings']:
|
| 204 |
+
env_data['settings'][key] = value
|
| 205 |
+
settings_changed = True
|
| 206 |
|
| 207 |
products_changed = False
|
| 208 |
for product in env_data['products']:
|
|
|
|
| 210 |
product['product_id'] = uuid4().hex
|
| 211 |
products_changed = True
|
| 212 |
|
| 213 |
+
if products_changed or settings_changed:
|
| 214 |
save_env_data(env_id, env_data)
|
| 215 |
|
| 216 |
return env_data
|
| 217 |
|
| 218 |
+
|
| 219 |
def save_env_data(env_id, env_data):
|
| 220 |
all_data = load_data()
|
| 221 |
all_data[env_id] = env_data
|
|
|
|
| 295 |
products = data.get('products', [])
|
| 296 |
categories = data.get('categories', [])
|
| 297 |
organization_info = data.get('organization_info', {})
|
| 298 |
+
settings = data.get('settings', {})
|
| 299 |
+
currency_code = settings.get('currency_code', 'KGS')
|
| 300 |
+
chat_name = settings.get('chat_name', 'EVA')
|
| 301 |
+
|
| 302 |
product_info_list = []
|
| 303 |
for p in products:
|
| 304 |
if p.get('in_stock', True):
|
|
|
|
| 477 |
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 478 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
| 479 |
<style>
|
| 480 |
+
{% if settings.color_scheme == 'forest' %}
|
| 481 |
+
:root {
|
| 482 |
+
--bg-dark: #2F4F4F;
|
| 483 |
+
--bg-medium: #556B2F;
|
| 484 |
+
--accent: #90EE90;
|
| 485 |
+
--accent-hover: #98FB98;
|
| 486 |
+
--text-light: #F5F5DC;
|
| 487 |
+
--text-dark: #333;
|
| 488 |
+
--danger: #CD5C5C;
|
| 489 |
+
--danger-hover: #F08080;
|
| 490 |
+
}
|
| 491 |
+
{% elif settings.color_scheme == 'ocean' %}
|
| 492 |
+
:root {
|
| 493 |
+
--bg-dark: #000080;
|
| 494 |
+
--bg-medium: #1E90FF;
|
| 495 |
+
--accent: #87CEEB;
|
| 496 |
+
--accent-hover: #ADD8E6;
|
| 497 |
+
--text-light: #F0F8FF;
|
| 498 |
+
--text-dark: #333;
|
| 499 |
+
--danger: #FF6347;
|
| 500 |
+
--danger-hover: #FF4500;
|
| 501 |
+
}
|
| 502 |
+
{% elif settings.color_scheme == 'sunset' %}
|
| 503 |
+
:root {
|
| 504 |
+
--bg-dark: #8B4513;
|
| 505 |
+
--bg-medium: #D2691E;
|
| 506 |
+
--accent: #FFA500;
|
| 507 |
+
--accent-hover: #FFD700;
|
| 508 |
+
--text-light: #FFF8DC;
|
| 509 |
+
--text-dark: #333;
|
| 510 |
+
--danger: #DC143C;
|
| 511 |
+
--danger-hover: #FF0000;
|
| 512 |
+
}
|
| 513 |
+
{% else %}
|
| 514 |
:root {
|
| 515 |
--bg-dark: #003C43;
|
| 516 |
--bg-medium: #135D66;
|
|
|
|
| 521 |
--danger: #E57373;
|
| 522 |
--danger-hover: #EF5350;
|
| 523 |
}
|
| 524 |
+
{% endif %}
|
| 525 |
+
|
| 526 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 527 |
html { -webkit-tap-highlight-color: transparent; }
|
| 528 |
body {
|
|
|
|
| 742 |
.chat-product-card-actions { display: flex; flex-direction: column; gap: 5px; }
|
| 743 |
.chat-product-link, .chat-add-to-cart { display: inline-block; background-color: #E0F2F1; color: var(--bg-medium); padding: 5px 10px; border-radius: 15px; cursor: pointer; font-size: 0.85rem; text-decoration: none; transition: background-color 0.2s; font-weight: 500; text-align: center; width: 100%; }
|
| 744 |
.chat-product-link:hover, .chat-add-to-cart:hover { background-color: #B2DFDB; }
|
|
|
|
| 745 |
|
| 746 |
</style>
|
| 747 |
</head>
|
|
|
|
| 749 |
<div class="container">
|
| 750 |
<div class="top-bar">
|
| 751 |
<a href="{{ url_for('catalog', env_id=env_id) }}" class="logo">
|
| 752 |
+
<img src="{{ chat_avatar_url }}" alt="Logo">
|
| 753 |
</a>
|
| 754 |
<div class="search-wrapper">
|
| 755 |
<i class="fas fa-search"></i>
|
|
|
|
| 1208 |
<head>
|
| 1209 |
<meta charset="UTF-8">
|
| 1210 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
| 1211 |
+
<title>Gippo312 - Чат с {{ settings.chat_name }}</title>
|
| 1212 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
| 1213 |
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 1214 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Swiper/10.2.0/swiper-bundle.min.css">
|
| 1215 |
<style>
|
| 1216 |
+
{% if settings.color_scheme == 'forest' %}
|
| 1217 |
+
:root {
|
| 1218 |
+
--bg-dark: #2F4F4F;
|
| 1219 |
+
--bg-medium: #556B2F;
|
| 1220 |
+
--accent: #90EE90;
|
| 1221 |
+
--accent-hover: #98FB98;
|
| 1222 |
+
--text-light: #F5F5DC;
|
| 1223 |
+
--text-dark: #333;
|
| 1224 |
+
--danger: #CD5C5C;
|
| 1225 |
+
--danger-hover: #F08080;
|
| 1226 |
+
--chat-bg: #F5F5DC;
|
| 1227 |
+
}
|
| 1228 |
+
{% elif settings.color_scheme == 'ocean' %}
|
| 1229 |
:root {
|
| 1230 |
+
--bg-dark: #000080;
|
| 1231 |
+
--bg-medium: #1E90FF;
|
| 1232 |
+
--accent: #87CEEB;
|
| 1233 |
+
--accent-hover: #ADD8E6;
|
| 1234 |
+
--text-light: #F0F8FF;
|
| 1235 |
+
--text-dark: #333;
|
| 1236 |
+
--danger: #FF6347;
|
| 1237 |
+
--danger-hover: #FF4500;
|
| 1238 |
+
--chat-bg: #F0F8FF;
|
| 1239 |
+
}
|
| 1240 |
+
{% elif settings.color_scheme == 'sunset' %}
|
| 1241 |
+
:root {
|
| 1242 |
+
--bg-dark: #8B4513;
|
| 1243 |
+
--bg-medium: #D2691E;
|
| 1244 |
+
--accent: #FFA500;
|
| 1245 |
+
--accent-hover: #FFD700;
|
| 1246 |
+
--text-light: #FFF8DC;
|
| 1247 |
+
--text-dark: #333;
|
| 1248 |
+
--danger: #DC143C;
|
| 1249 |
+
--danger-hover: #FF0000;
|
| 1250 |
+
--chat-bg: #FFF8DC;
|
| 1251 |
+
}
|
| 1252 |
+
{% else %}
|
| 1253 |
+
:root {
|
| 1254 |
+
--bg-dark: #003C43;
|
| 1255 |
+
--bg-medium: #135D66;
|
| 1256 |
+
--accent: #48D1CC;
|
| 1257 |
+
--accent-hover: #77E4D8;
|
| 1258 |
--text-light: #E3FEF7;
|
| 1259 |
--text-dark: #333;
|
| 1260 |
--danger: #E57373;
|
| 1261 |
--danger-hover: #EF5350;
|
| 1262 |
--chat-bg: #f0f2f5;
|
| 1263 |
}
|
| 1264 |
+
{% endif %}
|
| 1265 |
+
|
| 1266 |
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 1267 |
html { -webkit-tap-highlight-color: transparent; height: 100%; }
|
| 1268 |
body {
|
|
|
|
| 1293 |
flex-shrink: 0;
|
| 1294 |
}
|
| 1295 |
.chat-header a { color: var(--text-light); font-size: 1.2rem; text-decoration: none; }
|
| 1296 |
+
.chat-header .logo { width: 40px; height: 40px; border-radius: 50%; margin: 0 15px; border: 2px solid var(--accent); }
|
| 1297 |
.chat-header h1 { font-size: 1.2rem; font-weight: 600; }
|
| 1298 |
#chat-messages {
|
| 1299 |
flex-grow: 1;
|
|
|
|
| 1412 |
<div class="chat-container">
|
| 1413 |
<div class="chat-header">
|
| 1414 |
<a href="{{ url_for('catalog', env_id=env_id) }}"><i class="fas fa-arrow-left"></i></a>
|
| 1415 |
+
<img src="{{ chat_avatar_url }}" alt="Logo" class="logo">
|
| 1416 |
+
<h1>Чат с {{ settings.chat_name }}</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1417 |
</div>
|
| 1418 |
<div id="chat-messages"></div>
|
| 1419 |
<div class="chat-input-container">
|
|
|
|
| 1736 |
window.addEventListener('click', e => { if (e.target.classList.contains('modal')) closeModal(e.target.id); });
|
| 1737 |
window.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelectorAll('.modal').forEach(m => closeModal(m.id)); });
|
| 1738 |
|
| 1739 |
+
addMessageToChatUI('👋 Здравствуйте! Я ваш виртуальный помощник {{ settings.chat_name }}. Чем могу помочь?', 'ai');
|
| 1740 |
+
chatHistory.push({ role: 'ai', text: 'Здравствуйте! Я ваш виртуальный помощник {{ settings.chat_name }}. Чем могу помочь?' });
|
|
|
|
| 1741 |
});
|
| 1742 |
</script>
|
| 1743 |
</body>
|
|
|
|
| 1930 |
return;
|
| 1931 |
}
|
| 1932 |
const orderId = document.getElementById('orderId').textContent;
|
| 1933 |
+
const whatsappNumber = "{{ whatsapp_number }}".replace(/[^0-9]/g, '');
|
| 1934 |
let message = `Здравствуйте! Хочу подтвердить или изменить свой заказ на Gippo312:%0A%0A`;
|
| 1935 |
message += `*Номер заказа:* ${orderId}%0A%0A`;
|
| 1936 |
|
|
|
|
| 2050 |
.chat-message .bubble { display: inline-block; padding: 8px 12px; border-radius: 15px; max-width: 80%; }
|
| 2051 |
.chat-message.user .bubble { background-color: #dcf8c6; }
|
| 2052 |
.chat-message.ai .bubble { background-color: #f1f1f1; }
|
| 2053 |
+
.current-avatar { max-width: 60px; max-height: 60px; border-radius: 50%; vertical-align: middle; margin-left: 10px; border: 2px solid var(--bg-medium);}
|
| 2054 |
</style>
|
| 2055 |
</head>
|
| 2056 |
<body>
|
| 2057 |
<div class="container">
|
| 2058 |
<div class="header">
|
| 2059 |
<div class="logo-title-container" style="display: flex; align-items: center; gap: 15px;">
|
| 2060 |
+
<img src="{{ chat_avatar_url }}" alt="Gippo312 Logo">
|
| 2061 |
<h1><i class="fas fa-tools"></i> Админ-панель Gippo312 (Среда: {{ env_id }})</h1>
|
| 2062 |
</div>
|
| 2063 |
<a href="{{ url_for('catalog', env_id=env_id) }}" class="button" style="background-color: var(--bg-medium); color: white;"><i class="fas fa-store"></i> Перейти в каталог</a>
|
|
|
|
| 2083 |
</div>
|
| 2084 |
<p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
| 2085 |
</div>
|
| 2086 |
+
|
| 2087 |
+
<div class="section">
|
| 2088 |
+
<h2><i class="fas fa-cog"></i> Настройки магазина и чата</h2>
|
| 2089 |
+
<details>
|
| 2090 |
+
<summary><i class="fas fa-chevron-down"></i> Развернуть/Свернуть</summary>
|
| 2091 |
+
<div class="form-content">
|
| 2092 |
+
<form method="POST" enctype="multipart/form-data">
|
| 2093 |
+
<input type="hidden" name="action" value="update_settings">
|
| 2094 |
+
|
| 2095 |
+
<label for="whatsapp_number">Номер WhatsApp для заказов:</label>
|
| 2096 |
+
<input type="tel" id="whatsapp_number" name="whatsapp_number" value="{{ settings.whatsapp_number }}" placeholder="+996XXXXXXXXX">
|
| 2097 |
+
|
| 2098 |
+
<label for="currency_code">Валюта магазина:</label>
|
| 2099 |
+
<select id="currency_code" name="currency_code">
|
| 2100 |
+
{% for code, name in currencies.items() %}
|
| 2101 |
+
<option value="{{ code }}" {% if settings.currency_code == code %}selected{% endif %}>{{ name }} ({{ code }})</option>
|
| 2102 |
+
{% endfor %}
|
| 2103 |
+
</select>
|
| 2104 |
+
|
| 2105 |
+
<label for="chat_name">Имя чат-ассистента:</label>
|
| 2106 |
+
<input type="text" id="chat_name" name="chat_name" value="{{ settings.chat_name }}">
|
| 2107 |
+
|
| 2108 |
+
<label for="chat_avatar">Аватар чата (загрузите новый, чтобы изменить):</label>
|
| 2109 |
+
<input type="file" id="chat_avatar" name="chat_avatar" accept="image/png, image/jpeg, image/gif, image/webp">
|
| 2110 |
+
{% if settings.chat_avatar %}
|
| 2111 |
+
<p style="font-size: 0.85rem; margin-top: 5px;">Текущий аватар: <img src="{{ chat_avatar_url }}" class="current-avatar"></p>
|
| 2112 |
+
{% endif %}
|
| 2113 |
+
|
| 2114 |
+
<label for="color_scheme">Цветовая схема каталога и чата:</label>
|
| 2115 |
+
<select id="color_scheme" name="color_scheme">
|
| 2116 |
+
{% for key, name in color_schemes.items() %}
|
| 2117 |
+
<option value="{{ key }}" {% if settings.color_scheme == key %}selected{% endif %}>{{ name }}</option>
|
| 2118 |
+
{% endfor %}
|
| 2119 |
+
</select>
|
| 2120 |
+
|
| 2121 |
+
<button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить настройки</button>
|
| 2122 |
+
</form>
|
| 2123 |
+
</div>
|
| 2124 |
+
</details>
|
| 2125 |
+
</div>
|
| 2126 |
+
|
| 2127 |
|
| 2128 |
<div class="section">
|
| 2129 |
+
<h2><i class="fas fa-comments"></i> Диалоги с {{ settings.chat_name }}</h2>
|
| 2130 |
<div class="item-list">
|
| 2131 |
{% if chats %}
|
| 2132 |
{% for chat_id, chat_data in chats.items()|sort(reverse=True) %}
|
|
|
|
| 2180 |
|
| 2181 |
<div class="flex-item">
|
| 2182 |
<div class="section">
|
| 2183 |
+
<h2><i class="fas fa-info-circle"></i> Информация о магазине</h2>
|
| 2184 |
+
<details>
|
| 2185 |
<summary><i class="fas fa-chevron-down"></i> Развернуть/Свернуть</summary>
|
| 2186 |
<div class="form-content">
|
| 2187 |
+
<form method="POST">
|
| 2188 |
<input type="hidden" name="action" value="update_org_info">
|
| 2189 |
+
<label for="about_us">О нас (для ИИ-ассистента):</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2190 |
<textarea id="about_us" name="about_us" rows="4">{{ organization_info.get('about_us', '') }}</textarea>
|
| 2191 |
+
<label for="shipping">Информация о доставке (для ИИ-ассистента):</label>
|
| 2192 |
<textarea id="shipping" name="shipping" rows="4">{{ organization_info.get('shipping', '') }}</textarea>
|
| 2193 |
+
<label for="returns">Информация о возврате/обмене (для ИИ-ассистента):</label>
|
| 2194 |
<textarea id="returns" name="returns" rows="4">{{ organization_info.get('returns', '') }}</textarea>
|
| 2195 |
+
<label for="contact">Контактная информация (для ИИ-ассистента):</label>
|
| 2196 |
<textarea id="contact" name="contact" rows="4">{{ organization_info.get('contact', '') }}</textarea>
|
| 2197 |
+
<button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить</button>
|
| 2198 |
</form>
|
| 2199 |
+
<p style="font-size: 0.85rem; color: #999;">Эта информация будет использоваться ИИ-ассистентом для ответов на вопросы о вашем магазине.</p>
|
| 2200 |
</div>
|
| 2201 |
</details>
|
| 2202 |
</div>
|
|
|
|
| 2212 |
<input type="hidden" name="action" value="add_product">
|
| 2213 |
<label for="add_name">Название товара *:</label>
|
| 2214 |
<input type="text" id="add_name" name="name" required>
|
| 2215 |
+
<label for="add_price">Цена ({{ currency_code }}) *:</label>
|
| 2216 |
<input type="number" id="add_price" name="price" step="0.01" min="0" required>
|
| 2217 |
<label for="add_photos">Фотографии (до 10 шт.):</label>
|
| 2218 |
<input type="file" id="add_photos" name="photos" accept="image/*" multiple>
|
|
|
|
| 2284 |
{% endif %}
|
| 2285 |
</h3>
|
| 2286 |
<p><strong>Категория:</strong> {{ product.get('category', 'Без категории') }}</p>
|
| 2287 |
+
<p><strong>Цена:</strong> {{ "%.2f"|format(product.price) }} {{ currency_code }}</p>
|
| 2288 |
<p class="description" title="{{ product.get('description', '') }}"><strong>Описание:</strong> {{ product.get('description', 'N/A')[:150] }}{% if product.get('description', '')|length > 150 %}...{% endif %}</p>
|
| 2289 |
{% set colors = product.get('colors', []) %}
|
| 2290 |
<p><strong>Цвета/Вар-ты:</strong> {{ colors|select('ne', '')|join(', ') if colors|select('ne', '')|list|length > 0 else 'Нет' }}</p>
|
|
|
|
| 2310 |
<input type="hidden" name="product_id" value="{{ product.get('product_id', '') }}">
|
| 2311 |
<label>Название *:</label>
|
| 2312 |
<input type="text" name="name" value="{{ product['name'] }}" required>
|
| 2313 |
+
<label>Цена ({{ currency_code }}) *:</label>
|
| 2314 |
<input type="number" name="price" step="0.01" min="0" value="{{ product['price'] }}" required>
|
| 2315 |
<label>Заменить фотографии (выберите новые файлы, до 10 шт.):</label>
|
| 2316 |
<input type="file" id="edit_photos_{{ loop.index0 }}" name="photos" accept="image/*" multiple>
|
|
|
|
| 2558 |
if new_id not in all_data:
|
| 2559 |
break
|
| 2560 |
|
| 2561 |
+
all_data[new_id] = {
|
| 2562 |
+
'products': [],
|
| 2563 |
+
'categories': [],
|
| 2564 |
+
'orders': {},
|
| 2565 |
+
'organization_info': {
|
| 2566 |
+
"about_us": "Мы — Gippo312, ваш надежный партнер в мире уникальных товаров.",
|
| 2567 |
+
"shipping": "Доставка осуществляется по всему Кыргызстану.",
|
| 2568 |
+
"returns": "Возврат и обмен товара возможен в течение 14 дней.",
|
| 2569 |
+
"contact": "Наш магазин находится по адресу: ... Связаться с нами можно по телефону ..."
|
| 2570 |
+
},
|
| 2571 |
+
'settings': {
|
| 2572 |
+
"whatsapp_number": "+996701202013",
|
| 2573 |
+
"currency_code": "KGS",
|
| 2574 |
+
"chat_name": "EVA",
|
| 2575 |
+
"chat_avatar": None,
|
| 2576 |
+
"color_scheme": "default"
|
| 2577 |
+
},
|
| 2578 |
+
'chats': {}
|
| 2579 |
+
}
|
| 2580 |
+
save_data(all_data)
|
| 2581 |
flash(f'Новая среда с ID {new_id} успешно создана.', 'success')
|
| 2582 |
return redirect(url_for('admhosto'))
|
| 2583 |
|
|
|
|
| 2596 |
def catalog(env_id):
|
| 2597 |
data = get_env_data(env_id)
|
| 2598 |
all_products_raw = data.get('products', [])
|
| 2599 |
+
settings = data.get('settings', {})
|
|
|
|
| 2600 |
|
| 2601 |
product_categories = set(p.get('category', 'Без категории') for p in all_products_raw)
|
| 2602 |
admin_categories = set(data.get('categories', []))
|
|
|
|
| 2615 |
|
| 2616 |
ordered_categories = [cat for cat in all_cat_names if products_by_category.get(cat)]
|
| 2617 |
|
| 2618 |
+
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
|
| 2619 |
+
|
| 2620 |
return render_template_string(
|
| 2621 |
CATALOG_TEMPLATE,
|
| 2622 |
products_by_category=products_by_category,
|
| 2623 |
ordered_categories=ordered_categories,
|
| 2624 |
products_json=json.dumps(products_sorted_for_js),
|
| 2625 |
repo_id=REPO_ID,
|
| 2626 |
+
currency_code=settings.get('currency_code', 'KGS'),
|
| 2627 |
+
settings=settings,
|
| 2628 |
+
chat_avatar_url=chat_avatar_url,
|
| 2629 |
env_id=env_id
|
| 2630 |
)
|
| 2631 |
|
|
|
|
| 2633 |
def product_detail(env_id, index):
|
| 2634 |
data = get_env_data(env_id)
|
| 2635 |
all_products_raw = data.get('products', [])
|
| 2636 |
+
settings = data.get('settings', {})
|
|
|
|
| 2637 |
products_in_stock = [p for p in all_products_raw if p.get('in_stock', True)]
|
| 2638 |
products_sorted = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
| 2639 |
|
|
|
|
| 2646 |
PRODUCT_DETAIL_TEMPLATE,
|
| 2647 |
product=product,
|
| 2648 |
repo_id=REPO_ID,
|
| 2649 |
+
currency_code=settings.get('currency_code', 'KGS'),
|
| 2650 |
env_id=env_id
|
| 2651 |
)
|
| 2652 |
|
|
|
|
| 2710 |
def view_order(env_id, order_id):
|
| 2711 |
data = get_env_data(env_id)
|
| 2712 |
order = data.get('orders', {}).get(order_id)
|
| 2713 |
+
settings = data.get('settings', {})
|
| 2714 |
+
|
|
|
|
|
|
|
|
|
|
| 2715 |
return render_template_string(ORDER_TEMPLATE,
|
| 2716 |
order=order,
|
| 2717 |
repo_id=REPO_ID,
|
| 2718 |
+
currency_code=settings.get('currency_code', 'KGS'),
|
| 2719 |
+
whatsapp_number=settings.get('whatsapp_number', ''),
|
| 2720 |
env_id=env_id)
|
| 2721 |
|
| 2722 |
|
|
|
|
| 2727 |
categories = data.get('categories', [])
|
| 2728 |
organization_info = data.get('organization_info', {})
|
| 2729 |
chats = data.get('chats', {})
|
| 2730 |
+
settings = data.get('settings', {})
|
| 2731 |
|
| 2732 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 2733 |
data['orders'] = {}
|
|
|
|
| 2765 |
flash(f"Не удалось удалить категорию '{category_to_delete}'.", 'error')
|
| 2766 |
|
| 2767 |
elif action == 'update_org_info':
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2768 |
organization_info['about_us'] = request.form.get('about_us', '').strip()
|
| 2769 |
organization_info['shipping'] = request.form.get('shipping', '').strip()
|
| 2770 |
organization_info['returns'] = request.form.get('returns', '').strip()
|
| 2771 |
organization_info['contact'] = request.form.get('contact', '').strip()
|
| 2772 |
+
data['organization_info'] = organization_info
|
| 2773 |
+
save_env_data(env_id, data)
|
| 2774 |
+
flash("Информация о магазине успешно обновлена.", 'success')
|
| 2775 |
+
|
| 2776 |
+
elif action == 'update_settings':
|
| 2777 |
+
settings['whatsapp_number'] = request.form.get('whatsapp_number', '').strip()
|
| 2778 |
+
settings['currency_code'] = request.form.get('currency_code', 'KGS')
|
| 2779 |
+
settings['chat_name'] = request.form.get('chat_name', 'EVA').strip()
|
| 2780 |
+
settings['color_scheme'] = request.form.get('color_scheme', 'default')
|
| 2781 |
+
|
| 2782 |
+
avatar_file = request.files.get('chat_avatar')
|
| 2783 |
+
if avatar_file and avatar_file.filename:
|
| 2784 |
+
if HF_TOKEN_WRITE:
|
| 2785 |
+
try:
|
| 2786 |
+
api = HfApi()
|
| 2787 |
|
| 2788 |
+
old_avatar = settings.get('chat_avatar')
|
| 2789 |
if old_avatar:
|
| 2790 |
try:
|
| 2791 |
api.delete_files(repo_id=REPO_ID, paths_in_repo=[f"avatars/{old_avatar}"], repo_type="dataset", token=HF_TOKEN_WRITE)
|
| 2792 |
+
except Exception:
|
| 2793 |
pass
|
| 2794 |
|
| 2795 |
+
ext = os.path.splitext(avatar_file.filename)[1].lower()
|
| 2796 |
+
avatar_filename = f"avatar_{env_id}_{int(time.time())}{ext}"
|
| 2797 |
+
|
| 2798 |
+
uploads_dir = 'uploads_temp'
|
| 2799 |
+
os.makedirs(uploads_dir, exist_ok=True)
|
| 2800 |
+
temp_path = os.path.join(uploads_dir, avatar_filename)
|
| 2801 |
+
avatar_file.save(temp_path)
|
| 2802 |
+
|
| 2803 |
api.upload_file(
|
| 2804 |
+
path_or_fileobj=temp_path,
|
| 2805 |
path_in_repo=f"avatars/{avatar_filename}",
|
| 2806 |
+
repo_id=REPO_ID, repo_type="dataset", token=HF_TOKEN_WRITE
|
|
|
|
|
|
|
|
|
|
| 2807 |
)
|
| 2808 |
+
settings['chat_avatar'] = avatar_filename
|
| 2809 |
+
os.remove(temp_path)
|
| 2810 |
+
flash("Аватар чата успешно обновлен.", 'success')
|
| 2811 |
+
except Exception as e:
|
| 2812 |
+
flash(f"Ошибка при загрузке аватара: {e}", 'error')
|
| 2813 |
+
else:
|
| 2814 |
+
flash("HF_TOKEN (write) не настроен. Аватар не был загружен.", "warning")
|
| 2815 |
+
|
| 2816 |
+
data['settings'] = settings
|
| 2817 |
save_env_data(env_id, data)
|
| 2818 |
flash("Настройки магазина и чата успешно обновлены.", 'success')
|
| 2819 |
|
|
|
|
| 3033 |
display_categories = sorted(data.get('categories', []))
|
| 3034 |
display_organization_info = data.get('organization_info', {})
|
| 3035 |
display_chats = data.get('chats', {})
|
| 3036 |
+
display_settings = data.get('settings', {})
|
| 3037 |
+
|
| 3038 |
+
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{display_settings['chat_avatar']}" if display_settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
|
| 3039 |
|
| 3040 |
return render_template_string(
|
| 3041 |
ADMIN_TEMPLATE,
|
|
|
|
| 3043 |
categories=display_categories,
|
| 3044 |
organization_info=display_organization_info,
|
| 3045 |
chats=display_chats,
|
| 3046 |
+
settings=display_settings,
|
| 3047 |
repo_id=REPO_ID,
|
| 3048 |
+
currency_code=display_settings.get('currency_code', 'KGS'),
|
| 3049 |
+
chat_avatar_url=chat_avatar_url,
|
| 3050 |
currencies=CURRENCIES,
|
| 3051 |
+
color_schemes=COLOR_SCHEMES,
|
| 3052 |
env_id=env_id
|
| 3053 |
)
|
| 3054 |
|
|
|
|
| 3104 |
def chat_page(env_id):
|
| 3105 |
data = get_env_data(env_id)
|
| 3106 |
all_products_raw = data.get('products', [])
|
| 3107 |
+
settings = data.get('settings', {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3108 |
products_in_stock = [p for p in all_products_raw if p.get('in_stock', True)]
|
| 3109 |
products_sorted_for_js = sorted(products_in_stock, key=lambda p: (not p.get('is_top', False), p.get('name', '').lower()))
|
| 3110 |
|
| 3111 |
+
chat_avatar_url = f"https://huggingface.co/datasets/{REPO_ID}/resolve/main/avatars/{settings['chat_avatar']}" if settings.get('chat_avatar') else "https://huggingface.co/spaces/gippo312/admin/resolve/main/Picsart_25-11-04_12-02-21-390.png"
|
| 3112 |
+
|
| 3113 |
return render_template_string(
|
| 3114 |
CHAT_TEMPLATE,
|
| 3115 |
products_json=json.dumps(products_sorted_for_js),
|
| 3116 |
repo_id=REPO_ID,
|
| 3117 |
+
currency_code=settings.get('currency_code', 'KGS'),
|
| 3118 |
+
settings=settings,
|
| 3119 |
+
chat_avatar_url=chat_avatar_url,
|
| 3120 |
+
env_id=env_id
|
|
|
|
| 3121 |
)
|
| 3122 |
|
| 3123 |
@app.route('/<env_id>/get_chat/<chat_id>')
|