Kgshop commited on
Commit
a8c75d8
·
verified ·
1 Parent(s): 71c173f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -218
app.py CHANGED
@@ -304,7 +304,7 @@ def generate_ai_description_from_image(image_data, language):
304
  final_prompt = f"{base_prompt}{lang_suffix}"
305
 
306
  try:
307
- model = genai.GenerativeModel('gemma-3-27b-it')
308
 
309
  response = model.generate_content([final_prompt, image])
310
 
@@ -324,7 +324,7 @@ def generate_ai_description_from_image(image_data, language):
324
  elif " Billing account not found" in str(e):
325
  raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
326
  elif "Could not find model" in str(e):
327
- raise ValueError(f"Модель 'learnlm-2.0-flash-experimental' не найдена или недоступна.")
328
  elif "resource has been exhausted" in str(e).lower():
329
  raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
330
  elif "content has been blocked" in str(e).lower():
@@ -335,19 +335,6 @@ def generate_ai_description_from_image(image_data, language):
335
  else:
336
  raise ValueError(f"Ошибка при генерации контента: {e}")
337
 
338
- def transcribe_audio_with_ai(audio_data):
339
- if not configure_gemini():
340
- raise ValueError("Google AI API не настроен.")
341
- try:
342
- model = genai.GenerativeModel('gemini-1.5-flash-latest')
343
- audio_file = {'mime_type': 'audio/webm', 'data': audio_data}
344
- prompt = "Расшифруй это аудиосообщение. Отвечай только расшифрованным текстом, без лишних слов."
345
- response = model.generate_content([prompt, audio_file])
346
- return response.text
347
- except Exception as e:
348
- logging.error(f"Error during AI audio transcription: {e}")
349
- raise ValueError(f"Ошибка при расшифровке аудио: {e}")
350
-
351
  def generate_chat_response(message, chat_history_from_client, env_id):
352
  if not is_chat_active(env_id):
353
  return "Извините, чат в данный момент неактивен. Пожалуйста, свяжитесь с нами другим способом."
@@ -407,7 +394,7 @@ def generate_chat_response(message, chat_history_from_client, env_id):
407
  response = None
408
 
409
  try:
410
- model = genai.GenerativeModel('gemma-3-27b-it')
411
 
412
  model_chat_history_for_gemini = [
413
  {'role': 'user', 'parts': [{'text': system_instruction_content}]}
@@ -478,13 +465,11 @@ ADMHOSTO_TEMPLATE = '''
478
  h1 { font-weight: 600; color: var(--bg-medium); margin-bottom: 25px; text-align: center; }
479
  .section { margin-bottom: 30px; }
480
  .add-env-form { margin-bottom: 20px; text-align: center; }
481
- .search-container { margin-bottom: 20px; }
482
- #env-search-input { width: 100%; padding: 10px 15px; border: 1px solid #ddd; border-radius: 6px; font-size: 1rem; }
483
  .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: var(--accent); color: var(--text-on-accent); font-weight: 600; cursor: pointer; transition: background-color 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 5px; }
484
  .button:hover { background-color: var(--accent-hover); }
485
  .button:disabled { background-color: #ccc; cursor: not-allowed; }
486
  .env-list { list-style: none; padding: 0; }
487
- .env-item { background: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin-bottom: 10px; display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 15px; transition: opacity 0.3s ease; }
488
  .env-details { display: flex; flex-direction: column; }
489
  .env-id { font-weight: 600; color: var(--bg-medium); font-size: 1.2rem; }
490
  .env-status { font-size: 0.85rem; color: #666; }
@@ -516,10 +501,14 @@ ADMHOSTO_TEMPLATE = '''
516
  </div>
517
 
518
  <div class="section">
519
- <h2><i class="fas fa-list-ul"></i> Существующие среды</h2>
520
- <div class="search-container">
521
- <input type="text" id="env-search-input" placeholder="Поиск по ID среды...">
522
  </div>
 
 
 
 
523
  {% if environments %}
524
  <ul class="env-list">
525
  {% for env in environments %}
@@ -560,23 +549,40 @@ ADMHOSTO_TEMPLATE = '''
560
  </div>
561
  </div>
562
  <script>
563
- document.addEventListener('DOMContentLoaded', function() {
564
- const searchInput = document.getElementById('env-search-input');
565
- if (searchInput) {
566
- searchInput.addEventListener('input', function() {
567
- const searchTerm = this.value.toLowerCase();
568
- const envItems = document.querySelectorAll('.env-item');
569
- envItems.forEach(item => {
570
- const envId = item.querySelector('.env-id').textContent.toLowerCase();
571
- if (envId.includes(searchTerm)) {
572
- item.style.display = 'grid';
573
- } else {
574
- item.style.display = 'none';
575
- }
576
- });
577
- });
 
 
 
578
  }
579
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  </script>
581
  </body>
582
  </html>
@@ -1445,7 +1451,7 @@ CHAT_TEMPLATE = '''
1445
  border-color: var(--bg-medium);
1446
  box-shadow: 0 0 0 3px rgba(19, 93, 102, 0.15);
1447
  }
1448
- #chat-send-button, #record-button {
1449
  background-color: var(--bg-medium);
1450
  color: white;
1451
  border: none;
@@ -1460,11 +1466,8 @@ CHAT_TEMPLATE = '''
1460
  flex-shrink: 0;
1461
  font-size: 1.2rem;
1462
  }
1463
- #record-button.recording {
1464
- background-color: var(--danger);
1465
- }
1466
- #chat-send-button:hover, #record-button:hover { background-color: var(--bg-dark); }
1467
- #chat-send-button:active, #record-button:active { transform: scale(0.9); }
1468
  #chat-send-button:disabled { background-color: #cccccc; cursor: not-allowed; }
1469
  .floating-buttons-container { position: fixed; bottom: 25px; right: 25px; z-index: 1000; }
1470
  .floating-button { background-color: var(--accent); color: var(--bg-dark); border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(72, 209, 204, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
@@ -1510,8 +1513,6 @@ CHAT_TEMPLATE = '''
1510
  .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%; }
1511
  .chat-product-link:hover, .chat-add-to-cart:hover { background-color: #B2DFDB; }
1512
  body.dark-theme .chat-product-link, body.dark-theme .chat-add-to-cart { background-color: #444; color: var(--accent); }
1513
- .message-bubble audio { width: 250px; height: 40px; }
1514
- .transcript-text { font-style: italic; color: #888; margin-top: 5px; font-size: 0.9em; border-top: 1px solid #ccc; padding-top: 5px; }
1515
  </style>
1516
  </head>
1517
  <body class="{{ 'dark-theme' if settings.color_scheme == 'dark' else '' }}">
@@ -1524,8 +1525,7 @@ CHAT_TEMPLATE = '''
1524
  <div id="chat-messages"></div>
1525
  <div class="chat-input-container">
1526
  <input type="text" id="chat-input" placeholder="Напишите сообщение..." autocomplete="off">
1527
- <button id="record-button"><i class="fas fa-microphone"></i></button>
1528
- <button id="chat-send-button" style="display: none;"><i class="fas fa-paper-plane"></i></button>
1529
  </div>
1530
  </div>
1531
 
@@ -1584,11 +1584,6 @@ CHAT_TEMPLATE = '''
1584
  const chatMessagesDiv = document.getElementById('chat-messages');
1585
  const chatInput = document.getElementById('chat-input');
1586
  const chatSendButton = document.getElementById('chat-send-button');
1587
- const recordButton = document.getElementById('record-button');
1588
-
1589
- let mediaRecorder;
1590
- let audioChunks = [];
1591
- let isRecording = false;
1592
 
1593
  function getProductById(productId) { return allProducts.find(p => p.product_id === productId); }
1594
  function getProductIndexById(productId) { return allProducts.findIndex(p => p.product_id === productId); }
@@ -1756,74 +1751,59 @@ CHAT_TEMPLATE = '''
1756
  }, 10);
1757
  }
1758
 
1759
- function addMessageToChatUI(text, role, audioSrc = null) {
1760
  const messageElement = document.createElement('div');
1761
  messageElement.className = `chat-message ${role}`;
1762
-
1763
- const bubble = document.createElement('div');
1764
- bubble.className = 'message-bubble';
1765
 
1766
- if(audioSrc){
1767
- const audioPlayer = document.createElement('audio');
1768
- audioPlayer.controls = true;
1769
- audioPlayer.src = audioSrc;
1770
- bubble.appendChild(audioPlayer);
1771
- }
1772
-
1773
- if (text) {
1774
- const productRegex = /\[ID_ТОВАРА:\\s*([a-fA-F0-9]+)\\s*Название:\\s*([^\]]+)\]/g;
1775
- let lastIndex = 0;
1776
- let match;
1777
-
1778
- while ((match = productRegex.exec(text)) !== null) {
1779
- if (match.index > lastIndex) {
1780
- const textNode = document.createElement('span');
1781
- textNode.innerHTML = text.substring(lastIndex, match.index).replace(/\\n/g, '<br>');
1782
- bubble.appendChild(textNode);
1783
- }
1784
- const productId = match[1];
1785
- const product = getProductById(productId);
1786
- if (product) {
1787
- const photoUrl = product.photos && product.photos.length > 0 ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${product.photos[0]}` : '';
1788
- const card = document.createElement('div');
1789
- card.className = 'chat-product-card';
1790
- card.innerHTML = `<img src="${photoUrl}" alt="${product.name}">
1791
- <div class="chat-product-card-info">
1792
- <strong>${product.name}</strong>
1793
- <span>${product.price.toFixed(0)} ${currencyCode}</span>
1794
- </div>
1795
- <div class="chat-product-card-actions">
1796
- <a href="#" class="chat-product-link" data-id="${productId}">Обзор</a>
1797
- <a href="#" class="chat-add-to-cart" data-id="${productId}"><i class="fas fa-cart-plus"></i></a>
1798
- </div>`;
1799
- messageElement.appendChild(card);
1800
- }
1801
- lastIndex = match.index + match[0].length;
1802
  }
1803
-
1804
- if (lastIndex < text.length) {
1805
- const textNode = document.createElement('span');
1806
- textNode.innerHTML = text.substring(lastIndex).replace(/\\n/g, '<br>');
1807
- bubble.appendChild(textNode);
 
 
 
 
 
 
 
 
 
 
 
1808
  }
1809
- }
1810
-
1811
- if (bubble.hasChildNodes()){
1812
- messageElement.prepend(bubble);
1813
  }
1814
 
 
 
 
 
 
 
 
1815
  chatMessagesDiv.appendChild(messageElement);
1816
  chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
1817
- return messageElement;
1818
  }
1819
-
1820
  async function sendMessage() {
1821
  const message = chatInput.value.trim();
1822
  if (!message) return;
1823
  addMessageToChatUI(message, 'user');
1824
  chatHistory.push({ role: 'user', text: message });
1825
  chatInput.value = '';
1826
- toggleSendButton();
1827
  chatSendButton.disabled = true;
1828
 
1829
  try {
@@ -1844,87 +1824,9 @@ CHAT_TEMPLATE = '''
1844
  }
1845
  }
1846
 
1847
- async function sendVoiceMessage(audioBase64) {
1848
- const audioDataUrl = 'data:audio/webm;base64,' + audioBase64;
1849
- const userMessageElement = addMessageToChatUI(null, 'user', audioDataUrl);
1850
-
1851
- try {
1852
- const response = await fetch(`/${envId}/chat_with_audio`, {
1853
- method: 'POST',
1854
- headers: { 'Content-Type': 'application/json' },
1855
- body: JSON.stringify({ audio: audioBase64, history: chatHistory, chat_id: chatId })
1856
- });
1857
-
1858
- const result = await response.json();
1859
- if (!response.ok) throw new Error(result.error);
1860
-
1861
- if(result.user_text) {
1862
- const transcriptDiv = document.createElement('div');
1863
- transcriptDiv.className = 'transcript-text';
1864
- transcriptDiv.innerText = `Вы: "${result.user_text}"`;
1865
- userMessageElement.querySelector('.message-bubble').appendChild(transcriptDiv);
1866
- chatHistory.push({ role: 'user', text: result.user_text });
1867
- }
1868
-
1869
- addMessageToChatUI(result.ai_text, 'ai');
1870
- chatHistory.push({ role: 'ai', text: result.ai_text });
1871
-
1872
- } catch (error) {
1873
- addMessageToChatUI(`Ошибка обработки аудио: ${error.message}`, 'ai');
1874
- }
1875
- }
1876
-
1877
- function toggleSendButton(){
1878
- if(chatInput.value.trim()){
1879
- recordButton.style.display = 'none';
1880
- chatSendButton.style.display = 'flex';
1881
- } else {
1882
- recordButton.style.display = 'flex';
1883
- chatSendButton.style.display = 'none';
1884
- }
1885
- }
1886
-
1887
- recordButton.addEventListener('click', async () => {
1888
- if (isRecording) {
1889
- mediaRecorder.stop();
1890
- isRecording = false;
1891
- recordButton.classList.remove('recording');
1892
- recordButton.innerHTML = '<i class="fas fa-microphone"></i>';
1893
- chatInput.disabled = false;
1894
- } else {
1895
- try {
1896
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
1897
- mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
1898
- mediaRecorder.ondataavailable = event => {
1899
- if (event.data.size > 0) audioChunks.push(event.data);
1900
- };
1901
- mediaRecorder.onstop = () => {
1902
- const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
1903
- const reader = new FileReader();
1904
- reader.onloadend = () => {
1905
- const base64String = reader.result.split(',')[1];
1906
- sendVoiceMessage(base64String);
1907
- };
1908
- reader.readAsDataURL(audioBlob);
1909
- audioChunks = [];
1910
- stream.getTracks().forEach(track => track.stop());
1911
- };
1912
- mediaRecorder.start();
1913
- isRecording = true;
1914
- recordButton.classList.add('recording');
1915
- recordButton.innerHTML = '<i class="fas fa-stop"></i>';
1916
- chatInput.disabled = true;
1917
- } catch (err) {
1918
- console.error("Error accessing microphone:", err);
1919
- alert("Не удалось получить доступ к микрофону. Проверьте разрешения в браузере.");
1920
- }
1921
- }
1922
- });
1923
-
1924
  chatSendButton.addEventListener('click', sendMessage);
1925
  chatInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendMessage(); });
1926
- chatInput.addEventListener('input', toggleSendButton);
1927
-
1928
  chatMessagesDiv.addEventListener('click', e => {
1929
  if(e.target.closest('.chat-product-link')) {
1930
  e.preventDefault();
@@ -1938,7 +1840,6 @@ CHAT_TEMPLATE = '''
1938
 
1939
  document.addEventListener('DOMContentLoaded', () => {
1940
  updateCartButton();
1941
- toggleSendButton();
1942
  window.addEventListener('click', e => { if (e.target.classList.contains('modal')) closeModal(e.target.id); });
1943
  window.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelectorAll('.modal').forEach(m => closeModal(m.id)); });
1944
 
@@ -3441,39 +3342,6 @@ def handle_chat_with_ai(env_id):
3441
  logging.error(f"Error in chat handler: {e}")
3442
  return jsonify({"error": f"Ошибка чата: {e}"}), 500
3443
 
3444
- @app.route('/<env_id>/chat_with_audio', methods=['POST'])
3445
- def handle_chat_with_audio(env_id):
3446
- if not is_chat_active(env_id):
3447
- return jsonify({"error": "Чат неактивен."}), 403
3448
-
3449
- request_data = request.get_json()
3450
- audio_base64 = request_data.get('audio')
3451
- chat_history_from_client = request_data.get('history', [])
3452
- chat_id = request_data.get('chat_id')
3453
-
3454
- if not audio_base64 or not chat_id:
3455
- return jsonify({"error": "Аудио или ID чата отсутствуют."}), 400
3456
-
3457
- try:
3458
- audio_data = base64.b64decode(audio_base64)
3459
- transcribed_text = transcribe_audio_with_ai(audio_data)
3460
-
3461
- ai_response_text = generate_chat_response(transcribed_text, chat_history_from_client, env_id)
3462
-
3463
- data = get_env_data(env_id)
3464
- if 'chats' not in data: data['chats'] = {}
3465
- if chat_id not in data['chats']: data['chats'][chat_id] = []
3466
-
3467
- timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
3468
- data['chats'][chat_id].append({'role': 'user', 'text': transcribed_text, 'timestamp': timestamp})
3469
- data['chats'][chat_id].append({'role': 'ai', 'text': ai_response_text, 'timestamp': timestamp})
3470
- save_env_data(env_id, data)
3471
-
3472
- return jsonify({"user_text": transcribed_text, "ai_text": ai_response_text})
3473
- except Exception as e:
3474
- logging.error(f"Error in audio chat handler: {e}")
3475
- return jsonify({"error": f"Ошибка чата: {e}"}), 500
3476
-
3477
  @app.route('/<env_id>/chat')
3478
  def chat_page(env_id):
3479
  if not is_chat_active(env_id):
 
304
  final_prompt = f"{base_prompt}{lang_suffix}"
305
 
306
  try:
307
+ model = genai.GenerativeModel('gemma-2-9b-it')
308
 
309
  response = model.generate_content([final_prompt, image])
310
 
 
324
  elif " Billing account not found" in str(e):
325
  raise ValueError("Проблема с биллингом аккаунта Google Cloud. Проверьте ваш аккаунт.")
326
  elif "Could not find model" in str(e):
327
+ raise ValueError(f"Модель 'gemma-2-9b-it' не найдена или недоступна.")
328
  elif "resource has been exhausted" in str(e).lower():
329
  raise ValueError("Квота запросов исчерпана. Попробуйте позже.")
330
  elif "content has been blocked" in str(e).lower():
 
335
  else:
336
  raise ValueError(f"Ошибка при генерации контента: {e}")
337
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  def generate_chat_response(message, chat_history_from_client, env_id):
339
  if not is_chat_active(env_id):
340
  return "Извините, чат в данный момент неактивен. Пожалуйста, свяжитесь с нами другим способом."
 
394
  response = None
395
 
396
  try:
397
+ model = genai.GenerativeModel('gemma-2-9b-it')
398
 
399
  model_chat_history_for_gemini = [
400
  {'role': 'user', 'parts': [{'text': system_instruction_content}]}
 
465
  h1 { font-weight: 600; color: var(--bg-medium); margin-bottom: 25px; text-align: center; }
466
  .section { margin-bottom: 30px; }
467
  .add-env-form { margin-bottom: 20px; text-align: center; }
 
 
468
  .button { padding: 10px 18px; border: none; border-radius: 6px; background-color: var(--accent); color: var(--text-on-accent); font-weight: 600; cursor: pointer; transition: background-color 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 5px; }
469
  .button:hover { background-color: var(--accent-hover); }
470
  .button:disabled { background-color: #ccc; cursor: not-allowed; }
471
  .env-list { list-style: none; padding: 0; }
472
+ .env-item { background: #fdfdff; border: 1px solid #e0e0e0; border-radius: 8px; padding: 15px; margin-bottom: 10px; display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 15px; }
473
  .env-details { display: flex; flex-direction: column; }
474
  .env-id { font-weight: 600; color: var(--bg-medium); font-size: 1.2rem; }
475
  .env-status { font-size: 0.85rem; color: #666; }
 
501
  </div>
502
 
503
  <div class="section">
504
+ <div class="search-wrapper" style="margin-bottom: 20px; position: relative;">
505
+ <i class="fas fa-search" style="position: absolute; top: 50%; left: 15px; transform: translateY(-50%); color: #aaa;"></i>
506
+ <input type="text" id="env-search-input" onkeyup="filterEnvironments()" placeholder="Поиск по ID среды..." style="width: 100%; padding: 10px 15px 10px 40px; border-radius: 6px; border: 1px solid #ddd; font-size: 1rem; box-sizing: border-box;">
507
  </div>
508
+ </div>
509
+
510
+ <div class="section">
511
+ <h2><i class="fas fa-list-ul"></i> Существующие среды</h2>
512
  {% if environments %}
513
  <ul class="env-list">
514
  {% for env in environments %}
 
549
  </div>
550
  </div>
551
  <script>
552
+ function filterEnvironments() {
553
+ const input = document.getElementById('env-search-input');
554
+ const filter = input.value.toLowerCase();
555
+ const envList = document.querySelector('.env-list');
556
+ const items = envList.getElementsByTagName('li');
557
+ let found = false;
558
+
559
+ for (let i = 0; i < items.length; i++) {
560
+ const envIdElement = items[i].querySelector('.env-id');
561
+ if (envIdElement) {
562
+ const txtValue = envIdElement.textContent || envIdElement.innerText;
563
+ if (txtValue.toLowerCase().indexOf(filter) > -1) {
564
+ items[i].style.display = "";
565
+ found = true;
566
+ } else {
567
+ items[i].style.display = "none";
568
+ }
569
+ }
570
  }
571
+
572
+ let noResultsMsg = document.getElementById('no-results');
573
+ if (!found && filter !== '') {
574
+ if (!noResultsMsg) {
575
+ noResultsMsg = document.createElement('p');
576
+ noResultsMsg.id = 'no-results';
577
+ noResultsMsg.textContent = 'Среда с таким ID не найдена.';
578
+ noResultsMsg.style.textAlign = 'center';
579
+ noResultsMsg.style.marginTop = '20px';
580
+ envList.parentNode.appendChild(noResultsMsg);
581
+ }
582
+ } else if (noResultsMsg) {
583
+ noResultsMsg.remove();
584
+ }
585
+ }
586
  </script>
587
  </body>
588
  </html>
 
1451
  border-color: var(--bg-medium);
1452
  box-shadow: 0 0 0 3px rgba(19, 93, 102, 0.15);
1453
  }
1454
+ #chat-send-button {
1455
  background-color: var(--bg-medium);
1456
  color: white;
1457
  border: none;
 
1466
  flex-shrink: 0;
1467
  font-size: 1.2rem;
1468
  }
1469
+ #chat-send-button:hover { background-color: var(--bg-dark); }
1470
+ #chat-send-button:active { transform: scale(0.9); }
 
 
 
1471
  #chat-send-button:disabled { background-color: #cccccc; cursor: not-allowed; }
1472
  .floating-buttons-container { position: fixed; bottom: 25px; right: 25px; z-index: 1000; }
1473
  .floating-button { background-color: var(--accent); color: var(--bg-dark); border: none; border-radius: 50%; width: 55px; height: 55px; font-size: 1.5rem; cursor: pointer; display: none; align-items: center; justify-content: center; box-shadow: 0 4px 15px rgba(72, 209, 204, 0.4); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
 
1513
  .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%; }
1514
  .chat-product-link:hover, .chat-add-to-cart:hover { background-color: #B2DFDB; }
1515
  body.dark-theme .chat-product-link, body.dark-theme .chat-add-to-cart { background-color: #444; color: var(--accent); }
 
 
1516
  </style>
1517
  </head>
1518
  <body class="{{ 'dark-theme' if settings.color_scheme == 'dark' else '' }}">
 
1525
  <div id="chat-messages"></div>
1526
  <div class="chat-input-container">
1527
  <input type="text" id="chat-input" placeholder="Напишите сообщение..." autocomplete="off">
1528
+ <button id="chat-send-button"><i class="fas fa-paper-plane"></i></button>
 
1529
  </div>
1530
  </div>
1531
 
 
1584
  const chatMessagesDiv = document.getElementById('chat-messages');
1585
  const chatInput = document.getElementById('chat-input');
1586
  const chatSendButton = document.getElementById('chat-send-button');
 
 
 
 
 
1587
 
1588
  function getProductById(productId) { return allProducts.find(p => p.product_id === productId); }
1589
  function getProductIndexById(productId) { return allProducts.findIndex(p => p.product_id === productId); }
 
1751
  }, 10);
1752
  }
1753
 
1754
+ function addMessageToChatUI(text, role) {
1755
  const messageElement = document.createElement('div');
1756
  messageElement.className = `chat-message ${role}`;
 
 
 
1757
 
1758
+ const productRegex = /\[ID_ТОВАРА:\\s*([a-fA-F0-9]+)\\s*Название:\\s*([^\]]+)\]/g;
1759
+ let lastIndex = 0;
1760
+ const contentFragment = document.createDocumentFragment();
1761
+ let match;
1762
+
1763
+ while ((match = productRegex.exec(text)) !== null) {
1764
+ if (match.index > lastIndex) {
1765
+ const textNode = document.createElement('div');
1766
+ textNode.className = 'message-bubble';
1767
+ textNode.innerHTML = text.substring(lastIndex, match.index).replace(/\\n/g, '<br>');
1768
+ messageElement.appendChild(textNode);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1769
  }
1770
+ const productId = match[1];
1771
+ const product = getProductById(productId);
1772
+ if (product) {
1773
+ const photoUrl = product.photos && product.photos.length > 0 ? `https://huggingface.co/datasets/${repoId}/resolve/main/photos/${product.photos[0]}` : '';
1774
+ const card = document.createElement('div');
1775
+ card.className = 'chat-product-card';
1776
+ card.innerHTML = `<img src="${photoUrl}" alt="${product.name}">
1777
+ <div class="chat-product-card-info">
1778
+ <strong>${product.name}</strong>
1779
+ <span>${product.price.toFixed(0)} ${currencyCode}</span>
1780
+ </div>
1781
+ <div class="chat-product-card-actions">
1782
+ <a href="#" class="chat-product-link" data-id="${productId}">Обзор</a>
1783
+ <a href="#" class="chat-add-to-cart" data-id="${productId}"><i class="fas fa-cart-plus"></i></a>
1784
+ </div>`;
1785
+ messageElement.appendChild(card);
1786
  }
1787
+ lastIndex = match.index + match[0].length;
 
 
 
1788
  }
1789
 
1790
+ if (lastIndex < text.length) {
1791
+ const textNode = document.createElement('div');
1792
+ textNode.className = 'message-bubble';
1793
+ textNode.innerHTML = text.substring(lastIndex).replace(/\\n/g, '<br>');
1794
+ messageElement.appendChild(textNode);
1795
+ }
1796
+
1797
  chatMessagesDiv.appendChild(messageElement);
1798
  chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
 
1799
  }
1800
+
1801
  async function sendMessage() {
1802
  const message = chatInput.value.trim();
1803
  if (!message) return;
1804
  addMessageToChatUI(message, 'user');
1805
  chatHistory.push({ role: 'user', text: message });
1806
  chatInput.value = '';
 
1807
  chatSendButton.disabled = true;
1808
 
1809
  try {
 
1824
  }
1825
  }
1826
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1827
  chatSendButton.addEventListener('click', sendMessage);
1828
  chatInput.addEventListener('keypress', e => { if (e.key === 'Enter') sendMessage(); });
1829
+
 
1830
  chatMessagesDiv.addEventListener('click', e => {
1831
  if(e.target.closest('.chat-product-link')) {
1832
  e.preventDefault();
 
1840
 
1841
  document.addEventListener('DOMContentLoaded', () => {
1842
  updateCartButton();
 
1843
  window.addEventListener('click', e => { if (e.target.classList.contains('modal')) closeModal(e.target.id); });
1844
  window.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelectorAll('.modal').forEach(m => closeModal(m.id)); });
1845
 
 
3342
  logging.error(f"Error in chat handler: {e}")
3343
  return jsonify({"error": f"Ошибка чата: {e}"}), 500
3344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3345
  @app.route('/<env_id>/chat')
3346
  def chat_page(env_id):
3347
  if not is_chat_active(env_id):