Update app.py
Browse files
app.py
CHANGED
|
@@ -41,15 +41,14 @@ CURRENCY_NAME = 'Кыргызский сом'
|
|
| 41 |
DOWNLOAD_RETRIES = 3
|
| 42 |
DOWNLOAD_DELAY = 5
|
| 43 |
|
|
|
|
|
|
|
| 44 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 45 |
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 46 |
-
|
| 47 |
-
|
| 48 |
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
|
| 49 |
-
|
| 50 |
files_to_download = [specific_file] if specific_file else SYNC_FILES
|
| 51 |
all_successful = True
|
| 52 |
-
|
| 53 |
for file_name in files_to_download:
|
| 54 |
success = False
|
| 55 |
for attempt in range(retries + 1):
|
|
@@ -67,7 +66,7 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 67 |
success = True
|
| 68 |
break
|
| 69 |
except RepositoryNotFoundError:
|
| 70 |
-
|
| 71 |
except HfHubHTTPError as e:
|
| 72 |
if e.response.status_code == 404:
|
| 73 |
if attempt == 0 and not os.path.exists(file_name):
|
|
@@ -82,16 +81,13 @@ def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWN
|
|
| 82 |
else:
|
| 83 |
pass
|
| 84 |
except requests.exceptions.RequestException as e:
|
| 85 |
-
|
| 86 |
except Exception as e:
|
| 87 |
-
|
| 88 |
-
|
| 89 |
if attempt < retries:
|
| 90 |
time.sleep(delay)
|
| 91 |
-
|
| 92 |
if not success:
|
| 93 |
all_successful = False
|
| 94 |
-
|
| 95 |
return all_successful
|
| 96 |
|
| 97 |
def upload_db_to_hf(specific_file=None):
|
|
@@ -100,7 +96,6 @@ def upload_db_to_hf(specific_file=None):
|
|
| 100 |
try:
|
| 101 |
api = HfApi()
|
| 102 |
files_to_upload = [specific_file] if specific_file else SYNC_FILES
|
| 103 |
-
|
| 104 |
for file_name in files_to_upload:
|
| 105 |
if os.path.exists(file_name):
|
| 106 |
try:
|
|
@@ -113,7 +108,7 @@ def upload_db_to_hf(specific_file=None):
|
|
| 113 |
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 114 |
)
|
| 115 |
except Exception as e:
|
| 116 |
-
|
| 117 |
else:
|
| 118 |
pass
|
| 119 |
except Exception as e:
|
|
@@ -132,28 +127,33 @@ def load_data():
|
|
| 132 |
"returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.",
|
| 133 |
"contact": f"Наш магазин находится по адресу: {STORE_ADDRESS}. Связаться с нами можно по телефону: {WHATSAPP_NUMBER} или через WhatsApp по этому же номеру. Мы работаем ежедневно с 9:00 до 18:00."
|
| 134 |
}
|
| 135 |
-
|
|
|
|
|
|
|
| 136 |
data = default_data
|
| 137 |
try:
|
| 138 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 139 |
data = json.load(file)
|
| 140 |
if not isinstance(data, dict):
|
| 141 |
-
|
| 142 |
if 'products' not in data: data['products'] = []
|
| 143 |
if 'categories' not in data: data['categories'] = []
|
| 144 |
if 'orders' not in data: data['orders'] = {}
|
| 145 |
if 'organization_info' not in data: data['organization_info'] = default_organization_info
|
|
|
|
|
|
|
| 146 |
except FileNotFoundError:
|
| 147 |
if download_db_from_hf(specific_file=DATA_FILE):
|
| 148 |
try:
|
| 149 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 150 |
data = json.load(file)
|
| 151 |
-
if not isinstance(data, dict):
|
| 152 |
-
data = default_data
|
| 153 |
if 'products' not in data: data['products'] = []
|
| 154 |
if 'categories' not in data: data['categories'] = []
|
| 155 |
if 'orders' not in data: data['orders'] = {}
|
| 156 |
if 'organization_info' not in data: data['organization_info'] = default_organization_info
|
|
|
|
|
|
|
| 157 |
except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
|
| 158 |
data = default_data
|
| 159 |
else:
|
|
@@ -163,12 +163,13 @@ def load_data():
|
|
| 163 |
try:
|
| 164 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 165 |
data = json.load(file)
|
| 166 |
-
if not isinstance(data, dict):
|
| 167 |
-
data = default_data
|
| 168 |
if 'products' not in data: data['products'] = []
|
| 169 |
if 'categories' not in data: data['categories'] = []
|
| 170 |
if 'orders' not in data: data['orders'] = {}
|
| 171 |
if 'organization_info' not in data: data['organization_info'] = default_organization_info
|
|
|
|
|
|
|
| 172 |
except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
|
| 173 |
data = default_data
|
| 174 |
else:
|
|
@@ -198,6 +199,7 @@ def save_data(data):
|
|
| 198 |
if 'categories' not in data: data['categories'] = []
|
| 199 |
if 'orders' not in data: data['orders'] = {}
|
| 200 |
if 'organization_info' not in data: data['organization_info'] = {}
|
|
|
|
| 201 |
|
| 202 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 203 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
|
@@ -220,7 +222,7 @@ def generate_ai_description_from_image(image_data, language):
|
|
| 220 |
|
| 221 |
try:
|
| 222 |
if not image_data:
|
| 223 |
-
|
| 224 |
image_stream = io.BytesIO(image_data)
|
| 225 |
image = Image.open(image_stream).convert('RGB')
|
| 226 |
except Exception as e:
|
|
@@ -241,35 +243,32 @@ def generate_ai_description_from_image(image_data, language):
|
|
| 241 |
final_prompt = f"{base_prompt}{lang_suffix}"
|
| 242 |
|
| 243 |
try:
|
| 244 |
-
model = genai.GenerativeModel('
|
| 245 |
-
|
| 246 |
response = model.generate_content([final_prompt, image])
|
| 247 |
-
|
| 248 |
if hasattr(response, 'text'):
|
| 249 |
return response.text
|
| 250 |
else:
|
| 251 |
if response.parts:
|
| 252 |
-
|
| 253 |
else:
|
| 254 |
response.resolve()
|
| 255 |
return response.text
|
| 256 |
-
|
| 257 |
except Exception as e:
|
| 258 |
if "API key not valid" in str(e):
|
| 259 |
-
|
| 260 |
elif " Billing account not found" in str(e):
|
| 261 |
-
|
| 262 |
elif "Could not find model" in str(e):
|
| 263 |
-
|
| 264 |
elif "resource has been exhausted" in str(e).lower():
|
| 265 |
-
|
| 266 |
elif "content has been blocked" in str(e).lower():
|
| 267 |
-
|
| 268 |
-
|
| 269 |
reason = e.response.prompt_feedback.block_reason
|
| 270 |
-
|
| 271 |
else:
|
| 272 |
-
|
| 273 |
|
| 274 |
def generate_chat_response(message, chat_history_from_client):
|
| 275 |
if not configure_gemini():
|
|
@@ -301,7 +300,6 @@ def generate_chat_response(message, chat_history_from_client):
|
|
| 301 |
if organization_info.get("contact"):
|
| 302 |
org_info_str += f"Контактная информация: {organization_info['contact']}\n"
|
| 303 |
|
| 304 |
-
|
| 305 |
system_instruction_content = (
|
| 306 |
"Ты - доброжелательный и очень полезный виртуальный консультант для магазина O&CO. "
|
| 307 |
"Твоя задача - помогать пользователям находить товары, отвечать на вопросы о них, предлагать варианты, а также предоставлять информацию о магазине. "
|
|
@@ -323,7 +321,7 @@ def generate_chat_response(message, chat_history_from_client):
|
|
| 323 |
response = None
|
| 324 |
|
| 325 |
try:
|
| 326 |
-
model = genai.GenerativeModel('
|
| 327 |
|
| 328 |
model_chat_history_for_gemini = []
|
| 329 |
for entry in chat_history_from_client:
|
|
@@ -334,7 +332,6 @@ def generate_chat_response(message, chat_history_from_client):
|
|
| 334 |
})
|
| 335 |
|
| 336 |
chat = model.start_chat(history=model_chat_history_for_gemini)
|
| 337 |
-
|
| 338 |
response = chat.send_message(message, generation_config={'max_output_tokens': 1000})
|
| 339 |
|
| 340 |
if hasattr(response, 'text'):
|
|
@@ -347,82 +344,46 @@ def generate_chat_response(message, chat_history_from_client):
|
|
| 347 |
generated_text = response.text
|
| 348 |
else:
|
| 349 |
raise ValueError("AI did not return a valid text response.")
|
| 350 |
-
|
| 351 |
return generated_text
|
| 352 |
-
|
| 353 |
except Exception as e:
|
| 354 |
if "API key not valid" in str(e):
|
| 355 |
-
|
| 356 |
elif " Billing account not found" in str(e):
|
| 357 |
-
|
| 358 |
elif "Could not find model" in str(e):
|
| 359 |
-
|
| 360 |
elif "resource has been exhausted" in str(e).lower():
|
| 361 |
-
|
| 362 |
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):
|
| 363 |
-
|
| 364 |
-
|
| 365 |
else:
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
def generate_order_whatsapp_message_ai(order):
|
| 369 |
-
if not configure_gemini():
|
| 370 |
-
raise ValueError("Google AI API не настроен.")
|
| 371 |
-
|
| 372 |
-
customer_name = order.get('user_info', {}).get('name', 'клиент')
|
| 373 |
-
order_id = order.get('id', 'N/A')
|
| 374 |
-
total_price = f"{order.get('total_price', 0):.2f} {CURRENCY_CODE}"
|
| 375 |
-
product_summaries = []
|
| 376 |
-
for item in order.get('cart', []):
|
| 377 |
-
product_summaries.append(f"{item.get('name')} x{item.get('quantity')} ({item.get('price'):.2f} {CURRENCY_CODE} каждый)")
|
| 378 |
-
|
| 379 |
-
products_list_str = ", ".join(product_summaries) if product_summaries else "товары"
|
| 380 |
-
|
| 381 |
-
prompt = (
|
| 382 |
-
f"Ты - виртуальный помощник для магазина O&CO. Твоя задача - составить вежливое и информативное сообщение "
|
| 383 |
-
f"для клиента магазина по поводу его заказа. Используй информацию о заказе, но избегай уп��минания "
|
| 384 |
-
f"личной информации клиента (например, его номер телефона или адрес). "
|
| 385 |
-
f"Сообщение должно быть на русском языке. "
|
| 386 |
-
f"Предложи клиенту связаться для подтверждения и уточнения деталей доставки и оплаты."
|
| 387 |
-
f"Начни с обращения 'Добрый день, [Имя клиента]!' или 'Добрый день!' если имя не указано."
|
| 388 |
-
f"Примерный формат сообщения: 'Добрый день, [Имя клиента]! Ваш заказ №[ID заказа] на сумму [Сумма] {CURRENCY_CODE} с товарами [Список товаров] успешно создан. Пожалуйста, свяжитесь с нами для подтверждения и уточнения деталей доставки и оплаты. С уважением, команда O&CO.'. "
|
| 389 |
-
f"Сделай его чуть более развернутым, дружелюбным и профессиональным, но при этом лаконичным.\n\n"
|
| 390 |
-
f"Информация о заказе:\n"
|
| 391 |
-
f"Имя клиента: {customer_name}\n"
|
| 392 |
-
f"ID заказа: {order_id}\n"
|
| 393 |
-
f"Список товаров: {products_list_str}\n"
|
| 394 |
-
f"Общая сумма: {total_price}\n"
|
| 395 |
-
f"Текущая дата: {datetime.now().strftime('%Y-%m-%d')}"
|
| 396 |
-
)
|
| 397 |
|
|
|
|
| 398 |
try:
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
response.resolve()
|
| 408 |
-
return response.text
|
| 409 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
except Exception as e:
|
| 411 |
-
|
| 412 |
-
raise ValueError("Внутренняя ошибка конфигурации API.")
|
| 413 |
-
elif " Billing account not found" in str(e):
|
| 414 |
-
raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
|
| 415 |
-
elif "Could not find model" in str(e):
|
| 416 |
-
raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.")
|
| 417 |
-
elif "resource has been exhausted" in str(e).lower():
|
| 418 |
-
raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
|
| 419 |
-
elif "content has been blocked" in str(e).lower():
|
| 420 |
-
reason = "неизвестна"
|
| 421 |
-
if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback') and e.response.prompt_feedback.block_reason:
|
| 422 |
-
reason = e.response.prompt_feedback.block_reason
|
| 423 |
-
raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте изменить запрос.")
|
| 424 |
-
else:
|
| 425 |
-
raise ValueError(f"Ошибка при генерации контента: {e}")
|
| 426 |
|
| 427 |
CATALOG_TEMPLATE = '''
|
| 428 |
<!DOCTYPE html>
|
|
@@ -657,7 +618,6 @@ CATALOG_TEMPLATE = '''
|
|
| 657 |
#chat-send-button { background-color: #0070D1; color: white; border: none; border-radius: 20px; width: 40px; height: 40px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: background-color 0.3s; }
|
| 658 |
#chat-send-button:hover { background-color: #005CBF; }
|
| 659 |
#chat-send-button:disabled { background-color: #cccccc; cursor: not-allowed; }
|
| 660 |
-
|
| 661 |
.chat-product-card { background-color: #f0f2f5; border-radius: 12px; padding: 10px; margin-top: 8px; display: flex; align-items: center; gap: 12px; border: 1px solid #e0e0e0; }
|
| 662 |
.chat-product-card img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
|
| 663 |
.chat-product-card-info { flex-grow: 1; }
|
|
@@ -671,7 +631,6 @@ CATALOG_TEMPLATE = '''
|
|
| 671 |
}
|
| 672 |
.chat-product-link:hover, .chat-add-to-cart:hover { background-color: #BBDEFB; }
|
| 673 |
.chat-product-card-actions .fa-cart-plus { font-size: 0.9em; }
|
| 674 |
-
|
| 675 |
</style>
|
| 676 |
</head>
|
| 677 |
<body>
|
|
@@ -1328,10 +1287,9 @@ ORDER_TEMPLATE = '''
|
|
| 1328 |
.order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #0070D1; text-align: right; }
|
| 1329 |
.order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
|
| 1330 |
.order-summary strong { font-size: 1.3rem; color: #0A2A66; }
|
| 1331 |
-
.customer-info
|
| 1332 |
-
.customer-info
|
| 1333 |
-
.customer-info
|
| 1334 |
-
.customer-info-form input[type="tel"]::placeholder { color: #aaa; }
|
| 1335 |
.actions { margin-top: 30px; text-align: center; }
|
| 1336 |
.button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #0070D1; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
|
| 1337 |
.button:hover { background-color: #005CBF; }
|
|
@@ -1369,85 +1327,28 @@ ORDER_TEMPLATE = '''
|
|
| 1369 |
<p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
|
| 1370 |
</div>
|
| 1371 |
|
| 1372 |
-
<div class="customer-info
|
| 1373 |
-
<h2><i class="fas fa-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
<p><strong>WhatsApp:</strong> {{ order.user_info.whatsapp_number }}</p>
|
| 1377 |
-
<p style="font-size: 0.9rem; color: #666; margin-top: 15px;">Мы свяжемся с вами для подтверждения заказа.</p>
|
| 1378 |
-
{% else %}
|
| 1379 |
-
<p>Для подтверждения заказа и уточнения деталей, пожалуйста, укажите ваше имя и номер WhatsApp:</p>
|
| 1380 |
-
<label for="customer_name">Ваше имя:</label>
|
| 1381 |
-
<input type="text" id="customer_name" placeholder="Иван Иванов" required>
|
| 1382 |
-
<label for="customer_whatsapp">Ваш номер WhatsApp (начиная с +):</label>
|
| 1383 |
-
<input type="tel" id="customer_whatsapp" placeholder="+996XXXXXXXXX" required pattern="^\\+?[0-9]{7,15}$">
|
| 1384 |
-
{% endif %}
|
| 1385 |
</div>
|
| 1386 |
|
| 1387 |
<div class="actions">
|
| 1388 |
-
<button class="button" onclick="sendOrderViaWhatsApp(
|
| 1389 |
</div>
|
| 1390 |
|
| 1391 |
<a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
|
| 1392 |
|
| 1393 |
<script>
|
| 1394 |
-
|
| 1395 |
-
const
|
| 1396 |
-
const customerWhatsappInput = document.getElementById('customer_whatsapp');
|
| 1397 |
-
let customerName = customerNameInput ? customerNameInput.value.trim() : '{{ order.user_info.name if order.user_info else '' }}';
|
| 1398 |
-
let customerWhatsapp = customerWhatsappInput ? customerWhatsappInput.value.trim() : '{{ order.user_info.whatsapp_number if order.user_info else '' }}';
|
| 1399 |
-
|
| 1400 |
-
if (!customerName && customerNameInput) {
|
| 1401 |
-
alert("Пожалуйста, введите ваше имя.");
|
| 1402 |
-
customerNameInput.focus();
|
| 1403 |
-
return;
|
| 1404 |
-
}
|
| 1405 |
-
if (!customerWhatsapp && customerWhatsappInput) {
|
| 1406 |
-
alert("Пожалуйста, введите ваш номер WhatsApp.");
|
| 1407 |
-
customerWhatsappInput.focus();
|
| 1408 |
-
return;
|
| 1409 |
-
}
|
| 1410 |
-
if (customerWhatsapp && customerWhatsappInput && !customerWhatsappInput.checkValidity()) {
|
| 1411 |
-
alert("Пожалуйста, введите корректный номер WhatsApp (начиная с +).");
|
| 1412 |
-
customerWhatsappInput.focus();
|
| 1413 |
-
return;
|
| 1414 |
-
}
|
| 1415 |
-
|
| 1416 |
-
if (customerNameInput && customerWhatsappInput) {
|
| 1417 |
-
try {
|
| 1418 |
-
const updateResponse = await fetch(`/order/${orderId}/update_customer_info`, {
|
| 1419 |
-
method: 'POST',
|
| 1420 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1421 |
-
body: JSON.stringify({ name: customerName, whatsapp_number: customerWhatsapp })
|
| 1422 |
-
});
|
| 1423 |
-
if (!updateResponse.ok) {
|
| 1424 |
-
const errorData = await updateResponse.json();
|
| 1425 |
-
throw new Error(errorData.error || 'Не удалось обновить информацию о клиенте.');
|
| 1426 |
-
}
|
| 1427 |
-
const customerInfoForm = document.querySelector('.customer-info-form');
|
| 1428 |
-
if (customerInfoForm) {
|
| 1429 |
-
customerInfoForm.innerHTML = `
|
| 1430 |
-
<h2><i class="fas fa-user"></i> Контактная информация</h2>
|
| 1431 |
-
<p><strong>Имя:</strong> ${customerName}</p>
|
| 1432 |
-
<p><strong>WhatsApp:</strong> ${customerWhatsapp}</p>
|
| 1433 |
-
<p style="font-size: 0.9rem; color: #666; margin-top: 15px;">Мы свяжемся с вами для подтверждения заказа.</p>
|
| 1434 |
-
`;
|
| 1435 |
-
}
|
| 1436 |
-
} catch (error) {
|
| 1437 |
-
console.error('Ошибка при обновлении информации о клиенте:', error);
|
| 1438 |
-
alert(`Ошибка: ${error.message}`);
|
| 1439 |
-
return;
|
| 1440 |
-
}
|
| 1441 |
-
}
|
| 1442 |
-
|
| 1443 |
-
const whatsappNumber = "{{ whatsapp_number }}";
|
| 1444 |
const orderUrl = `{{ request.url }}`;
|
|
|
|
| 1445 |
|
| 1446 |
-
let message = `Здравствуйте! Хочу подтвердить свой заказ на OCO
|
| 1447 |
-
|
| 1448 |
-
|
| 1449 |
-
|
| 1450 |
-
message += `%0A%0AСсылка на заказ: ${encodeURIComponent(orderUrl)}%0A%0AПожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
|
| 1451 |
|
| 1452 |
const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
|
| 1453 |
window.open(whatsappUrl, '_blank');
|
|
@@ -1512,7 +1413,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1512 |
details { background-color: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
|
| 1513 |
details > summary { cursor: pointer; font-weight: 600; color: #303F9F; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
|
| 1514 |
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; }
|
| 1515 |
-
details[open] > summary::after {
|
| 1516 |
details[open] > summary { border-bottom: 1px solid #e0e0e0; }
|
| 1517 |
details .form-content { padding: 20px; }
|
| 1518 |
.color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
@@ -1537,24 +1438,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1537 |
.status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
|
| 1538 |
.ai-generate-button { background-color: #8D6EC8; margin-top: 5px; margin-bottom: 10px; }
|
| 1539 |
.ai-generate-button:hover { background-color: #7B4DB5; }
|
| 1540 |
-
|
| 1541 |
-
.order-item-admin { display: grid; grid-template-columns: 100px 1fr 150px; gap: 15px; padding: 15px 0; border-bottom: 1px solid #f0f0f0; align-items: center;}
|
| 1542 |
-
.order-item-admin:last-child { border-bottom: none; }
|
| 1543 |
-
.order-item-admin .order-id { font-weight: bold; font-size: 1.1rem; color: #333; }
|
| 1544 |
-
.order-item-admin .order-meta-info p { margin: 3px 0; font-size: 0.85rem; color: #666; }
|
| 1545 |
-
.order-item-admin .order-actions button { margin-top: 0; padding: 8px 12px; font-size: 0.8rem; }
|
| 1546 |
-
.order-details-modal { max-width: 600px; }
|
| 1547 |
-
.order-details-modal h3 { font-size: 1.3rem; margin-bottom: 15px; }
|
| 1548 |
-
.order-details-modal .detail-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px dotted #eee; }
|
| 1549 |
-
.order-details-modal .detail-row:last-child { border-bottom: none; }
|
| 1550 |
-
.order-details-modal .detail-row strong { color: #004282; }
|
| 1551 |
-
.modal { display: none; position: fixed; z-index: 1001; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6); backdrop-filter: blur(5px); overflow-y: auto; }
|
| 1552 |
-
.modal-content { background: #ffffff; color: #333; margin: 5% auto; padding: 25px; border-radius: 15px; width: 90%; max-width: 700px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); animation: slideIn 0.3s ease-out; position: relative; }
|
| 1553 |
-
.close { position: absolute; top: 15px; right: 15px; font-size: 1.8rem; color: #aaa; cursor: pointer; transition: color 0.3s; line-height: 1; }
|
| 1554 |
-
.close:hover { color: #666; }
|
| 1555 |
-
@keyframes slideIn { from { transform: translateY(-30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
| 1556 |
-
.modal-content h2 { margin-top: 0; margin-bottom: 20px; color: #0A2A66; display: flex; align-items: center; gap: 10px;}
|
| 1557 |
-
.modal-content h3 { font-size: 1.3rem; margin-top: 20px; margin-bottom: 10px; color: #004282;}
|
| 1558 |
</style>
|
| 1559 |
</head>
|
| 1560 |
<body>
|
|
@@ -1582,12 +1466,39 @@ ADMIN_TEMPLATE = '''
|
|
| 1582 |
<button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
|
| 1583 |
</form>
|
| 1584 |
<form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
|
| 1585 |
-
<button type="submit" class="button download-hf-button" title="Скачать файлы
|
| 1586 |
</form>
|
| 1587 |
</div>
|
| 1588 |
<p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
| 1589 |
</div>
|
| 1590 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1591 |
<div class="flex-container">
|
| 1592 |
<div class="flex-item">
|
| 1593 |
<div class="section">
|
|
@@ -1826,50 +1737,6 @@ ADMIN_TEMPLATE = '''
|
|
| 1826 |
{% endif %}
|
| 1827 |
</div>
|
| 1828 |
|
| 1829 |
-
<div class="section">
|
| 1830 |
-
<h2><i class="fas fa-clipboard-list"></i> Управление заказами</h2>
|
| 1831 |
-
{% if orders %}
|
| 1832 |
-
<div class="item-list">
|
| 1833 |
-
{% for order_id, order in orders.items() %}
|
| 1834 |
-
<div class="item order-item-admin">
|
| 1835 |
-
<div class="order-id">
|
| 1836 |
-
№{{ order.id }}
|
| 1837 |
-
</div>
|
| 1838 |
-
<div class="order-meta-info">
|
| 1839 |
-
<p><strong>Дата:</strong> {{ order.created_at }}</p>
|
| 1840 |
-
<p><strong>Клиент:</strong> {{ order.user_info.name if order.user_info and order.user_info.name else 'Не указано' }}</p>
|
| 1841 |
-
<p><strong>WhatsApp:</strong> {{ order.user_info.whatsapp_number if order.user_info and order.user_info.whatsapp_number else 'Не указано' }}</p>
|
| 1842 |
-
<p><strong>Сумма:</strong> {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</p>
|
| 1843 |
-
<p><strong>Товаров:</strong> {{ order.cart|length }}</p>
|
| 1844 |
-
</div>
|
| 1845 |
-
<div class="order-actions">
|
| 1846 |
-
<button class="button" onclick="openOrderDetailsModal('{{ order.id }}')"><i class="fas fa-info"></i> Детали</button>
|
| 1847 |
-
{% if order.user_info and order.user_info.whatsapp_number %}
|
| 1848 |
-
<button class="button ai-generate-button" onclick="generateAndSendWhatsAppMessage('{{ order.id }}', '{{ order.user_info.name if order.user_info.name else '' }}', '{{ order.user_info.whatsapp_number }}')">
|
| 1849 |
-
<i class="fab fa-whatsapp"></i> AI-сообщение
|
| 1850 |
-
</button>
|
| 1851 |
-
{% endif %}
|
| 1852 |
-
<form method="POST" style="margin:0;" onsubmit="return confirm('Вы уверены, что хотите удалить заказ №{{ order.id }}?');">
|
| 1853 |
-
<input type="hidden" name="action" value="delete_order">
|
| 1854 |
-
<input type="hidden" name="order_id" value="{{ order.id }}">
|
| 1855 |
-
<button type="submit" class="delete-button"><i class="fas fa-trash-alt"></i> Удалить</button>
|
| 1856 |
-
</form>
|
| 1857 |
-
</div>
|
| 1858 |
-
</div>
|
| 1859 |
-
{% endfor %}
|
| 1860 |
-
</div>
|
| 1861 |
-
{% else %}
|
| 1862 |
-
<p>Заказов пока нет.</p>
|
| 1863 |
-
{% endif %}
|
| 1864 |
-
</div>
|
| 1865 |
-
|
| 1866 |
-
</div>
|
| 1867 |
-
|
| 1868 |
-
<div id="orderDetailsModal" class="modal">
|
| 1869 |
-
<div class="modal-content order-details-modal">
|
| 1870 |
-
<span class="close" onclick="closeModal('orderDetailsModal')" aria-label="Закрыть">×</span>
|
| 1871 |
-
<div id="modalOrderDetailsContent">Загрузка...</div>
|
| 1872 |
-
</div>
|
| 1873 |
</div>
|
| 1874 |
|
| 1875 |
<script>
|
|
@@ -1920,7 +1787,7 @@ ADMIN_TEMPLATE = '''
|
|
| 1920 |
const photoInput = document.getElementById(photoInputId);
|
| 1921 |
const descriptionTextarea = document.getElementById(descriptionTextareaId);
|
| 1922 |
const languageSelect = document.getElementById(languageSelectId);
|
| 1923 |
-
const generateButton = descriptionTextarea.nextElementSibling;
|
| 1924 |
|
| 1925 |
if (!photoInput || !descriptionTextarea || !languageSelect || !generateButton) {
|
| 1926 |
alert("Ошибка: Не найдены элементы формы для генерации.");
|
|
@@ -1980,105 +1847,11 @@ ADMIN_TEMPLATE = '''
|
|
| 1980 |
};
|
| 1981 |
reader.readAsDataURL(file);
|
| 1982 |
}
|
| 1983 |
-
|
| 1984 |
-
function openModal(modalId) {
|
| 1985 |
-
const modal = document.getElementById(modalId);
|
| 1986 |
-
if (modal) {
|
| 1987 |
-
modal.style.display = "block";
|
| 1988 |
-
document.body.style.overflow = 'hidden';
|
| 1989 |
-
}
|
| 1990 |
-
}
|
| 1991 |
-
|
| 1992 |
-
function closeModal(modalId) {
|
| 1993 |
-
const modal = document.getElementById(modalId);
|
| 1994 |
-
if (modal) {
|
| 1995 |
-
modal.style.display = "none";
|
| 1996 |
-
}
|
| 1997 |
-
const anyModalOpen = document.querySelector('.modal[style*="display: block"]');
|
| 1998 |
-
if (!anyModalOpen) {
|
| 1999 |
-
document.body.style.overflow = 'auto';
|
| 2000 |
-
}
|
| 2001 |
-
}
|
| 2002 |
-
|
| 2003 |
-
async function openOrderDetailsModal(orderId) {
|
| 2004 |
-
const modalContent = document.getElementById('modalOrderDetailsContent');
|
| 2005 |
-
if (!modalContent) return;
|
| 2006 |
-
modalContent.innerHTML = '<p style="text-align:center; padding: 40px;">Загрузка...</p>';
|
| 2007 |
-
try {
|
| 2008 |
-
const response = await fetch(`/admin/order_details_html/${orderId}`);
|
| 2009 |
-
if (!response.ok) {
|
| 2010 |
-
throw new Error(`Ошибка ${response.status}: ${response.statusText}`);
|
| 2011 |
-
}
|
| 2012 |
-
const html = await response.text();
|
| 2013 |
-
modalContent.innerHTML = html;
|
| 2014 |
-
openModal('orderDetailsModal');
|
| 2015 |
-
} catch (error) {
|
| 2016 |
-
console.error('Ошибка загрузки деталей заказа:', error);
|
| 2017 |
-
modalContent.innerHTML = `<p style="color: #dc3545; text-align:center; padding: 40px;">Не удалось загрузить информацию о заказе. ${error.message}</p>`;
|
| 2018 |
-
}
|
| 2019 |
-
}
|
| 2020 |
-
|
| 2021 |
-
async function generateAndSendWhatsAppMessage(orderId, customerName, customerWhatsapp) {
|
| 2022 |
-
const button = event.currentTarget;
|
| 2023 |
-
button.disabled = true;
|
| 2024 |
-
const originalText = button.innerHTML;
|
| 2025 |
-
button.innerHTML = '<i class="fas fa-sync fa-spin"></i> Генерация...';
|
| 2026 |
-
|
| 2027 |
-
try {
|
| 2028 |
-
const response = await fetch(`/admin/generate_whatsapp_message/${orderId}`);
|
| 2029 |
-
if (!response.ok) {
|
| 2030 |
-
const errorData = await response.json();
|
| 2031 |
-
throw new Error(errorData.error || `Ошибка сервера: ${response.status}`);
|
| 2032 |
-
}
|
| 2033 |
-
const result = await response.json();
|
| 2034 |
-
const aiMessage = result.message;
|
| 2035 |
-
|
| 2036 |
-
let finalMessage = `${aiMessage}`;
|
| 2037 |
-
|
| 2038 |
-
const whatsappUrl = `https://api.whatsapp.com/send?phone=${customerWhatsapp}&text=${encodeURIComponent(finalMessage)}`;
|
| 2039 |
-
window.open(whatsappUrl, '_blank');
|
| 2040 |
-
} catch (error) {
|
| 2041 |
-
console.error('Ошибка генерации WhatsApp сообщения:', error);
|
| 2042 |
-
alert(`Ошибка: ${error.message}`);
|
| 2043 |
-
} finally {
|
| 2044 |
-
button.disabled = false;
|
| 2045 |
-
button.innerHTML = originalText;
|
| 2046 |
-
}
|
| 2047 |
-
}
|
| 2048 |
</script>
|
| 2049 |
</body>
|
| 2050 |
</html>
|
| 2051 |
'''
|
| 2052 |
|
| 2053 |
-
ADMIN_ORDER_DETAILS_MODAL_HTML = '''
|
| 2054 |
-
<div style="padding: 10px;">
|
| 2055 |
-
<h3>Детали Заказа №{{ order.id }}</h3>
|
| 2056 |
-
<div class="detail-row"><strong>Дата создания:</strong> <span>{{ order.created_at }}</span></div>
|
| 2057 |
-
<div class="detail-row"><strong>Статус:</strong> <span>{{ order.status }}</span></div>
|
| 2058 |
-
<div class="detail-row"><strong>Клиент:</strong> <span>{{ order.user_info.name if order.user_info and order.user_info.name else 'Не указано' }}</span></div>
|
| 2059 |
-
<div class="detail-row"><strong>WhatsApp:</strong> <span>{{ order.user_info.whatsapp_number if order.user_info and order.user_info.whatsapp_number else 'Не указано' }}</span></div>
|
| 2060 |
-
<div class="detail-row"><strong>Общая сумма:</strong> <span>{{ "%.2f"|format(order.total_price) }} {{ currency_code }}</span></div>
|
| 2061 |
-
|
| 2062 |
-
<h3 style="margin-top: 25px;">Товары:</h3>
|
| 2063 |
-
{% if order.cart %}
|
| 2064 |
-
{% for item in order.cart %}
|
| 2065 |
-
<div style="display: flex; gap: 10px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px dotted #eee; align-items: center;">
|
| 2066 |
-
<img src="{{ item.photo_url }}" alt="{{ item.name }}" style="width: 50px; height: 50px; object-fit: contain; border-radius: 5px;">
|
| 2067 |
-
<div>
|
| 2068 |
-
<strong>{{ item.name }} {% if item.color != 'N/A' %}({{ item.color }}){% endif %}</strong><br>
|
| 2069 |
-
<span>{{ "%.2f"|format(item.price) }} {{ currency_code }} × {{ item.quantity }}</span>
|
| 2070 |
-
</div>
|
| 2071 |
-
<div style="margin-left: auto; font-weight: bold;">
|
| 2072 |
-
{{ "%.2f"|format(item.price * item.quantity) }} {{ currency_code }}
|
| 2073 |
-
</div>
|
| 2074 |
-
</div>
|
| 2075 |
-
{% endfor %}
|
| 2076 |
-
{% else %}
|
| 2077 |
-
<p>Товары в заказе отсутствуют.</p>
|
| 2078 |
-
{% endif %}
|
| 2079 |
-
</div>
|
| 2080 |
-
'''
|
| 2081 |
-
|
| 2082 |
@app.route('/')
|
| 2083 |
def catalog():
|
| 2084 |
data = load_data()
|
|
@@ -2139,7 +1912,6 @@ def create_order():
|
|
| 2139 |
return jsonify({"error": "Корзина пуста или не передана."}), 400
|
| 2140 |
|
| 2141 |
cart_items = order_data['cart']
|
| 2142 |
-
|
| 2143 |
total_price = 0
|
| 2144 |
processed_cart = []
|
| 2145 |
for item in cart_items:
|
|
@@ -2179,76 +1951,22 @@ def create_order():
|
|
| 2179 |
data = load_data()
|
| 2180 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 2181 |
data['orders'] = {}
|
| 2182 |
-
|
| 2183 |
data['orders'][order_id] = new_order
|
| 2184 |
save_data(data)
|
| 2185 |
return jsonify({"order_id": order_id}), 201
|
| 2186 |
-
|
| 2187 |
except Exception as e:
|
| 2188 |
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
|
| 2189 |
|
| 2190 |
-
|
| 2191 |
@app.route('/order/<order_id>')
|
| 2192 |
def view_order(order_id):
|
| 2193 |
data = load_data()
|
| 2194 |
order = data.get('orders', {}).get(order_id)
|
| 2195 |
-
|
| 2196 |
return render_template_string(ORDER_TEMPLATE,
|
| 2197 |
order=order,
|
| 2198 |
repo_id=REPO_ID,
|
| 2199 |
currency_code=CURRENCY_CODE,
|
| 2200 |
whatsapp_number=WHATSAPP_NUMBER)
|
| 2201 |
|
| 2202 |
-
@app.route('/order/<order_id>/update_customer_info', methods=['POST'])
|
| 2203 |
-
def update_order_customer_info(order_id):
|
| 2204 |
-
data = load_data()
|
| 2205 |
-
order = data.get('orders', {}).get(order_id)
|
| 2206 |
-
|
| 2207 |
-
if not order:
|
| 2208 |
-
return jsonify({"error": "Заказ не найден."}), 404
|
| 2209 |
-
|
| 2210 |
-
customer_info = request.get_json()
|
| 2211 |
-
name = customer_info.get('name', '').strip()
|
| 2212 |
-
whatsapp_number = customer_info.get('whatsapp_number', '').strip()
|
| 2213 |
-
|
| 2214 |
-
if not name or not whatsapp_number:
|
| 2215 |
-
return jsonify({"error": "Имя и номер WhatsApp обязательны."}), 400
|
| 2216 |
-
|
| 2217 |
-
order['user_info'] = {
|
| 2218 |
-
'name': name,
|
| 2219 |
-
'whatsapp_number': whatsapp_number
|
| 2220 |
-
}
|
| 2221 |
-
save_data(data)
|
| 2222 |
-
return jsonify({"message": "Информация о клиенте успешно обновлена."}), 200
|
| 2223 |
-
|
| 2224 |
-
@app.route('/admin/order_details_html/<order_id>')
|
| 2225 |
-
def admin_order_details_html(order_id):
|
| 2226 |
-
data = load_data()
|
| 2227 |
-
order = data.get('orders', {}).get(order_id)
|
| 2228 |
-
if not order:
|
| 2229 |
-
return "<p style='text-align: center; color: #dc3545;'>Заказ не найден.</p>", 404
|
| 2230 |
-
return render_template_string(ADMIN_ORDER_DETAILS_MODAL_HTML, order=order, currency_code=CURRENCY_CODE)
|
| 2231 |
-
|
| 2232 |
-
@app.route('/admin/generate_whatsapp_message/<order_id>')
|
| 2233 |
-
def generate_admin_whatsapp_message(order_id):
|
| 2234 |
-
data = load_data()
|
| 2235 |
-
order = data.get('orders', {}).get(order_id)
|
| 2236 |
-
|
| 2237 |
-
if not order:
|
| 2238 |
-
return jsonify({"error": "Заказ не найден."}), 404
|
| 2239 |
-
|
| 2240 |
-
customer_whatsapp = order.get('user_info', {}).get('whatsapp_number')
|
| 2241 |
-
if not customer_whatsapp:
|
| 2242 |
-
return jsonify({"error": "Номер WhatsApp клиента не указан для этого заказа."}), 400
|
| 2243 |
-
|
| 2244 |
-
try:
|
| 2245 |
-
ai_message = generate_order_whatsapp_message_ai(order)
|
| 2246 |
-
return jsonify({"message": ai_message})
|
| 2247 |
-
except ValueError as ve:
|
| 2248 |
-
return jsonify({"error": str(ve)}), 400
|
| 2249 |
-
except Exception as e:
|
| 2250 |
-
return jsonify({"error": f"Ошибка генерации сообщения: {e}"}), 500
|
| 2251 |
-
|
| 2252 |
|
| 2253 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 2254 |
def admin():
|
|
@@ -2256,11 +1974,13 @@ def admin():
|
|
| 2256 |
products = data.get('products', [])
|
| 2257 |
categories = data.get('categories', [])
|
| 2258 |
organization_info = data.get('organization_info', {})
|
| 2259 |
-
|
|
|
|
|
|
|
|
|
|
| 2260 |
|
| 2261 |
if request.method == 'POST':
|
| 2262 |
action = request.form.get('action')
|
| 2263 |
-
|
| 2264 |
try:
|
| 2265 |
if action == 'add_category':
|
| 2266 |
category_name = request.form.get('category_name', '').strip()
|
|
@@ -2270,9 +1990,9 @@ def admin():
|
|
| 2270 |
save_data(data)
|
| 2271 |
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
|
| 2272 |
elif not category_name:
|
| 2273 |
-
|
| 2274 |
else:
|
| 2275 |
-
|
| 2276 |
|
| 2277 |
elif action == 'delete_category':
|
| 2278 |
category_to_delete = request.form.get('category_name')
|
|
@@ -2298,6 +2018,16 @@ def admin():
|
|
| 2298 |
data['organization_info'] = organization_info
|
| 2299 |
save_data(data)
|
| 2300 |
flash("Информация о магазине успешно обновлена.", 'success')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2301 |
|
| 2302 |
elif action == 'add_product':
|
| 2303 |
name = request.form.get('name', '').strip()
|
|
@@ -2312,13 +2042,12 @@ def admin():
|
|
| 2312 |
if not name or not price_str:
|
| 2313 |
flash("Название и цена товара обязательны.", 'error')
|
| 2314 |
return redirect(url_for('admin'))
|
| 2315 |
-
|
| 2316 |
try:
|
| 2317 |
price = round(float(price_str), 2)
|
| 2318 |
if price < 0: price = 0
|
| 2319 |
except ValueError:
|
| 2320 |
-
|
| 2321 |
-
|
| 2322 |
|
| 2323 |
photos_list = []
|
| 2324 |
if photos_files and HF_TOKEN_WRITE:
|
|
@@ -2335,9 +2064,8 @@ def admin():
|
|
| 2335 |
try:
|
| 2336 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 2337 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 2338 |
-
|
| 2339 |
-
|
| 2340 |
-
|
| 2341 |
safe_name = secure_filename(name.replace(' ', '_'))[:50]
|
| 2342 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 2343 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
|
@@ -2359,16 +2087,15 @@ def admin():
|
|
| 2359 |
try: os.remove(temp_path)
|
| 2360 |
except OSError: pass
|
| 2361 |
elif photo and not photo.filename:
|
| 2362 |
-
|
| 2363 |
try:
|
| 2364 |
-
|
| 2365 |
-
|
| 2366 |
except OSError as e:
|
| 2367 |
pass
|
| 2368 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 2369 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 2370 |
|
| 2371 |
-
|
| 2372 |
new_product = {
|
| 2373 |
'product_id': uuid4().hex,
|
| 2374 |
'name': name, 'price': price, 'description': description,
|
|
@@ -2384,13 +2111,10 @@ def admin():
|
|
| 2384 |
elif action == 'edit_product':
|
| 2385 |
product_id = request.form.get('product_id')
|
| 2386 |
product_to_edit = next((p for p in products if p.get('product_id') == product_id), None)
|
| 2387 |
-
|
| 2388 |
if product_to_edit is None:
|
| 2389 |
-
|
| 2390 |
-
|
| 2391 |
-
|
| 2392 |
original_name = product_to_edit.get('name', 'N/A')
|
| 2393 |
-
|
| 2394 |
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
|
| 2395 |
price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
|
| 2396 |
product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
|
|
@@ -2399,13 +2123,12 @@ def admin():
|
|
| 2399 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 2400 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 2401 |
product_to_edit['is_top'] = 'is_top' in request.form
|
| 2402 |
-
|
| 2403 |
try:
|
| 2404 |
price = round(float(price_str), 2)
|
| 2405 |
if price < 0: price = 0
|
| 2406 |
product_to_edit['price'] = price
|
| 2407 |
except ValueError:
|
| 2408 |
-
|
| 2409 |
|
| 2410 |
photos_files = request.files.getlist('photos')
|
| 2411 |
if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
|
|
@@ -2423,9 +2146,8 @@ def admin():
|
|
| 2423 |
try:
|
| 2424 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 2425 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 2426 |
-
|
| 2427 |
-
|
| 2428 |
-
|
| 2429 |
safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
|
| 2430 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 2431 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
|
@@ -2439,11 +2161,11 @@ def admin():
|
|
| 2439 |
except Exception as e:
|
| 2440 |
flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
|
| 2441 |
if os.path.exists(temp_path):
|
| 2442 |
-
|
| 2443 |
-
|
| 2444 |
try:
|
| 2445 |
-
|
| 2446 |
-
|
| 2447 |
except OSError as e:
|
| 2448 |
pass
|
| 2449 |
|
|
@@ -2464,24 +2186,20 @@ def admin():
|
|
| 2464 |
product_to_edit['photos'] = new_photos_list
|
| 2465 |
flash("Фотографии товара успешно обновлены.", "success")
|
| 2466 |
elif uploaded_count == 0 and any(f.filename for f in photos_files):
|
| 2467 |
-
|
| 2468 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 2469 |
flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
|
| 2470 |
-
|
| 2471 |
save_data(data)
|
| 2472 |
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
|
| 2473 |
|
| 2474 |
elif action == 'delete_product':
|
| 2475 |
product_id = request.form.get('product_id')
|
| 2476 |
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
|
| 2477 |
-
|
| 2478 |
if product_index == -1:
|
| 2479 |
flash(f"Ошибка удаления: товар с ID '{product_id}' не найден.", 'error')
|
| 2480 |
return redirect(url_for('admin'))
|
| 2481 |
-
|
| 2482 |
deleted_product = products.pop(product_index)
|
| 2483 |
product_name = deleted_product.get('name', 'N/A')
|
| 2484 |
-
|
| 2485 |
photos_to_delete = deleted_product.get('photos', [])
|
| 2486 |
if photos_to_delete and HF_TOKEN_WRITE:
|
| 2487 |
try:
|
|
@@ -2494,48 +2212,62 @@ def admin():
|
|
| 2494 |
commit_message=f"Delete photos for deleted product {product_name}"
|
| 2495 |
)
|
| 2496 |
except Exception as e:
|
| 2497 |
-
|
| 2498 |
elif photos_to_delete and not HF_TOKEN_WRITE:
|
| 2499 |
flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
|
| 2500 |
-
|
| 2501 |
data['products'] = products
|
| 2502 |
save_data(data)
|
| 2503 |
flash(f"Товар '{product_name}' удален.", 'success')
|
| 2504 |
-
|
| 2505 |
-
elif action == 'delete_order':
|
| 2506 |
-
order_id_to_delete = request.form.get('order_id')
|
| 2507 |
-
if order_id_to_delete in orders:
|
| 2508 |
-
del orders[order_id_to_delete]
|
| 2509 |
-
data['orders'] = orders
|
| 2510 |
-
save_data(data)
|
| 2511 |
-
flash(f"Заказ №{order_id_to_delete} успешно удален.", 'success')
|
| 2512 |
-
else:
|
| 2513 |
-
flash(f"Заказ №{order_id_to_delete} не найден.", 'error')
|
| 2514 |
-
|
| 2515 |
else:
|
| 2516 |
-
|
| 2517 |
-
|
| 2518 |
return redirect(url_for('admin'))
|
| 2519 |
-
|
| 2520 |
except Exception as e:
|
| 2521 |
-
|
| 2522 |
-
|
| 2523 |
|
| 2524 |
current_data = load_data()
|
| 2525 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2526 |
display_categories = sorted(current_data.get('categories', []))
|
| 2527 |
display_organization_info = current_data.get('organization_info', {})
|
| 2528 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2529 |
|
| 2530 |
return render_template_string(
|
| 2531 |
ADMIN_TEMPLATE,
|
| 2532 |
products=display_products,
|
| 2533 |
categories=display_categories,
|
| 2534 |
organization_info=display_organization_info,
|
| 2535 |
-
|
|
|
|
| 2536 |
repo_id=REPO_ID,
|
| 2537 |
currency_code=CURRENCY_CODE
|
| 2538 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2539 |
|
| 2540 |
@app.route('/generate_description_ai', methods=['POST'])
|
| 2541 |
def handle_generate_description_ai():
|
|
@@ -2545,7 +2277,6 @@ def handle_generate_description_ai():
|
|
| 2545 |
|
| 2546 |
if not base64_image:
|
| 2547 |
return jsonify({"error": "Изображение не найдено в запросе."}), 400
|
| 2548 |
-
|
| 2549 |
try:
|
| 2550 |
image_data = base64.b64decode(base64_image)
|
| 2551 |
result_text = generate_ai_description_from_image(image_data, language)
|
|
@@ -2563,7 +2294,6 @@ def handle_chat_with_ai():
|
|
| 2563 |
|
| 2564 |
if not user_message:
|
| 2565 |
return jsonify({"error": "Сообщение не может быть пустым."}), 400
|
| 2566 |
-
|
| 2567 |
try:
|
| 2568 |
ai_response_text = generate_chat_response(user_message, chat_history_from_client)
|
| 2569 |
return jsonify({"text": ai_response_text})
|
|
|
|
| 41 |
DOWNLOAD_RETRIES = 3
|
| 42 |
DOWNLOAD_DELAY = 5
|
| 43 |
|
| 44 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 45 |
+
|
| 46 |
def download_db_from_hf(specific_file=None, retries=DOWNLOAD_RETRIES, delay=DOWNLOAD_DELAY):
|
| 47 |
if not HF_TOKEN_READ and not HF_TOKEN_WRITE:
|
| 48 |
+
logging.warning("HF_TOKEN_READ/HF_TOKEN_WRITE not set. Download might fail for private repos.")
|
|
|
|
| 49 |
token_to_use = HF_TOKEN_READ if HF_TOKEN_READ else HF_TOKEN_WRITE
|
|
|
|
| 50 |
files_to_download = [specific_file] if specific_file else SYNC_FILES
|
| 51 |
all_successful = True
|
|
|
|
| 52 |
for file_name in files_to_download:
|
| 53 |
success = False
|
| 54 |
for attempt in range(retries + 1):
|
|
|
|
| 66 |
success = True
|
| 67 |
break
|
| 68 |
except RepositoryNotFoundError:
|
| 69 |
+
return False
|
| 70 |
except HfHubHTTPError as e:
|
| 71 |
if e.response.status_code == 404:
|
| 72 |
if attempt == 0 and not os.path.exists(file_name):
|
|
|
|
| 81 |
else:
|
| 82 |
pass
|
| 83 |
except requests.exceptions.RequestException as e:
|
| 84 |
+
pass
|
| 85 |
except Exception as e:
|
| 86 |
+
pass
|
|
|
|
| 87 |
if attempt < retries:
|
| 88 |
time.sleep(delay)
|
|
|
|
| 89 |
if not success:
|
| 90 |
all_successful = False
|
|
|
|
| 91 |
return all_successful
|
| 92 |
|
| 93 |
def upload_db_to_hf(specific_file=None):
|
|
|
|
| 96 |
try:
|
| 97 |
api = HfApi()
|
| 98 |
files_to_upload = [specific_file] if specific_file else SYNC_FILES
|
|
|
|
| 99 |
for file_name in files_to_upload:
|
| 100 |
if os.path.exists(file_name):
|
| 101 |
try:
|
|
|
|
| 108 |
commit_message=f"Sync {file_name} {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
| 109 |
)
|
| 110 |
except Exception as e:
|
| 111 |
+
pass
|
| 112 |
else:
|
| 113 |
pass
|
| 114 |
except Exception as e:
|
|
|
|
| 127 |
"returns": "Возврат и обмен товара возможен в течение 14 дней с момента покупки, при условии сохранения товарного вида, упаковки и чека. Некоторые категории товаров могут иметь особые условия возврата. Пожалуйста, свяжитесь с нами для оформления возврата или обмена.",
|
| 128 |
"contact": f"Наш магазин находится по адресу: {STORE_ADDRESS}. Связаться с нами можно по телефону: {WHATSAPP_NUMBER} или через WhatsApp по этому же номеру. Мы работаем ежедневно с 9:00 до 18:00."
|
| 129 |
}
|
| 130 |
+
default_whatsapp_config = {'enabled': False, 'gateway_url': '', 'token': '', 'webhook_secret': uuid4().hex}
|
| 131 |
+
default_data = {'products': [], 'categories': [], 'orders': {}, 'organization_info': default_organization_info, 'whatsapp_config': default_whatsapp_config}
|
| 132 |
+
|
| 133 |
data = default_data
|
| 134 |
try:
|
| 135 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 136 |
data = json.load(file)
|
| 137 |
if not isinstance(data, dict):
|
| 138 |
+
raise FileNotFoundError
|
| 139 |
if 'products' not in data: data['products'] = []
|
| 140 |
if 'categories' not in data: data['categories'] = []
|
| 141 |
if 'orders' not in data: data['orders'] = {}
|
| 142 |
if 'organization_info' not in data: data['organization_info'] = default_organization_info
|
| 143 |
+
if 'whatsapp_config' not in data: data['whatsapp_config'] = default_whatsapp_config
|
| 144 |
+
if 'webhook_secret' not in data.get('whatsapp_config', {}): data['whatsapp_config']['webhook_secret'] = uuid4().hex
|
| 145 |
except FileNotFoundError:
|
| 146 |
if download_db_from_hf(specific_file=DATA_FILE):
|
| 147 |
try:
|
| 148 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 149 |
data = json.load(file)
|
| 150 |
+
if not isinstance(data, dict): data = default_data
|
|
|
|
| 151 |
if 'products' not in data: data['products'] = []
|
| 152 |
if 'categories' not in data: data['categories'] = []
|
| 153 |
if 'orders' not in data: data['orders'] = {}
|
| 154 |
if 'organization_info' not in data: data['organization_info'] = default_organization_info
|
| 155 |
+
if 'whatsapp_config' not in data: data['whatsapp_config'] = default_whatsapp_config
|
| 156 |
+
if 'webhook_secret' not in data.get('whatsapp_config', {}): data['whatsapp_config']['webhook_secret'] = uuid4().hex
|
| 157 |
except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
|
| 158 |
data = default_data
|
| 159 |
else:
|
|
|
|
| 163 |
try:
|
| 164 |
with open(DATA_FILE, 'r', encoding='utf-8') as file:
|
| 165 |
data = json.load(file)
|
| 166 |
+
if not isinstance(data, dict): data = default_data
|
|
|
|
| 167 |
if 'products' not in data: data['products'] = []
|
| 168 |
if 'categories' not in data: data['categories'] = []
|
| 169 |
if 'orders' not in data: data['orders'] = {}
|
| 170 |
if 'organization_info' not in data: data['organization_info'] = default_organization_info
|
| 171 |
+
if 'whatsapp_config' not in data: data['whatsapp_config'] = default_whatsapp_config
|
| 172 |
+
if 'webhook_secret' not in data.get('whatsapp_config', {}): data['whatsapp_config']['webhook_secret'] = uuid4().hex
|
| 173 |
except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
|
| 174 |
data = default_data
|
| 175 |
else:
|
|
|
|
| 199 |
if 'categories' not in data: data['categories'] = []
|
| 200 |
if 'orders' not in data: data['orders'] = {}
|
| 201 |
if 'organization_info' not in data: data['organization_info'] = {}
|
| 202 |
+
if 'whatsapp_config' not in data: data['whatsapp_config'] = {'enabled': False, 'gateway_url': '', 'token': '', 'webhook_secret': uuid4().hex}
|
| 203 |
|
| 204 |
with open(DATA_FILE, 'w', encoding='utf-8') as file:
|
| 205 |
json.dump(data, file, ensure_ascii=False, indent=4)
|
|
|
|
| 222 |
|
| 223 |
try:
|
| 224 |
if not image_data:
|
| 225 |
+
raise ValueError("Файл изображения не найден.")
|
| 226 |
image_stream = io.BytesIO(image_data)
|
| 227 |
image = Image.open(image_stream).convert('RGB')
|
| 228 |
except Exception as e:
|
|
|
|
| 243 |
final_prompt = f"{base_prompt}{lang_suffix}"
|
| 244 |
|
| 245 |
try:
|
| 246 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
|
|
|
| 247 |
response = model.generate_content([final_prompt, image])
|
|
|
|
| 248 |
if hasattr(response, 'text'):
|
| 249 |
return response.text
|
| 250 |
else:
|
| 251 |
if response.parts:
|
| 252 |
+
return "".join(part.text for part in response.parts if hasattr(part, 'text'))
|
| 253 |
else:
|
| 254 |
response.resolve()
|
| 255 |
return response.text
|
|
|
|
| 256 |
except Exception as e:
|
| 257 |
if "API key not valid" in str(e):
|
| 258 |
+
raise ValueError("Внутренняя ошибка конфигурации API.")
|
| 259 |
elif " Billing account not found" in str(e):
|
| 260 |
+
raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
|
| 261 |
elif "Could not find model" in str(e):
|
| 262 |
+
raise ValueError(f"Модель 'gemini-1.5-flash' не найдена или недоступна.")
|
| 263 |
elif "resource has been exhausted" in str(e).lower():
|
| 264 |
+
raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
|
| 265 |
elif "content has been blocked" in str(e).lower():
|
| 266 |
+
reason = "неизвестна"
|
| 267 |
+
if hasattr(e, 'response') and hasattr(e.response, 'prompt_feedback') and e.response.prompt_feedback.block_reason:
|
| 268 |
reason = e.response.prompt_feedback.block_reason
|
| 269 |
+
raise ValueError(f"Генерация контента заблокирована из-за политики безопасности (причина: {reason}). Попробуйте другое изображение ил�� запрос.")
|
| 270 |
else:
|
| 271 |
+
raise ValueError(f"Ошибка при генерации контента: {e}")
|
| 272 |
|
| 273 |
def generate_chat_response(message, chat_history_from_client):
|
| 274 |
if not configure_gemini():
|
|
|
|
| 300 |
if organization_info.get("contact"):
|
| 301 |
org_info_str += f"Контактная информация: {organization_info['contact']}\n"
|
| 302 |
|
|
|
|
| 303 |
system_instruction_content = (
|
| 304 |
"Ты - доброжелательный и очень полезный виртуальный консультант для магазина O&CO. "
|
| 305 |
"Твоя задача - помогать пользователям находить товары, отвечать на вопросы о них, предлагать варианты, а также предоставлять информацию о магазине. "
|
|
|
|
| 321 |
response = None
|
| 322 |
|
| 323 |
try:
|
| 324 |
+
model = genai.GenerativeModel('gemini-1.5-flash', system_instruction=system_instruction_content)
|
| 325 |
|
| 326 |
model_chat_history_for_gemini = []
|
| 327 |
for entry in chat_history_from_client:
|
|
|
|
| 332 |
})
|
| 333 |
|
| 334 |
chat = model.start_chat(history=model_chat_history_for_gemini)
|
|
|
|
| 335 |
response = chat.send_message(message, generation_config={'max_output_tokens': 1000})
|
| 336 |
|
| 337 |
if hasattr(response, 'text'):
|
|
|
|
| 344 |
generated_text = response.text
|
| 345 |
else:
|
| 346 |
raise ValueError("AI did not return a valid text response.")
|
|
|
|
| 347 |
return generated_text
|
|
|
|
| 348 |
except Exception as e:
|
| 349 |
if "API key not valid" in str(e):
|
| 350 |
+
return "Внутренняя ошибка конфигурации API."
|
| 351 |
elif " Billing account not found" in str(e):
|
| 352 |
+
return "Проблема с биллингом аккаунта Google Cloud."
|
| 353 |
elif "Could not find model" in str(e):
|
| 354 |
+
return "Модель 'gemini-1.5-flash' не найдена или недоступна."
|
| 355 |
elif "resource has been exhausted" in str(e).lower():
|
| 356 |
+
return "Квота запросов исчерпана. Попробуйте позже."
|
| 357 |
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):
|
| 358 |
+
reason = response.prompt_feedback.block_reason if (response and hasattr(response, 'prompt_feedback')) else "неизвестна"
|
| 359 |
+
return f"Извините, Ваш запрос был заблокирован из-за политики безопасности (причина: {reason}). Пожалуйста, переформулируйте его."
|
| 360 |
else:
|
| 361 |
+
return f"Извините, произошла ошибка: {e}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
+
def process_whatsapp_message(from_number, user_message, config):
|
| 364 |
try:
|
| 365 |
+
ai_response = generate_chat_response(user_message, [])
|
| 366 |
+
|
| 367 |
+
gateway_url = config.get('gateway_url')
|
| 368 |
+
token = config.get('token')
|
| 369 |
+
|
| 370 |
+
if not gateway_url:
|
| 371 |
+
logging.error("WhatsApp Gateway URL is not configured.")
|
| 372 |
+
return
|
|
|
|
|
|
|
| 373 |
|
| 374 |
+
headers = {'Content-Type': 'application/json'}
|
| 375 |
+
if token:
|
| 376 |
+
headers['Authorization'] = f"Bearer {token}"
|
| 377 |
+
|
| 378 |
+
reply_payload = {
|
| 379 |
+
"to": from_number,
|
| 380 |
+
"message": ai_response
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
requests.post(gateway_url, json=reply_payload, headers=headers, timeout=10)
|
| 384 |
+
|
| 385 |
except Exception as e:
|
| 386 |
+
logging.error(f"Error processing WhatsApp message for {from_number}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
CATALOG_TEMPLATE = '''
|
| 389 |
<!DOCTYPE html>
|
|
|
|
| 618 |
#chat-send-button { background-color: #0070D1; color: white; border: none; border-radius: 20px; width: 40px; height: 40px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: background-color 0.3s; }
|
| 619 |
#chat-send-button:hover { background-color: #005CBF; }
|
| 620 |
#chat-send-button:disabled { background-color: #cccccc; cursor: not-allowed; }
|
|
|
|
| 621 |
.chat-product-card { background-color: #f0f2f5; border-radius: 12px; padding: 10px; margin-top: 8px; display: flex; align-items: center; gap: 12px; border: 1px solid #e0e0e0; }
|
| 622 |
.chat-product-card img { width: 50px; height: 50px; object-fit: cover; border-radius: 8px; flex-shrink: 0; }
|
| 623 |
.chat-product-card-info { flex-grow: 1; }
|
|
|
|
| 631 |
}
|
| 632 |
.chat-product-link:hover, .chat-add-to-cart:hover { background-color: #BBDEFB; }
|
| 633 |
.chat-product-card-actions .fa-cart-plus { font-size: 0.9em; }
|
|
|
|
| 634 |
</style>
|
| 635 |
</head>
|
| 636 |
<body>
|
|
|
|
| 1287 |
.order-summary { margin-top: 30px; padding-top: 20px; border-top: 2px solid #0070D1; text-align: right; }
|
| 1288 |
.order-summary p { margin-bottom: 10px; font-size: 1.1rem; }
|
| 1289 |
.order-summary strong { font-size: 1.3rem; color: #0A2A66; }
|
| 1290 |
+
.customer-info { margin-top: 30px; background-color: #f9f9f9; padding: 20px; border-radius: 8px; border: 1px solid #e0e0e0;}
|
| 1291 |
+
.customer-info p { margin-bottom: 8px; font-size: 0.95rem; }
|
| 1292 |
+
.customer-info strong { color: #004282; }
|
|
|
|
| 1293 |
.actions { margin-top: 30px; text-align: center; }
|
| 1294 |
.button { padding: 12px 25px; border: none; border-radius: 8px; background-color: #0070D1; color: white; font-weight: 600; cursor: pointer; transition: background-color 0.3s ease, transform 0.1s ease; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; }
|
| 1295 |
.button:hover { background-color: #005CBF; }
|
|
|
|
| 1327 |
<p><strong>ИТОГО К ОПЛАТЕ: {{ "%.2f"|format(order.total_price) }} {{ currency_code }}</strong></p>
|
| 1328 |
</div>
|
| 1329 |
|
| 1330 |
+
<div class="customer-info">
|
| 1331 |
+
<h2><i class="fas fa-info-circle"></i> Статус заказа</h2>
|
| 1332 |
+
<p>Этот заказ был оформлен без входа в систему.</p>
|
| 1333 |
+
<p>Пожалуйста, свяжитесь с нами по WhatsApp для подтверждения и уточнения деталей.</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1334 |
</div>
|
| 1335 |
|
| 1336 |
<div class="actions">
|
| 1337 |
+
<button class="button" onclick="sendOrderViaWhatsApp()"><i class="fab fa-whatsapp"></i> Отправить заказ</button>
|
| 1338 |
</div>
|
| 1339 |
|
| 1340 |
<a href="{{ url_for('catalog') }}" class="catalog-link">← Вернуться в каталог</a>
|
| 1341 |
|
| 1342 |
<script>
|
| 1343 |
+
function sendOrderViaWhatsApp() {
|
| 1344 |
+
const orderId = '{{ order.id }}';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1345 |
const orderUrl = `{{ request.url }}`;
|
| 1346 |
+
const whatsappNumber = "{{ whatsapp_number }}";
|
| 1347 |
|
| 1348 |
+
let message = `Здравствуйте! Хочу подтвердить свой заказ на OCO:%0A%0A`;
|
| 1349 |
+
message += `*Номер заказа:* ${orderId}%0A`;
|
| 1350 |
+
message += `*Ссылка на заказ:* ${encodeURIComponent(orderUrl)}%0A%0A`;
|
| 1351 |
+
message += `Пожалуйста, свяжитесь со мной для уточнения деталей оплаты и доставки.`;
|
|
|
|
| 1352 |
|
| 1353 |
const whatsappUrl = `https://api.whatsapp.com/send?phone=${whatsappNumber}&text=${message}`;
|
| 1354 |
window.open(whatsappUrl, '_blank');
|
|
|
|
| 1413 |
details { background-color: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; margin-bottom: 20px; }
|
| 1414 |
details > summary { cursor: pointer; font-weight: 600; color: #303F9F; display: block; padding: 15px; border-bottom: 1px solid #e0e0e0; list-style: none; position: relative; }
|
| 1415 |
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; }
|
| 1416 |
+
details[open] > summary::after { transform: translateY(-50%) rotate(180deg); }
|
| 1417 |
details[open] > summary { border-bottom: 1px solid #e0e0e0; }
|
| 1418 |
details .form-content { padding: 20px; }
|
| 1419 |
.color-input-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
|
|
|
|
| 1438 |
.status-indicator.top-product { background-color: #fff3cd; color: #856404; margin-left: 5px;}
|
| 1439 |
.ai-generate-button { background-color: #8D6EC8; margin-top: 5px; margin-bottom: 10px; }
|
| 1440 |
.ai-generate-button:hover { background-color: #7B4DB5; }
|
| 1441 |
+
.webhook-url-display { padding: 10px; background-color: #e9ecef; border: 1px solid #ced4da; border-radius: 6px; font-family: monospace; word-wrap: break-word; font-size: 0.9rem; margin-top: 5px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1442 |
</style>
|
| 1443 |
</head>
|
| 1444 |
<body>
|
|
|
|
| 1466 |
<button type="submit" class="button" title="Загрузить локальные файлы на Hugging Face"><i class="fas fa-upload"></i> Загрузить БД</button>
|
| 1467 |
</form>
|
| 1468 |
<form method="POST" action="{{ url_for('force_download') }}" style="display: inline;" onsubmit="return confirm('Вы уверены, что хотите принудительно скачать данные с сервера? Это перезапишет ваши локальные файлы.');">
|
| 1469 |
+
<button type="submit" class="button download-hf-button" title="Скачать файлы (перезапишет локальные)"><i class="fas fa-download"></i> Скачать БД</button>
|
| 1470 |
</form>
|
| 1471 |
</div>
|
| 1472 |
<p style="font-size: 0.85rem; color: #999;">Резервное копирование происходит автоматически каждые 30 минут, а также после каждого сохранения данных. Используйте эти кнопки для немедленной синхронизации.</p>
|
| 1473 |
</div>
|
| 1474 |
|
| 1475 |
+
<div class="section">
|
| 1476 |
+
<h2><i class="fab fa-whatsapp"></i> Интеграция с WhatsApp</h2>
|
| 1477 |
+
<details>
|
| 1478 |
+
<summary><i class="fas fa-cog"></i> Настроить WhatsApp Бота</summary>
|
| 1479 |
+
<div class="form-content">
|
| 1480 |
+
<form method="POST">
|
| 1481 |
+
<input type="hidden" name="action" value="update_whatsapp_config">
|
| 1482 |
+
<div>
|
| 1483 |
+
<input type="checkbox" id="whatsapp_enabled" name="whatsapp_enabled" {% if whatsapp_config.get('enabled') %}checked{% endif %}>
|
| 1484 |
+
<label for="whatsapp_enabled" class="inline-label">Включить WhatsApp-бота</label>
|
| 1485 |
+
</div>
|
| 1486 |
+
<label for="whatsapp_gateway_url">URL шлюза для отправки сообщений:</label>
|
| 1487 |
+
<input type="text" id="whatsapp_gateway_url" name="whatsapp_gateway_url" value="{{ whatsapp_config.get('gateway_url', '') }}" placeholder="https://api.yourgateway.com/send">
|
| 1488 |
+
<label for="whatsapp_token">Токен авторизации для шлюза (Bearer Token):</label>
|
| 1489 |
+
<input type="text" id="whatsapp_token" name="whatsapp_token" value="{{ whatsapp_config.get('token', '') }}" placeholder="Ваш секретный токен">
|
| 1490 |
+
|
| 1491 |
+
<label>Ваш URL для вебхука (вставьте это в настройки шлюза):</label>
|
| 1492 |
+
<div class="webhook-url-display">{{ webhook_url }}</div>
|
| 1493 |
+
<p style="font-size: 0.85rem; color: #999; margin-top: 10px;">Ваш шлюз должен отправлять POST-запросы на этот URL с JSON-телом вида: <code>{"from": "79991234567", "message": "текст сообщения"}</code>.</p>
|
| 1494 |
+
|
| 1495 |
+
<button type="submit" class="add-button"><i class="fas fa-save"></i> Сохранить настройки WhatsApp</button>
|
| 1496 |
+
</form>
|
| 1497 |
+
<p style="font-size: 0.85rem; color: #999;"><b>Внимание:</b> "Серые" методы интеграции с WhatsApp могут привести к блокировке номера. Используйте на свой страх и риск.</p>
|
| 1498 |
+
</div>
|
| 1499 |
+
</details>
|
| 1500 |
+
</div>
|
| 1501 |
+
|
| 1502 |
<div class="flex-container">
|
| 1503 |
<div class="flex-item">
|
| 1504 |
<div class="section">
|
|
|
|
| 1737 |
{% endif %}
|
| 1738 |
</div>
|
| 1739 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1740 |
</div>
|
| 1741 |
|
| 1742 |
<script>
|
|
|
|
| 1787 |
const photoInput = document.getElementById(photoInputId);
|
| 1788 |
const descriptionTextarea = document.getElementById(descriptionTextareaId);
|
| 1789 |
const languageSelect = document.getElementById(languageSelectId);
|
| 1790 |
+
const generateButton = descriptionTextarea.nextElementSibling;
|
| 1791 |
|
| 1792 |
if (!photoInput || !descriptionTextarea || !languageSelect || !generateButton) {
|
| 1793 |
alert("Ошибка: Не найдены элементы формы для генерации.");
|
|
|
|
| 1847 |
};
|
| 1848 |
reader.readAsDataURL(file);
|
| 1849 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1850 |
</script>
|
| 1851 |
</body>
|
| 1852 |
</html>
|
| 1853 |
'''
|
| 1854 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1855 |
@app.route('/')
|
| 1856 |
def catalog():
|
| 1857 |
data = load_data()
|
|
|
|
| 1912 |
return jsonify({"error": "Корзина пуста или не передана."}), 400
|
| 1913 |
|
| 1914 |
cart_items = order_data['cart']
|
|
|
|
| 1915 |
total_price = 0
|
| 1916 |
processed_cart = []
|
| 1917 |
for item in cart_items:
|
|
|
|
| 1951 |
data = load_data()
|
| 1952 |
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 1953 |
data['orders'] = {}
|
|
|
|
| 1954 |
data['orders'][order_id] = new_order
|
| 1955 |
save_data(data)
|
| 1956 |
return jsonify({"order_id": order_id}), 201
|
|
|
|
| 1957 |
except Exception as e:
|
| 1958 |
return jsonify({"error": "Ошибка сервера при сохранении заказа."}), 500
|
| 1959 |
|
|
|
|
| 1960 |
@app.route('/order/<order_id>')
|
| 1961 |
def view_order(order_id):
|
| 1962 |
data = load_data()
|
| 1963 |
order = data.get('orders', {}).get(order_id)
|
|
|
|
| 1964 |
return render_template_string(ORDER_TEMPLATE,
|
| 1965 |
order=order,
|
| 1966 |
repo_id=REPO_ID,
|
| 1967 |
currency_code=CURRENCY_CODE,
|
| 1968 |
whatsapp_number=WHATSAPP_NUMBER)
|
| 1969 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1970 |
|
| 1971 |
@app.route('/admin', methods=['GET', 'POST'])
|
| 1972 |
def admin():
|
|
|
|
| 1974 |
products = data.get('products', [])
|
| 1975 |
categories = data.get('categories', [])
|
| 1976 |
organization_info = data.get('organization_info', {})
|
| 1977 |
+
whatsapp_config = data.get('whatsapp_config', {})
|
| 1978 |
+
|
| 1979 |
+
if 'orders' not in data or not isinstance(data.get('orders'), dict):
|
| 1980 |
+
data['orders'] = {}
|
| 1981 |
|
| 1982 |
if request.method == 'POST':
|
| 1983 |
action = request.form.get('action')
|
|
|
|
| 1984 |
try:
|
| 1985 |
if action == 'add_category':
|
| 1986 |
category_name = request.form.get('category_name', '').strip()
|
|
|
|
| 1990 |
save_data(data)
|
| 1991 |
flash(f"Категория '{category_name}' успешно добавлена.", 'success')
|
| 1992 |
elif not category_name:
|
| 1993 |
+
flash("Название категории не может быть пустым.", 'error')
|
| 1994 |
else:
|
| 1995 |
+
flash(f"Категория '{category_name}' уже существует.", 'error')
|
| 1996 |
|
| 1997 |
elif action == 'delete_category':
|
| 1998 |
category_to_delete = request.form.get('category_name')
|
|
|
|
| 2018 |
data['organization_info'] = organization_info
|
| 2019 |
save_data(data)
|
| 2020 |
flash("Информация о магазине успешно обновлена.", 'success')
|
| 2021 |
+
|
| 2022 |
+
elif action == 'update_whatsapp_config':
|
| 2023 |
+
whatsapp_config['enabled'] = 'whatsapp_enabled' in request.form
|
| 2024 |
+
whatsapp_config['gateway_url'] = request.form.get('whatsapp_gateway_url', '').strip()
|
| 2025 |
+
whatsapp_config['token'] = request.form.get('whatsapp_token', '').strip()
|
| 2026 |
+
if 'webhook_secret' not in whatsapp_config or not whatsapp_config['webhook_secret']:
|
| 2027 |
+
whatsapp_config['webhook_secret'] = uuid4().hex
|
| 2028 |
+
data['whatsapp_config'] = whatsapp_config
|
| 2029 |
+
save_data(data)
|
| 2030 |
+
flash("Настройки WhatsApp успешно обновлены.", 'success')
|
| 2031 |
|
| 2032 |
elif action == 'add_product':
|
| 2033 |
name = request.form.get('name', '').strip()
|
|
|
|
| 2042 |
if not name or not price_str:
|
| 2043 |
flash("Название и цена товара обязательны.", 'error')
|
| 2044 |
return redirect(url_for('admin'))
|
|
|
|
| 2045 |
try:
|
| 2046 |
price = round(float(price_str), 2)
|
| 2047 |
if price < 0: price = 0
|
| 2048 |
except ValueError:
|
| 2049 |
+
flash("Неверный формат цены.", 'error')
|
| 2050 |
+
return redirect(url_for('admin'))
|
| 2051 |
|
| 2052 |
photos_list = []
|
| 2053 |
if photos_files and HF_TOKEN_WRITE:
|
|
|
|
| 2064 |
try:
|
| 2065 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 2066 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 2067 |
+
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
| 2068 |
+
continue
|
|
|
|
| 2069 |
safe_name = secure_filename(name.replace(' ', '_'))[:50]
|
| 2070 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 2071 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
|
|
|
| 2087 |
try: os.remove(temp_path)
|
| 2088 |
except OSError: pass
|
| 2089 |
elif photo and not photo.filename:
|
| 2090 |
+
pass
|
| 2091 |
try:
|
| 2092 |
+
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 2093 |
+
os.rmdir(uploads_dir)
|
| 2094 |
except OSError as e:
|
| 2095 |
pass
|
| 2096 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 2097 |
flash("HF_TOKEN (write) не настроен. Фотографии не были загружены.", "warning")
|
| 2098 |
|
|
|
|
| 2099 |
new_product = {
|
| 2100 |
'product_id': uuid4().hex,
|
| 2101 |
'name': name, 'price': price, 'description': description,
|
|
|
|
| 2111 |
elif action == 'edit_product':
|
| 2112 |
product_id = request.form.get('product_id')
|
| 2113 |
product_to_edit = next((p for p in products if p.get('product_id') == product_id), None)
|
|
|
|
| 2114 |
if product_to_edit is None:
|
| 2115 |
+
flash(f"Ошибка редактирования: товар с ID '{product_id}' не найден.", 'error')
|
| 2116 |
+
return redirect(url_for('admin'))
|
|
|
|
| 2117 |
original_name = product_to_edit.get('name', 'N/A')
|
|
|
|
| 2118 |
product_to_edit['name'] = request.form.get('name', product_to_edit['name']).strip()
|
| 2119 |
price_str = request.form.get('price', str(product_to_edit['price'])).replace(',', '.')
|
| 2120 |
product_to_edit['description'] = request.form.get('description', product_to_edit.get('description', '')).strip()
|
|
|
|
| 2123 |
product_to_edit['colors'] = [c.strip() for c in request.form.getlist('colors') if c.strip()]
|
| 2124 |
product_to_edit['in_stock'] = 'in_stock' in request.form
|
| 2125 |
product_to_edit['is_top'] = 'is_top' in request.form
|
|
|
|
| 2126 |
try:
|
| 2127 |
price = round(float(price_str), 2)
|
| 2128 |
if price < 0: price = 0
|
| 2129 |
product_to_edit['price'] = price
|
| 2130 |
except ValueError:
|
| 2131 |
+
flash(f"Неверный формат цены для товара '{original_name}'. Цена не изменена.", 'warning')
|
| 2132 |
|
| 2133 |
photos_files = request.files.getlist('photos')
|
| 2134 |
if photos_files and any(f.filename for f in photos_files) and HF_TOKEN_WRITE:
|
|
|
|
| 2146 |
try:
|
| 2147 |
ext = os.path.splitext(photo.filename)[1].lower()
|
| 2148 |
if ext not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']:
|
| 2149 |
+
flash(f"Файл {photo.filename} не является изображением и был пропущен.", "warning")
|
| 2150 |
+
continue
|
|
|
|
| 2151 |
safe_name = secure_filename(product_to_edit['name'].replace(' ', '_'))[:50]
|
| 2152 |
photo_filename = f"{safe_name}_{datetime.now().strftime('%Y%m%d%H%M%S%f')}{ext}"
|
| 2153 |
temp_path = os.path.join(uploads_dir, photo_filename)
|
|
|
|
| 2161 |
except Exception as e:
|
| 2162 |
flash(f"Ошибка при загрузке нового фото {photo.filename}.", 'error')
|
| 2163 |
if os.path.exists(temp_path):
|
| 2164 |
+
try: os.remove(temp_path)
|
| 2165 |
+
except OSError: pass
|
| 2166 |
try:
|
| 2167 |
+
if os.path.exists(uploads_dir) and not os.listdir(uploads_dir):
|
| 2168 |
+
os.rmdir(uploads_dir)
|
| 2169 |
except OSError as e:
|
| 2170 |
pass
|
| 2171 |
|
|
|
|
| 2186 |
product_to_edit['photos'] = new_photos_list
|
| 2187 |
flash("Фотографии товара успешно обновлены.", "success")
|
| 2188 |
elif uploaded_count == 0 and any(f.filename for f in photos_files):
|
| 2189 |
+
flash("Не удалось загрузить новые фотографии (возможно, неверный формат).", "error")
|
| 2190 |
elif not HF_TOKEN_WRITE and photos_files and any(f.filename for f in photos_files):
|
| 2191 |
flash("HF_TOKEN (write) не настроен. Фотографии не были обновлены.", "warning")
|
|
|
|
| 2192 |
save_data(data)
|
| 2193 |
flash(f"Товар '{product_to_edit['name']}' успешно обновлен.", 'success')
|
| 2194 |
|
| 2195 |
elif action == 'delete_product':
|
| 2196 |
product_id = request.form.get('product_id')
|
| 2197 |
product_index = next((i for i, p in enumerate(products) if p.get('product_id') == product_id), -1)
|
|
|
|
| 2198 |
if product_index == -1:
|
| 2199 |
flash(f"Ошибка удаления: товар с ID '{product_id}' не найден.", 'error')
|
| 2200 |
return redirect(url_for('admin'))
|
|
|
|
| 2201 |
deleted_product = products.pop(product_index)
|
| 2202 |
product_name = deleted_product.get('name', 'N/A')
|
|
|
|
| 2203 |
photos_to_delete = deleted_product.get('photos', [])
|
| 2204 |
if photos_to_delete and HF_TOKEN_WRITE:
|
| 2205 |
try:
|
|
|
|
| 2212 |
commit_message=f"Delete photos for deleted product {product_name}"
|
| 2213 |
)
|
| 2214 |
except Exception as e:
|
| 2215 |
+
flash(f"Не удалось удалить фото для товара '{product_name}' с сервера. Товар удален локально.", "warning")
|
| 2216 |
elif photos_to_delete and not HF_TOKEN_WRITE:
|
| 2217 |
flash(f"Товар '{product_name}' удален локально, но фото не удалены с сервера (токен не задан).", "warning")
|
|
|
|
| 2218 |
data['products'] = products
|
| 2219 |
save_data(data)
|
| 2220 |
flash(f"Товар '{product_name}' удален.", 'success')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2221 |
else:
|
| 2222 |
+
flash(f"Неизвестное действие: {action}", 'warning')
|
|
|
|
| 2223 |
return redirect(url_for('admin'))
|
|
|
|
| 2224 |
except Exception as e:
|
| 2225 |
+
flash(f"Произошла внутренняя ошибка при выполнении действия '{action}'. Подробности в логе сервера.", 'error')
|
| 2226 |
+
return redirect(url_for('admin'))
|
| 2227 |
|
| 2228 |
current_data = load_data()
|
| 2229 |
display_products = sorted(current_data.get('products', []), key=lambda p: p.get('name', '').lower())
|
| 2230 |
display_categories = sorted(current_data.get('categories', []))
|
| 2231 |
display_organization_info = current_data.get('organization_info', {})
|
| 2232 |
+
display_whatsapp_config = current_data.get('whatsapp_config', {})
|
| 2233 |
+
|
| 2234 |
+
webhook_url = "Сначала сохраните настройки, чтобы сгенерировать URL."
|
| 2235 |
+
if display_whatsapp_config.get('webhook_secret'):
|
| 2236 |
+
webhook_url = f"{request.host_url}whatsapp_webhook/{display_whatsapp_config['webhook_secret']}"
|
| 2237 |
|
| 2238 |
return render_template_string(
|
| 2239 |
ADMIN_TEMPLATE,
|
| 2240 |
products=display_products,
|
| 2241 |
categories=display_categories,
|
| 2242 |
organization_info=display_organization_info,
|
| 2243 |
+
whatsapp_config=display_whatsapp_config,
|
| 2244 |
+
webhook_url=webhook_url,
|
| 2245 |
repo_id=REPO_ID,
|
| 2246 |
currency_code=CURRENCY_CODE
|
| 2247 |
)
|
| 2248 |
+
|
| 2249 |
+
@app.route('/whatsapp_webhook/<string:secret>', methods=['POST'])
|
| 2250 |
+
def whatsapp_webhook(secret):
|
| 2251 |
+
data = load_data()
|
| 2252 |
+
config = data.get('whatsapp_config', {})
|
| 2253 |
+
|
| 2254 |
+
if not config.get('enabled'):
|
| 2255 |
+
return jsonify({"error": "WhatsApp bot is disabled"}), 403
|
| 2256 |
+
|
| 2257 |
+
if not config.get('webhook_secret') or secret != config.get('webhook_secret'):
|
| 2258 |
+
return jsonify({"error": "Unauthorized"}), 401
|
| 2259 |
+
|
| 2260 |
+
payload = request.get_json()
|
| 2261 |
+
if not payload or 'from' not in payload or 'message' not in payload:
|
| 2262 |
+
return jsonify({"error": "Invalid payload. 'from' and 'message' fields are required."}), 400
|
| 2263 |
+
|
| 2264 |
+
from_number = payload['from']
|
| 2265 |
+
user_message = payload['message']
|
| 2266 |
+
|
| 2267 |
+
thread = threading.Thread(target=process_whatsapp_message, args=(from_number, user_message, config))
|
| 2268 |
+
thread.start()
|
| 2269 |
+
|
| 2270 |
+
return jsonify({"status": "received"}), 200
|
| 2271 |
|
| 2272 |
@app.route('/generate_description_ai', methods=['POST'])
|
| 2273 |
def handle_generate_description_ai():
|
|
|
|
| 2277 |
|
| 2278 |
if not base64_image:
|
| 2279 |
return jsonify({"error": "Изображение не найдено в запросе."}), 400
|
|
|
|
| 2280 |
try:
|
| 2281 |
image_data = base64.b64decode(base64_image)
|
| 2282 |
result_text = generate_ai_description_from_image(image_data, language)
|
|
|
|
| 2294 |
|
| 2295 |
if not user_message:
|
| 2296 |
return jsonify({"error": "Сообщение не может быть пустым."}), 400
|
|
|
|
| 2297 |
try:
|
| 2298 |
ai_response_text = generate_chat_response(user_message, chat_history_from_client)
|
| 2299 |
return jsonify({"text": ai_response_text})
|