| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>دستیار هوشمند Gemma-4</title> |
| <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;700&display=swap" rel="stylesheet"> |
| <style> |
| :root { --bg-color: #E5DDD5; --chat-bg: #E5DDD5; --header-bg: #075E54; --user-msg: #DCF8C6; --bot-msg: #FFFFFF; --text-main: #303030; } |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| body, html { font-family: 'Vazirmatn', sans-serif; height: 100%; background-color: var(--bg-color); overflow: hidden; } |
| .chat-container { display: flex; flex-direction: column; height: 100vh; height: 100dvh; max-width: 800px; margin: 0 auto; background: var(--chat-bg); box-shadow: 0 0 20px rgba(0,0,0,0.1); } |
| .chat-header { background: var(--header-bg); color: white; padding: 15px 20px; display: flex; align-items: center; gap: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); z-index: 10; } |
| .avatar { width: 45px; height: 45px; background: #fff; border-radius: 50%; display: flex; justify-content: center; align-items: center; font-size: 24px; } |
| .header-info h2 { font-size: 1.1rem; font-weight: 700; margin-bottom: 2px; } |
| .header-info p { font-size: 0.8rem; color: #d1d1d1; } |
| .chat-box { flex-grow: 1; padding: 20px 15px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; background-image: url('https://www.transparenttextures.com/patterns/cubes.png'); } |
| .message-row { display: flex; flex-direction: column; max-width: 85%; animation: fadeIn 0.3s ease; } |
| .message-row.user { align-self: flex-start; } |
| .message-row.bot { align-self: flex-end; } |
| .message-bubble { padding: 12px 15px; border-radius: 18px; font-size: 0.95rem; line-height: 1.6; color: var(--text-main); position: relative; word-wrap: break-word; } |
| .message-row.user .message-bubble { background: var(--user-msg); border-top-right-radius: 4px; box-shadow: 1px 1px 3px rgba(0,0,0,0.08); } |
| .message-row.bot .message-bubble { background: var(--bot-msg); border-top-left-radius: 4px; box-shadow: -1px 1px 3px rgba(0,0,0,0.08); } |
| .file-preview-in-chat { max-width: 100%; border-radius: 12px; margin-bottom: 8px; } |
| .typing-indicator { display: none; padding: 12px 15px; background: var(--bot-msg); border-radius: 18px; border-top-left-radius: 4px; align-self: flex-end; box-shadow: -1px 1px 3px rgba(0,0,0,0.08); margin-top: 5px;} |
| .typing-indicator span { display: inline-block; width: 6px; height: 6px; background-color: #90949C; border-radius: 50%; margin: 0 2px; animation: bounce 1.3s linear infinite; } |
| .typing-indicator span:nth-child(2) { animation-delay: -1.1s; } |
| .typing-indicator span:nth-child(3) { animation-delay: -0.9s; } |
| .input-area { background: #f0f0f0; padding: 10px 15px; display: flex; flex-direction: column; z-index: 10; } |
| .attachment-preview { display: none; align-items: center; background: white; padding: 8px 12px; border-radius: 12px; margin-bottom: 10px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); gap: 10px;} |
| .attachment-preview img { width: 40px; height: 40px; border-radius: 6px; object-fit: cover; } |
| .attachment-preview .file-info { flex-grow: 1; font-size: 0.85rem; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
| .remove-file { background: #ff4d4d; color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; display: flex; justify-content: center; align-items: center; font-size: 14px;} |
| .input-wrapper { display: flex; align-items: flex-end; gap: 10px; } |
| .attach-btn { background: transparent; border: none; color: #888; font-size: 24px; cursor: pointer; padding: 5px; transition: color 0.3s; display: flex; align-items: center;} |
| .attach-btn:hover { color: var(--header-bg); } |
| #file-input { display: none; } |
| textarea { flex-grow: 1; border: none; border-radius: 20px; padding: 12px 15px; font-family: 'Vazirmatn', sans-serif; font-size: 0.95rem; resize: none; max-height: 120px; overflow-y: auto; outline: none; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } |
| .send-btn { background: var(--header-bg); color: white; border: none; border-radius: 50%; width: 45px; height: 45px; display: flex; justify-content: center; align-items: center; cursor: pointer; transition: transform 0.2s, background 0.3s; box-shadow: 0 2px 5px rgba(0,0,0,0.2); flex-shrink: 0;} |
| .send-btn:hover { background: #054c44; transform: scale(1.05); } |
| .send-btn:disabled { background: #a5c0bd; cursor: not-allowed; transform: none; } |
| .send-btn svg { width: 20px; height: 20px; transform: translateX(-2px); } |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes bounce { 0%, 60%, 100% { transform: translateY(0); } 30% { transform: translateY(-5px); } } |
| .message-bubble b, .message-bubble strong { font-weight: 700; } |
| .message-bubble code { background: #f4f4f4; padding: 2px 5px; border-radius: 4px; font-family: monospace; font-size: 0.9rem; direction: ltr; display: inline-block; white-space: pre-wrap; word-wrap: break-word; max-width: 100%;} |
| </style> |
| </head> |
| <body> |
|
|
| <div class="chat-container"> |
| <header class="chat-header"> |
| <div class="avatar">🤖</div> |
| <div class="header-info"> |
| <h2>Gemma-4 (پرو)</h2> |
| <p id="status-text">آماده پاسخگویی به شما</p> |
| </div> |
| </header> |
|
|
| <div class="chat-box" id="chat-box"> |
| <div class="message-row bot"> |
| <div class="message-bubble"> |
| سلام! من دستیار هوشمند Gemma هستم. میتونی سوالاتت رو بپرسی یا عکس و فایل صوتی بفرستی تا بررسی کنم. 😊 |
| </div> |
| </div> |
| </div> |
|
|
| <div class="typing-indicator" id="typing-indicator"> |
| <span></span><span></span><span></span> |
| </div> |
|
|
| <div class="input-area"> |
| <div class="attachment-preview" id="attachment-preview"> |
| <img src="" alt="preview" id="preview-img" style="display:none;"> |
| <div id="file-icon" style="font-size: 24px; display:none;">📄</div> |
| <div class="file-info" id="file-name">نام فایل</div> |
| <button class="remove-file" id="remove-file-btn">✕</button> |
| </div> |
|
|
| <div class="input-wrapper"> |
| <button class="attach-btn" id="attach-btn" title="ارسال عکس یا فایل"> |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg> |
| </button> |
| <input type="file" id="file-input" accept="image/*,audio/*,video/*"> |
| <textarea id="text-input" rows="1" placeholder="پیام خود را بنویسید..."></textarea> |
| <button class="send-btn" id="send-btn"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| const chatBox = document.getElementById('chat-box'); |
| const textInput = document.getElementById('text-input'); |
| const attachBtn = document.getElementById('attach-btn'); |
| const fileInput = document.getElementById('file-input'); |
| const sendBtn = document.getElementById('send-btn'); |
| const typingIndicator = document.getElementById('typing-indicator'); |
| const statusText = document.getElementById('status-text'); |
| |
| const attachmentPreview = document.getElementById('attachment-preview'); |
| const previewImg = document.getElementById('preview-img'); |
| const fileIcon = document.getElementById('file-icon'); |
| const fileNameDisplay = document.getElementById('file-name'); |
| const removeFileBtn = document.getElementById('remove-file-btn'); |
| |
| let currentFile = null; |
| |
| textInput.addEventListener('input', function() { |
| this.style.height = 'auto'; |
| this.style.height = (this.scrollHeight < 120 ? this.scrollHeight : 120) + 'px'; |
| }); |
| |
| attachBtn.addEventListener('click', () => fileInput.click()); |
| |
| fileInput.addEventListener('change', function() { |
| const file = this.files[0]; |
| if (file) { |
| currentFile = file; |
| fileNameDisplay.textContent = file.name; |
| attachmentPreview.style.display = 'flex'; |
| |
| if (file.type.startsWith('image/')) { |
| previewImg.src = URL.createObjectURL(file); |
| previewImg.style.display = 'block'; |
| fileIcon.style.display = 'none'; |
| } else { |
| previewImg.style.display = 'none'; |
| fileIcon.style.display = 'block'; |
| } |
| textInput.focus(); |
| } |
| }); |
| |
| removeFileBtn.addEventListener('click', () => { |
| currentFile = null; |
| fileInput.value = ''; |
| attachmentPreview.style.display = 'none'; |
| if (previewImg.src) URL.revokeObjectURL(previewImg.src); |
| }); |
| |
| function appendMessage(text, isUser, fileUrl = null, isImage = false) { |
| const row = document.createElement('div'); |
| row.className = `message-row ${isUser ? 'user' : 'bot'}`; |
| |
| let mediaHtml = ''; |
| if (fileUrl) { |
| if (isImage) mediaHtml = `<img src="${fileUrl}" class="file-preview-in-chat"><br>`; |
| else mediaHtml = `<div style="background:rgba(0,0,0,0.1); padding:8px; border-radius:8px; margin-bottom:8px;">📎 فایل ضمیمه شد</div>`; |
| } |
| |
| const formattedText = text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>').replace(/\n/g, '<br>'); |
| row.innerHTML = `<div class="message-bubble">${mediaHtml}${formattedText}</div>`; |
| chatBox.appendChild(row); |
| chatBox.scrollTop = chatBox.scrollHeight; |
| return row.querySelector('.message-bubble'); |
| } |
| |
| sendBtn.addEventListener('click', async () => { |
| const text = textInput.value.trim(); |
| if (!text && !currentFile) return; |
| |
| let fileUrlForUI = null; |
| let isImage = false; |
| if (currentFile) { |
| fileUrlForUI = URL.createObjectURL(currentFile); |
| isImage = currentFile.type.startsWith('image/'); |
| } |
| appendMessage(text, true, fileUrlForUI, isImage); |
| |
| const fileToSend = currentFile; |
| const textToSend = text; |
| |
| textInput.value = ''; |
| textInput.style.height = 'auto'; |
| removeFileBtn.click(); |
| |
| sendBtn.disabled = true; |
| textInput.disabled = true; |
| attachBtn.disabled = true; |
| typingIndicator.style.display = 'block'; |
| chatBox.scrollTop = chatBox.scrollHeight; |
| statusText.textContent = "در حال اتصال به هوش مصنوعی..."; |
| |
| try { |
| const formData = new FormData(); |
| formData.append('prompt', textToSend); |
| if (fileToSend) formData.append('file', fileToSend); |
| |
| const response = await fetch('/api/chat', { method: 'POST', body: formData }); |
| const data = await response.json(); |
| |
| if (data.status === 'success') { |
| const botBubble = appendMessage('', false); |
| let isFirstChunk = true; |
| |
| const eventSource = new EventSource(`/api/stream_chat/${data.run_id}`); |
| |
| eventSource.onmessage = function(event) { |
| if (event.data === "DONE") { |
| eventSource.close(); |
| statusText.textContent = "آماده پاسخگویی به شما"; |
| sendBtn.disabled = false; textInput.disabled = false; attachBtn.disabled = false; |
| textInput.focus(); |
| } else { |
| if (isFirstChunk) { |
| typingIndicator.style.display = 'none'; |
| statusText.textContent = "در حال نوشتن..."; |
| isFirstChunk = false; |
| } |
| try { |
| const parsed = JSON.parse(event.data); |
| const formattedText = parsed.text.replace(/\*\*(.*?)\*\*/g, '<b>$1</b>').replace(/\n/g, '<br>'); |
| botBubble.innerHTML = formattedText; |
| chatBox.scrollTop = chatBox.scrollHeight; |
| } catch (e) {} |
| } |
| }; |
| |
| eventSource.onerror = function() { |
| eventSource.close(); |
| typingIndicator.style.display = 'none'; |
| statusText.textContent = "آماده پاسخگویی به شما"; |
| |
| |
| if (botBubble.innerHTML === '' || botBubble.innerHTML === '<br>') { |
| botBubble.innerHTML = "⚠️ خطای شبکه: اتصال استریم با سرور قطع شد."; |
| } else { |
| botBubble.innerHTML += "<br><br>⚠️ <i>(ارتباط استریم با سرور قطع شد)</i>"; |
| } |
| |
| sendBtn.disabled = false; textInput.disabled = false; attachBtn.disabled = false; |
| }; |
| } else { |
| throw new Error(data.message || "خطای ناشناخته از سمت سرور"); |
| } |
| } catch (error) { |
| typingIndicator.style.display = 'none'; |
| statusText.textContent = "آماده پاسخگویی به شما"; |
| appendMessage(`⚠️ خطا: ${error.message}`, false); |
| sendBtn.disabled = false; textInput.disabled = false; attachBtn.disabled = false; |
| } |
| }); |
| |
| textInput.addEventListener('keypress', function (e) { |
| if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendBtn.click(); } |
| }); |
| </script> |
| </body> |
| </html> |