Spaces:
Build error
Build error
Update app.py
Browse files
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-
|
| 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"Модель '
|
| 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-
|
| 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;
|
| 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 |
-
|
| 520 |
-
|
| 521 |
-
|
| 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 |
-
|
| 564 |
-
const
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 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
|
| 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 |
-
#
|
| 1464 |
-
|
| 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="
|
| 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
|
| 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 |
-
|
| 1767 |
-
|
| 1768 |
-
|
| 1769 |
-
|
| 1770 |
-
|
| 1771 |
-
|
| 1772 |
-
|
| 1773 |
-
|
| 1774 |
-
|
| 1775 |
-
|
| 1776 |
-
|
| 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 |
-
|
| 1805 |
-
|
| 1806 |
-
|
| 1807 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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):
|