Hooshvare / index.html
Opera8's picture
Create index.html
39dc7b1 verified
Raw
History Blame Contribute Delete
14.6 kB
<!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>Google AI Chatbot</title>
<style>
:root {
--bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
--glass-bg: rgba(30, 41, 59, 0.85);
--glass-border: rgba(255, 255, 255, 0.08);
--text-main: #f8fafc;
--accent-color: #38bdf8;
--user-bubble: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
--ai-bubble: rgba(255, 255, 255, 0.08);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Tahoma, sans-serif;
margin: 0;
padding: 0;
background: var(--bg-gradient);
color: var(--text-main);
/* استفاده از dvh برای حل مشکل نوار آدرس در موبایل */
height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.chat-container {
width: 100%;
max-width: 850px;
height: 100%;
background: var(--glass-bg);
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
/* در صفحات بزرگتر از موبایل، حاشیه داشته باشد */
@media (min-width: 768px) {
.chat-container {
height: 90vh;
border-radius: 24px;
border: 1px solid var(--glass-border);
}
}
.chat-header {
padding: 15px 20px;
background: rgba(15, 23, 42, 0.6);
border-bottom: 1px solid var(--glass-border);
display: flex;
align-items: center;
gap: 12px;
}
.chat-header .status-dot {
width: 10px;
height: 10px;
background: #4ade80;
border-radius: 50%;
box-shadow: 0 0 10px #4ade80;
}
.chat-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: var(--accent-color);
}
.messages-box {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.messages-box::-webkit-scrollbar { width: 4px; }
.messages-box::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 10px; }
.message {
max-width: 85%;
padding: 12px 16px;
border-radius: 18px;
font-size: 14px;
line-height: 1.6;
word-wrap: break-word;
animation: fadeIn 0.3s ease forwards;
}
.message.user {
background: var(--user-bubble);
align-self: flex-start;
border-bottom-right-radius: 4px;
}
.message.ai {
background: var(--ai-bubble);
border: 1px solid var(--glass-border);
align-self: flex-end;
border-bottom-left-radius: 4px;
white-space: pre-wrap;
}
.input-wrapper {
background: rgba(15, 23, 42, 0.8);
border-top: 1px solid var(--glass-border);
padding: 12px 15px;
display: flex;
flex-direction: column;
gap: 10px;
/* فاصله برای آیفون‌های جدید */
padding-bottom: env(safe-area-inset-bottom, 12px);
}
.preview-container {
display: none;
align-items: center;
gap: 10px;
background: rgba(255, 255, 255, 0.1);
padding: 6px 10px;
border-radius: 10px;
width: fit-content;
}
.preview-container img {
width: 45px;
height: 45px;
object-fit: cover;
border-radius: 6px;
}
.preview-container .remove-btn {
color: #ef4444;
cursor: pointer;
font-weight: bold;
font-size: 12px;
padding: 5px;
}
.input-area {
display: flex;
gap: 8px;
align-items: stretch;
}
.upload-label {
background: rgba(255, 255, 255, 0.08);
border: 1px solid var(--glass-border);
border-radius: 14px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
min-height: 48px; /* مناسب برای لمس در موبایل */
}
.upload-label svg {
width: 24px;
height: 24px;
fill: var(--text-main);
}
.input-area input[type="text"] {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
padding: 0 15px;
border-radius: 14px;
color: white;
font-size: 15px;
outline: none;
min-height: 48px;
-webkit-appearance: none; /* جلوگیری از استایل‌های پیش‌فرض iOS */
}
.input-area input[type="text"]:focus {
border-color: var(--accent-color);
}
.input-area button {
background: var(--accent-color);
color: #0f172a;
border: none;
padding: 0 20px;
border-radius: 14px;
font-size: 15px;
font-weight: bold;
cursor: pointer;
min-height: 48px;
}
.input-area button:disabled {
background: #64748b;
color: #1e293b;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 12px 16px;
background: var(--ai-bubble);
border-radius: 12px;
align-self: flex-end;
border: 1px solid var(--glass-border);
}
.typing-indicator span {
width: 6px;
height: 6px;
background: var(--accent-color);
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
.typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1.0); } }
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<div class="status-dot"></div>
<h2>دستیار هوشمند گوگل</h2>
</div>
<div class="messages-box" id="messagesBox">
<div class="message ai">سلام! من متصل به هوشواره گوگل هستم. پیام یا تصویر خود را بفرستید.</div>
</div>
<div class="input-wrapper">
<div class="preview-container" id="previewContainer">
<img src="" id="imagePreview">
<span class="remove-btn" onclick="removeSelectedImage()">✕ حذف</span>
</div>
<div class="input-area">
<input type="file" id="fileInput" accept="image/*" style="display: none;" onchange="previewImage(event)">
<label for="fileInput" class="upload-label" title="آپلود تصویر">
<svg viewBox="0 0 24 24"><path d="M16.5 6v11.5c0 2.21-1.79 4-4 4s-4-1.79-4-4V5c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5v10.5c0 .55-.45 1-1 1s-1-.45-1-1V6H10v9.5c0 1.66 1.34 3 3 3s3-1.34 3-3V5c0-2.48-2.02-4.5-4.5-4.5S7 2.52 7 5v12.5c0 3.59 2.91 6.5 6.5 6.5s6.5-2.91 6.5-6V6h-1.5z"/></svg>
</label>
<input type="text" id="userInput" placeholder="پیام خود را بنویسید..." onkeypress="handleKeyPress(event)">
<button id="sendBtn" onclick="sendMessage()">ارسال</button>
</div>
</div>
</div>
<script>
const messagesBox = document.getElementById('messagesBox');
const userInput = document.getElementById('userInput');
const fileInput = document.getElementById('fileInput');
const sendBtn = document.getElementById('sendBtn');
const previewContainer = document.getElementById('previewContainer');
const imagePreview = document.getElementById('imagePreview');
function handleKeyPress(e) {
if (e.key === 'Enter') sendMessage();
}
function previewImage(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
imagePreview.src = e.target.result;
previewContainer.style.display = 'flex';
}
reader.readAsDataURL(file);
}
}
function removeSelectedImage() {
fileInput.value = '';
previewContainer.style.display = 'none';
imagePreview.src = '';
}
async function sendMessage() {
const text = userInput.value.trim();
const file = fileInput.files[0];
if (!text && !file) return;
if (file) appendImageMessage(imagePreview.src, 'user');
if (text) appendMessage(text, 'user');
userInput.value = '';
removeSelectedImage();
userInput.disabled = true;
sendBtn.disabled = true;
const indicator = document.createElement('div');
indicator.classList.add('typing-indicator');
indicator.id = 'typingIndicator';
indicator.innerHTML = '<span></span><span></span><span></span>';
messagesBox.appendChild(indicator);
messagesBox.scrollTop = messagesBox.scrollHeight;
const formData = new FormData();
formData.append('message', text || "این تصویر را تحلیل کن");
if (file) formData.append('image', file);
try {
const response = await fetch('/api/chat', { method: 'POST', body: formData });
if (!response.ok) {
document.getElementById('typingIndicator')?.remove();
appendMessage("خطا: مشکلی در دریافت پاسخ رخ داد.", 'ai');
resetInputs();
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let done = false;
let buffer = '';
let isFirstChunk = true;
let aiMessageDiv = null;
while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
if (isFirstChunk) {
document.getElementById('typingIndicator')?.remove();
aiMessageDiv = appendMessage("", 'ai');
isFirstChunk = false;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n'); // نکته: اینجا اصلاح شد
buffer = lines.pop();
for (let line of lines) {
if (line.trim().startsWith('data:')) {
try {
const jsonStr = line.substring(line.indexOf('data:') + 5).trim();
if (jsonStr) {
const data = JSON.parse(jsonStr);
aiMessageDiv.innerText = data.text;
messagesBox.scrollTop = messagesBox.scrollHeight;
}
} catch (e) {
// نادیده گرفتن استثناها
}
}
}
}
}
if (isFirstChunk) {
document.getElementById('typingIndicator')?.remove();
appendMessage("متأسفانه پاسخی دریافت نشد.", 'ai');
}
} catch (error) {
document.getElementById('typingIndicator')?.remove();
appendMessage("خطای شبکه: دریافت پاسخ از سرور انجام نشد.", 'ai');
} finally {
resetInputs();
}
}
function resetInputs() {
userInput.disabled = false;
sendBtn.disabled = false;
// فوکوس مجدد در موبایل کیبورد را باز میکند که ممکن است آزاردهنده باشد
// userInput.focus();
}
function appendMessage(text, sender) {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.innerText = text;
messagesBox.appendChild(msgDiv);
messagesBox.scrollTop = messagesBox.scrollHeight;
return msgDiv;
}
function appendImageMessage(src, sender) {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.style.padding = '5px';
const img = document.createElement('img');
img.src = src;
img.style.maxWidth = '100%';
img.style.borderRadius = '14px';
img.style.display = 'block';
msgDiv.appendChild(img);
messagesBox.appendChild(msgDiv);
messagesBox.scrollTop = messagesBox.scrollHeight;
}
</script>
</body>
</html>