ai-thought-processor / index.html
awewq33's picture
Manual changes saved
bdd2a14 verified
<!DOCTYPE html>
<html dir="rtl" lang="fa">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>AI Chat - Powered by Hugging Face</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@2.4.5/dist/purify.min.js"></script>
<script>
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true,
packages: {'[+]': ['ams', 'physics', 'mathtools']}
},
options: {
skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code'],
ignoreHtmlClass: 'tex2jax_ignore'
}
};
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&display=swap" rel="stylesheet"/>
<style>
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;700&display=swap');
body {
font-family: 'Vazirmatn', sans-serif;
background-color: #f8f7f7;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
margin: 0;
overflow: hidden;
}
.chat-display {
scrollbar-width: thin;
scrollbar-color: #888 #f1f1f1;
-webkit-overflow-scrolling: touch;
overflow-y: auto;
padding: 16px;
height: calc(100vh - 136px);
scroll-behavior: smooth;
font-size: large;
display: flex;
flex-direction: column;
align-items: center;
direction: ltr;
}
.chat-display::-webkit-scrollbar {
width: 5px;
}
.chat-display::-webkit-scrollbar-track {
background: #f1f1f1;
}
.chat-display::-webkit-scrollbar-thumb {
background-color: #888;
border-radius: 4px;
}
.message-container {
margin: 16px 0;
width: 100%;
max-width: 800px;
display: flex;
justify-content: center;
position: relative;
}
.message-box {
width: 100%;
font-size: 18px;
border-radius: 12px;
padding: 12px 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
line-height: 1.6;
overflow-wrap: break-word;
word-break: break-word;
}
.user-message {
background-color: #3b82f6;
color: white;
direction: rtl;
}
.ai-message {
background-color: #ffffff;
}
.message-actions {
display: flex;
gap: 8px;
margin-top: 8px;
justify-content: flex-end;
}
.message-action-btn {
background-color: rgba(255, 255, 255, 0.2);
color: white;
padding: 8px;
font-size: 18px;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
border: 1px solid rgba(255,255,255,0.3);
margin-top: 10px;
margin-left: 2px;
margin-right: 2px;
}
.message-action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.copy-message-btn {
background-color: #3b82f6;
}
.copy-message-btn:hover {
background-color: #2563eb;
}
.edit-message-btn {
background-color: #10b981;
}
.edit-message-btn:hover {
background-color: #059669;
}
.regenerate-message-btn {
background-color: #f59e0b;
}
.regenerate-message-btn:hover {
background-color: #d97706;
}
.user-message .message-action-btn {
background-color: rgba(255, 255, 255, 0.3);
}
.user-message .copy-message-btn:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.user-message .edit-message-btn:hover {
background-color: rgba(255, 255, 255, 0.5);
}
.code-block {
border: none;
border-radius: 12px;
margin: 16px 0;
background-color: #1e293b;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
direction: ltr;
text-align: left;
overflow: hidden;
}
.code-header {
background-color: #334155;
padding: 8px 12px;
font-weight: 600;
color: #e2e8f0;
font-size: 18px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #475569;
}
.code-content {
padding: 16px;
font-family: 'Fira Code', 'Consolas', monospace;
font-size: 18px;
color: #e2e8f0;
white-space: pre-wrap;
background-color: #1e293b;
line-height: 1.6;
border-radius: 0 0 12px 12px;
overflow-x: auto;
}
.code-content pre {
margin: 0;
padding: 0;
}
.code-content code {
display: block;
background: none;
padding: 0;
border-radius: 0;
line-height: 1.6;
}
.code-content code.hljs {
background: transparent;
padding: 0;
}
.copy-btn {
background-color: #3b82f6;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.copy-btn:hover {
background-color: #2563eb;
transform: translateY(-1px);
}
.copy-btn.copied {
background-color: #10b981;
}
.copy-btn.copied:hover {
background-color: #059669;
}
.copy-btn i {
font-size: 14px;
}
.input-container {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background-color: white;
border-radius: 12px;
box-shadow: 0 1px 5px rgba(0,0,0,0.1);
margin: 8px;
width: calc(100% - 16px);
}
.message-input {
flex: 1;
font-size: 18px;
border: none;
outline: none;
padding: 10px;
background: transparent;
resize: none;
min-height: 44px;
max-height: 120px;
}
.send-btn {
background: #3b82f6;
color: white;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.send-btn:hover {
background: #1d4ed8;
}
.send-btn.stop {
background: #ef4444;
}
.send-btn.stop:hover {
background: #dc2626;
}
.file-upload {
position: relative;
overflow: hidden;
display: inline-block;
width: 44px;
height: 44px;
}
.file-upload input[type="file"] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-btn {
background: #6b7280;
color: white;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.file-btn:hover {
background: #4b5563;
}
.rtl-text {
direction: rtl;
text-align: right;
}
.ltr-text {
direction: ltr;
text-align: left;
}
.latex-expression {
direction: ltr;
display: inline-block;
vertical-align: middle;
}
.typing-indicator {
display: flex;
gap: 4px;
padding: 8px;
}
.typing-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #888;
animation: typingAnimation 1.4s infinite ease-in-out;
}
.typing-dot:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typingAnimation {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-5px); }
}
.mobile-header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background-color: white;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
z-index: 100;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.mobile-title {
font-size: 18px;
font-weight: bold;
color: #1f2937;
}
.chat-container {
margin-top: 56px;
margin-bottom: 80px;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.input-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 8px;
background-color: #ffffff;
z-index: 100;
}
.mobile-sidebar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 80%;
max-width: 300px;
background-color: white;
box-shadow: -5px 0 15px rgba(0,0,0,0.1);
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 1000;
padding-top: 56px;
overflow-y: auto;
}
.mobile-sidebar.show {
transform: translateX(0);
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,0.5);
z-index: 999;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.sidebar-overlay.show {
opacity: 1;
pointer-events: all;
}
.mobile-model-selector {
position: fixed;
bottom: 80px;
right: 12px;
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
padding: 12px;
width: calc(100% - 24px);
max-width: 400px;
transform: translateY(120%);
transition: transform 0.3s ease;
z-index: 1000;
max-height: 70vh;
overflow-y: auto;
}
.mobile-model-selector.show {
transform: translateY(0);
}
.model-close-btn {
position: absolute;
top: 8px;
left: 8px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f3f4f6;
border-radius: 50%;
color: #4b5563;
}
.chatbot-option {
transition: all 0.2s ease;
border-radius: 8px;
padding: 8px;
margin: 4px 0;
}
.chatbot-option.selected {
background-color: #e6f3ff;
}
.notification {
position: fixed;
bottom: 90px;
left: 50%;
transform: translateX(-50%);
background-color: #ef4444;
color: white;
padding: 8px 16px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
max-width: 90%;
}
.notification.show {
opacity: 1;
}
.delete-chat-btn {
color: #ef4444;
background: none;
border: none;
padding: 4px;
opacity: 0.6;
transition: opacity 0.2s;
}
.delete-chat-btn:hover {
opacity: 1;
}
.mjx-chtml {
display: inline-block;
vertical-align: middle;
max-width: 100%;
}
.loading {
opacity: 0.7;
cursor: not-allowed;
}
.edit-textarea {
width: 100%;
min-height: 80px;
max-height: 500px;
font-size: 18px;
font-family: 'Vazirmatn', sans-serif;
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 8px;
resize: vertical;
background-color: #f9fafb;
direction: rtl;
overflow-y: auto;
}
.user-edit-textarea {
color: #ffffff;
background-color: #3b82f6;
border: 1px solid #1d4ed8;
}
.edit-actions {
display: flex;
gap: 8px;
margin-top: 8px;
justify-content: flex-end;
}
.save-edit-btn {
background-color: #10b981;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.save-edit-btn:hover {
background-color: #059669;
}
.cancel-edit-btn {
background-color: #ef4444;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s;
}
.cancel-edit-btn:hover {
background-color: #dc2626;
}
</style>
</head>
<body class="h-screen overflow-hidden">
<div class="mobile-header">
<button aria-label="باز کردن منو" class="w-10 h-10 flex items-center justify-center" id="mobileMenuBtn">
<i class="fas fa-bars text-gray-600"></i>
</button>
<div class="mobile-title" id="currentChatTitle">AI Chat</div>
<button aria-label="تغییر مدل هوش مصنوعی" class="w-10 h-10 flex items-center justify-center" id="modelSwitchBtn">
<i class="fas fa-brain text-gray-600"></i>
</button>
</div>
<div class="chat-container chat-display" id="chatDisplay">
<div class="text-center text-gray-500 py-10">
<i class="fas fa-comment-alt text-4xl mb-2"></i>
<p>گفتگویی جدید با هوش مصنوعی شروع کنید</p>
</div>
</div>
<div class="input-area">
<div class="input-container">
<div class="file-upload">
<button aria-label="انتخاب فایل" class="file-btn">
<i class="fas fa-paperclip"></i>
</button>
<input accept="image/*" id="fileInput" type="file"/>
</div>
<textarea aria-label="ورود پیام" class="message-input" id="messageInput" placeholder="پیام خود را بنویسید..." rows="1"></textarea>
<button aria-label="ارسال پیام" class="send-btn" id="sendButton">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<div class="sidebar-overlay" id="sidebarOverlay"></div>
<div class="mobile-sidebar" id="mobileSidebar">
<div class="p-4 border-b border-gray-200">
<button class="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-lg" id="newChatBtn">
<i class="fas fa-plus ml-2"></i> گفتگوی جدید
</button>
</div>
<div class="flex-1 overflow-y-auto">
<ul class="py-2" id="chatList"></ul>
</div>
</div>
<div class="mobile-model-selector" id="mobileModelSelector">
<button aria-label="بستن انتخاب‌گر مدل" class="model-close-btn" id="modelCloseBtn">
<i class="fas fa-times"></i>
</button>
<h3 class="text-xl font-bold mb-5 text-center text-gray-800">انتخاب هوش مصنوعی</h3>
<div class="model-selector-container space-y-3">
<!-- Llama 4 Scout -->
<div class="chatbot-option selected cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]" data-model="meta-llama/llama-4-scout-17b-16e-instruct">
<div class="flex items-center p-3 rounded-xl">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center shadow-lg mr-2">
<i class="fas fa-robot text-white"></i>
</div>
<div class="flex-1 mr-4 space-y-1">
<div class="font-bold text-gray-800">Llama 4 Scout</div>
<div class="text-sm text-green-600 font-medium flex items-center ">
<i class="fas fa-image"></i> .پشتیبانی کامل از تصاویر
</div>
</div>
<div class="selected-indicator">
<i class="fas fa-check-circle text-blue-500 text-xl opacity-0 transition-opacity"></i>
</div>
</div>
</div>
<!-- Llama 4 Maverick -->
<div class="chatbot-option cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]" data-model="Llama-4-Maverick-17B-128E-Instruct">
<div class="flex items-center p-3 rounded-xl">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-red-400 to-red-600 flex items-center justify-center shadow-lg mr-2">
<i class="fas fa-hat-cowboy text-white"></i>
</div>
<div class="flex-1 mr-4 space-y-2">
<div class="font-bold text-gray-800">Llama 4 Maverick</div>
<div class="text-sm text-green-600 font-medium flex items-center">
<i class="fas fa-image"></i> .پشتیبانی کامل از تصاویر
</div>
</div>
<div class="selected-indicator">
<i class="fas fa-check-circle text-blue-500 text-xl opacity-0 transition-opacity"></i>
</div>
</div>
</div>
<!-- MiniMax-M2 -->
<div class="chatbot-option cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]" data-model="MiniMaxAI/MiniMax-M2:novita">
<div class="flex items-center p-3 rounded-xl">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-purple-400 to-purple-600 flex items-center justify-center shadow-lg mr-2">
<i class="fas fa-gem text-white"></i>
</div>
<div class="flex-1 mr-4 space-y-1">
<div class="font-bold text-gray-800">MiniMax-M2</div>
<div class="text-sm text-red-600 font-medium flex items-center">
<i class="fas fa-times"></i> بدون پشتیبانی از تصاویر
</div>
</div>
<div class="selected-indicator">
<i class="fas fa-check-circle text-blue-500 text-xl opacity-0 transition-opacity"></i>
</div>
</div>
</div>
<!-- gpt-oss-120b -->
<div class="chatbot-option cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]" data-model="openai/gpt-oss-120b:novita">
<div class="flex items-center p-3 rounded-xl">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-indigo-400 to-indigo-600 flex items-center justify-center shadow-lg mr-2">
<i class="fas fa-star text-white"></i>
</div>
<div class="flex-1 mr-4 space-y-1">
<div class="font-bold text-gray-800">gpt-oss-120b</div>
<div class="text-sm text-red-600 font-medium flex items-center">
<i class="fas fa-times"></i> بدون پشتیبانی از تصاویر
</div>
</div>
<div class="selected-indicator">
<i class="fas fa-check-circle text-blue-500 text-xl opacity-0 transition-opacity"></i>
</div>
</div>
</div>
<!-- DeepSeek -->
<div class="chatbot-option cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]" data-model="deepseek-ai/DeepSeek-V3.2-Exp:novita">
<div class="flex items-center p-3 rounded-xl">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-green-400 to-green-600 flex items-center justify-center shadow-lg mr-2">
<i class="fas fa-brain text-white"></i>
</div>
<div class="flex-1 mr-4 space-y-1">
<div class="font-bold text-gray-800">DeepSeek</div>
<div class="text-sm text-red-600 font-medium flex items-center">
<i class="fas fa-times"></i> بدون پشتیبانی از تصاویر
</div>
</div>
<div class="selected-indicator">
<i class="fas fa-check-circle text-blue-500 text-xl opacity-0 transition-opacity"></i>
</div>
</div>
</div>
<!-- DeepSeek Turbo -->
<div class="chatbot-option cursor-pointer transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]" data-model="zai-org/GLM-4.6:novita">
<div class="flex items-center p-3 rounded-xl">
<div class="w-12 h-12 rounded-2xl bg-gradient-to-br from-yellow-400 to-orange-500 flex items-center justify-center shadow-lg mr-2">
<i class="fas fa-bolt text-white"></i>
</div>
<div class="flex-1 mr-4 space-y-1">
<div class="font-bold text-gray-800">GLM-4.6</div>
<div class="text-sm text-red-600 font-medium flex items-center">
<i class="fas fa-times"></i> بدون پشتیبانی از تصاویر
</div>
</div>
<div class="selected-indicator">
<i class="fas fa-check-circle text-blue-500 text-xl opacity-0 transition-opacity"></i>
</div>
</div>
</div>
</div>
<style>
.chatbot-option {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 2px solid transparent;
transition: all 0.3s ease;
}
.chatbot-option:hover {
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
border-color: #e2e8f0;
transform: translateY(-2px);
}
.chatbot-option.selected {
background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);
border-color: #3b82f6;
}
.chatbot-option.selected .selected-indicator i {
opacity: 1 !important;
}
</style>
</div>
<div class="notification" id="notification">
<i class="fas fa-exclamation-circle mr-2"></i>
<span id="notificationText"></span>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const debounce = (func, wait) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
};
const sanitizeHTML = (html) => DOMPurify.sanitize(html, {
ADD_TAGS: ['mjx-container', 'mjx-math', 'mjx-mrow', 'mjx-mi', 'mjx-mo', 'mjx-mfrac', 'mjx-mn', 'mjx-msup', 'mjx-mtext'],
ADD_ATTR: ['aria-hidden', 'jax', 'data-jax', 'display']
});
const MODEL_CONFIGS = {
"MiniMaxAI/MiniMax-M2:novita": {
"url": "https://router.huggingface.co/v1/chat/completions",
"supports_images": false
},
"deepseek-ai/DeepSeek-V3.2-Exp:novita": {
"url": "https://router.huggingface.co/v1/chat/completions",
"supports_images": false
},
"zai-org/GLM-4.6:novita": {
"url": "https://router.huggingface.co/v1/chat/completions",
"supports_images": false
},
"openai/gpt-oss-120b:novita": {
"url": "https://router.huggingface.co/v1/chat/completions",
"supports_images": false
},
"meta-llama/llama-4-scout-17b-16e-instruct": {
"url": "https://router.huggingface.co/v1/chat/completions",
"supports_images": true
},
"Llama-4-Maverick-17B-128E-Instruct": {
"url": "https://router.huggingface.co/sambanova/v1/chat/completions",
"supports_images": true
}
};
const elements = {
chatDisplay: document.getElementById('chatDisplay'),
messageInput: document.getElementById('messageInput'),
sendButton: document.getElementById('sendButton'),
fileInput: document.getElementById('fileInput'),
newChatBtn: document.getElementById('newChatBtn'),
chatList: document.getElementById('chatList'),
chatbotOptions: document.querySelectorAll('.chatbot-option'),
mobileMenuBtn: document.getElementById('mobileMenuBtn'),
mobileSidebar: document.getElementById('mobileSidebar'),
sidebarOverlay: document.getElementById('sidebarOverlay'),
modelSwitchBtn: document.getElementById('modelSwitchBtn'),
mobileModelSelector: document.getElementById('mobileModelSelector'),
modelCloseBtn: document.getElementById('modelCloseBtn'),
currentChatTitle: document.getElementById('currentChatTitle'),
notification: document.getElementById('notification'),
notificationText: document.getElementById('notificationText')
};
const state = {
chats: JSON.parse(localStorage.getItem('chats')) || [],
currentChatId: null,
isAIResponding: false,
currentModel: "meta-llama/llama-4-scout-17b-16e-instruct",
currentAIMessageDiv: null,
abortController: null,
currentAIResponse: ''
};
hljs.configure({ languages: ['javascript', 'python', 'html', 'css', 'java', 'cpp'] });
hljs.highlightAll();
function init() {
renderChatList();
if (state.chats.length === 0) {
createNewChat();
} else {
loadChat(state.chats[0].id);
}
elements.messageInput.removeEventListener('input', adjustInputHeight);
elements.messageInput.addEventListener('input', debounce(adjustInputHeight, 100));
window.removeEventListener('resize', adjustChatContainerHeight);
window.addEventListener('resize', adjustChatContainerHeight);
elements.chatDisplay.removeEventListener('touchmove', () => {});
elements.chatDisplay.addEventListener('touchmove', () => {}, { passive: true });
elements.sendButton.removeEventListener('click', handleSendButtonClick);
elements.sendButton.addEventListener('click', handleSendButtonClick);
setupEventListeners();
}
function setupEventListeners() {
elements.messageInput.removeEventListener('keydown', handleMessageInputKeydown);
elements.messageInput.addEventListener('keydown', handleMessageInputKeydown);
elements.fileInput.removeEventListener('change', handleFileInputChange);
elements.fileInput.addEventListener('change', handleFileInputChange);
elements.newChatBtn.removeEventListener('click', createNewChat);
elements.newChatBtn.addEventListener('click', createNewChat);
elements.mobileMenuBtn.removeEventListener('click', openSidebar);
elements.mobileMenuBtn.addEventListener('click', openSidebar);
elements.modelSwitchBtn.removeEventListener('click', openModelSelector);
elements.modelSwitchBtn.addEventListener('click', openModelSelector);
elements.modelCloseBtn.removeEventListener('click', closeModelSelector);
elements.modelCloseBtn.addEventListener('click', closeModelSelector);
elements.sidebarOverlay.removeEventListener('click', closeSidebar);
elements.sidebarOverlay.addEventListener('click', closeSidebar);
elements.chatbotOptions.forEach(option => {
option.removeEventListener('click', handleChatbotOptionClick);
option.addEventListener('click', () => handleChatbotOptionClick(option));
});
}
function handleMessageInputKeydown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (!state.isAIResponding) sendMessage();
}
}
function handleFileInputChange() {
if (elements.fileInput.files[0]) {
showNotification(`فایل انتخاب شد: ${elements.fileInput.files[0].name}`);
}
}
function handleChatbotOptionClick(option) {
state.currentModel = option.dataset.model;
updateSelectedModelUI();
if (state.currentChatId) {
const chat = state.chats.find(c => c.id === state.currentChatId);
chat.model = state.currentModel;
saveChats();
renderChatList();
}
showNotification(`مدل: ${getModelDisplayName(state.currentModel)}`);
closeModelSelector();
}
function adjustInputHeight() {
elements.messageInput.style.height = 'auto';
elements.messageInput.style.height = elements.messageInput.scrollHeight + 'px';
adjustChatContainerHeight();
}
function adjustTextareaHeight(textarea) {
textarea.style.height = 'auto';
textarea.style.height = `${textarea.scrollHeight}px`;
}
function adjustChatContainerHeight() {
const inputAreaHeight = document.querySelector('.input-area').offsetHeight;
elements.chatDisplay.style.marginBottom = inputAreaHeight + 'px';
scrollToBottom();
}
function createNewChat() {
const newChat = {
id: Date.now().toString(),
title: 'گفتگوی جدید',
messages: [],
createdAt: new Date().toISOString(),
model: state.currentModel
};
state.chats.unshift(newChat);
saveChats();
renderChatList();
loadChat(newChat.id);
closeSidebar();
}
function loadChat(chatId) {
state.currentChatId = chatId;
const chat = state.chats.find(c => c.id === chatId);
if (!chat) return;
state.currentModel = chat.model || "meta-llama/llama-4-scout-17b-16e-instruct";
elements.currentChatTitle.textContent = chat.title;
updateSelectedModelUI();
document.querySelectorAll('#chatList li').forEach(li => {
li.classList.toggle('bg-blue-100', li.dataset.chatId === chatId);
li.classList.toggle('text-blue-600', li.dataset.chatId === chatId);
});
renderMessages(chat.messages);
setTimeout(scrollToBottom, 50);
}
function deleteChat(chatId) {
state.chats = state.chats.filter(chat => chat.id !== chatId);
saveChats();
if (state.currentChatId === chatId) {
state.chats.length > 0 ? loadChat(state.chats[0].id) : createNewChat();
}
renderChatList();
showNotification('گفتگو حذف شد');
}
function updateSelectedModelUI() {
elements.chatbotOptions.forEach(option => {
option.classList.toggle('selected', option.dataset.model === state.currentModel);
});
}
function saveChats() {
localStorage.setItem('chats', JSON.stringify(state.chats));
}
function renderChatList() {
elements.chatList.innerHTML = state.chats.length === 0
? `<div class="text-center text-gray-500 py-4">
<i class="fas fa-comment-alt text-2xl mb-2"></i>
<p>گفتگویی وجود ندارد</p>
</div>`
: '';
state.chats.forEach(chat => {
const li = document.createElement('li');
li.dataset.chatId = chat.id;
li.className = `px-4 py-3 hover:bg-gray-100 cursor-pointer rtl-text border-b border-gray-100 flex justify-between items-center ${chat.id === state.currentChatId ? 'bg-blue-100 text-blue-600' : ''}`;
li.innerHTML = `
<div class="flex-1">
<div class="font-medium truncate">${sanitizeHTML(chat.title)}</div>
<div class="text-xs text-gray-500">${formatDate(chat.createdAt)}</div>
</div>
<button class="delete-chat-btn" data-chat-id="${chat.id}" aria-label="حذف گفتگو">
<i class="fas fa-trash"></i>
</button>
`;
li.addEventListener('click', e => {
if (!e.target.closest('.delete-chat-btn')) {
loadChat(chat.id);
closeSidebar();
}
});
li.querySelector('.delete-chat-btn').addEventListener('click', e => {
e.stopPropagation();
deleteChat(chat.id);
});
elements.chatList.appendChild(li);
});
}
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('fa-IR');
}
function renderMessages(messages) {
elements.chatDisplay.innerHTML = messages.length === 0
? `<div class="text-center text-gray-500 py-10">
<i class="fas fa-comment-alt text-4xl mb-2"></i>
<p>گفتگویی جدید با هوش مصنوعی شروع کنید</p>
</div>`
: '';
messages.forEach((msg, index) => {
msg.role === 'user' ? addUserMessage(msg.content, false, index) : addAIMessage(msg.content, false, index);
});
MathJax.typesetPromise().then(() => {
hljs.highlightAll();
scrollToBottom();
}).catch(err => console.error('MathJax Error:', err));
}
function isPersianText(text) {
return /[\u0600-\u06FF\uFB8A\u067E\u0686\u06AF\u200C\u200F]/.test(text);
}
function editUserMessage(messageIndex) {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (!chat || !chat.messages[messageIndex] || chat.messages[messageIndex].role !== 'user') return;
const messageContainer = elements.chatDisplay.querySelector(`.message-container[data-message-index="${messageIndex}"]`);
const messageDiv = messageContainer.querySelector('.message-box');
const originalContent = Array.isArray(chat.messages[messageIndex].content)
? chat.messages[messageIndex].content.find(item => item.type === 'text')?.text || ''
: chat.messages[messageIndex].content;
const textarea = document.createElement('textarea');
textarea.className = 'edit-textarea user-edit-textarea';
textarea.value = originalContent;
textarea.focus();
setTimeout(() => adjustTextareaHeight(textarea), 0);
textarea.addEventListener('input', () => adjustTextareaHeight(textarea));
const editActions = document.createElement('div');
editActions.className = 'edit-actions';
const saveBtn = document.createElement('button');
saveBtn.className = 'save-edit-btn';
saveBtn.textContent = 'تأیید';
saveBtn.addEventListener('click', () => {
chat.messages[messageIndex].content = Array.isArray(chat.messages[messageIndex].content)
? chat.messages[messageIndex].content.map(item => item.type === 'text' ? { ...item, text: textarea.value } : item)
: textarea.value;
chat.messages[messageIndex].timestamp = new Date().toISOString();
saveChats();
renderMessages(chat.messages);
showNotification('پیام کاربر ویرایش شد');
});
const cancelBtn = document.createElement('button');
cancelBtn.className = 'cancel-edit-btn';
cancelBtn.textContent = 'لغو';
cancelBtn.addEventListener('click', () => {
renderMessages(chat.messages);
});
editActions.appendChild(saveBtn);
editActions.appendChild(cancelBtn);
messageDiv.innerHTML = '';
messageDiv.appendChild(textarea);
messageDiv.appendChild(editActions);
}
function editAIMessage(messageIndex) {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (!chat || !chat.messages[messageIndex] || chat.messages[messageIndex].role !== 'assistant') return;
const messageContainer = elements.chatDisplay.querySelector(`.message-container[data-message-index="${messageIndex}"]`);
const messageDiv = messageContainer.querySelector('.message-box');
const originalContent = chat.messages[messageIndex].content;
const textarea = document.createElement('textarea');
textarea.className = 'edit-textarea';
textarea.value = originalContent;
textarea.focus();
setTimeout(() => adjustTextareaHeight(textarea), 0);
textarea.addEventListener('input', () => adjustTextareaHeight(textarea));
const editActions = document.createElement('div');
editActions.className = 'edit-actions';
const saveBtn = document.createElement('button');
saveBtn.className = 'save-edit-btn';
saveBtn.textContent = 'تأیید';
saveBtn.addEventListener('click', () => {
chat.messages[messageIndex].content = textarea.value;
chat.messages[messageIndex].timestamp = new Date().toISOString();
saveChats();
renderMessages(chat.messages);
showNotification('پاسخ هوش مصنوعی ویرایش شد');
});
const cancelBtn = document.createElement('button');
cancelBtn.className = 'cancel-edit-btn';
cancelBtn.textContent = 'لغو';
cancelBtn.addEventListener('click', () => {
renderMessages(chat.messages);
});
editActions.appendChild(saveBtn);
editActions.appendChild(cancelBtn);
messageDiv.innerHTML = '';
messageDiv.appendChild(textarea);
messageDiv.appendChild(editActions);
}
function regenerateAIResponse(messageIndex) {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (!chat || !chat.messages[messageIndex - 1] || chat.messages[messageIndex].role !== 'assistant') return;
const userMessage = chat.messages[messageIndex - 1].content;
chat.messages.splice(messageIndex, 1);
saveChats();
renderMessages(chat.messages);
getAIResponse(userMessage);
}
function addUserMessage(content, save = true, messageIndex = null) {
const messageContainer = document.createElement('div');
messageContainer.className = 'message-container';
messageContainer.dataset.messageIndex = messageIndex !== null ? messageIndex : state.chats.find(c => c.id === state.currentChatId).messages.length;
const messageDiv = document.createElement('div');
messageDiv.className = 'message-box user-message';
let htmlContent = '';
if (Array.isArray(content)) {
content.forEach(item => {
if (item.type === 'text') {
const isPersian = isPersianText(item.text);
htmlContent += `<div class="${isPersian ? 'rtl-text' : 'ltr-text'} latex-expression">${sanitizeHTML(marked.parse(item.text))}</div>`;
} else if (item.type === 'image_url') {
htmlContent += `<div class="mt-2"><img src="${sanitizeHTML(item.image_url.url)}" class="max-w-full h-auto rounded-lg" alt="تصویر ارسالی"></div>`;
}
});
} else {
const isPersian = isPersianText(content);
htmlContent = `<div class="${isPersian ? 'rtl-text' : 'ltr-text'} latex-expression">${sanitizeHTML(marked.parse(content))}</div>`;
}
messageDiv.innerHTML = htmlContent;
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'message-action-btn copy-message-btn';
copyBtn.innerHTML = '<i class="fas fa-copy text-white"></i>';
copyBtn.setAttribute('aria-label', 'کپی پیام');
copyBtn.addEventListener('click', () => {
const chat = state.chats.find(c => c.id === state.currentChatId);
const message = chat.messages[messageContainer.dataset.messageIndex].content;
const textToCopy = Array.isArray(message) ?
message.find(item => item.type === 'text')?.text || '' :
message;
navigator.clipboard.writeText(textToCopy);
showNotification('پیام کپی شد');
});
actionsDiv.appendChild(copyBtn);
const editBtn = document.createElement('button');
editBtn.className = 'message-action-btn edit-message-btn';
editBtn.innerHTML = '<i class="fas fa-edit text-white"></i>';
editBtn.setAttribute('aria-label', 'ویرایش پیام');
editBtn.addEventListener('click', () => editUserMessage(messageContainer.dataset.messageIndex));
actionsDiv.appendChild(editBtn);
messageDiv.appendChild(actionsDiv);
messageContainer.appendChild(messageDiv);
elements.chatDisplay.appendChild(messageContainer);
MathJax.typesetPromise().then(() => {
hljs.highlightAll();
scrollToBottom();
}).catch(err => console.error('MathJax Error:', err));
if (save && state.currentChatId) {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (chat) {
chat.messages.push({
role: 'user',
content,
timestamp: new Date().toISOString()
});
if (chat.messages.length === 1) {
const firstMessageText = Array.isArray(content) ? content.find(item => item.type === 'text')?.text || 'گفتگوی جدید' : content;
chat.title = firstMessageText.length > 30 ? firstMessageText.substring(0, 30) + '...' : firstMessageText;
elements.currentChatTitle.textContent = chat.title;
renderChatList();
}
saveChats();
}
}
}
function extractCodeBlocks(content) {
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
const codeBlocks = [];
let lastIndex = 0;
let match;
while ((match = codeBlockRegex.exec(content)) !== null) {
const language = match[1] || 'text';
const code = match[2].trim();
const startIndex = match.index;
if (startIndex > lastIndex) {
codeBlocks.push({ type: 'text', content: content.slice(lastIndex, startIndex) });
}
codeBlocks.push({ type: 'code', language, content: code });
lastIndex = codeBlockRegex.lastIndex;
}
if (lastIndex < content.length) {
codeBlocks.push({ type: 'text', content: content.slice(lastIndex) });
}
return codeBlocks.length === 0 ? [{ type: 'text', content }] : codeBlocks;
}
function initAIMessageStreaming() {
const messageContainer = document.createElement('div');
messageContainer.className = 'message-container';
messageContainer.dataset.messageIndex = state.chats.find(c => c.id === state.currentChatId).messages.length;
const messageDiv = document.createElement('div');
messageDiv.className = 'message-box ai-message';
messageDiv.id = 'streamingMessage';
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'message-action-btn copy-message-btn';
copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
copyBtn.setAttribute('aria-label', 'کپی پیام');
copyBtn.addEventListener('click', () => {
const chat = state.chats.find(c => c.id === state.currentChatId);
const message = chat.messages[messageContainer.dataset.messageIndex].content;
navigator.clipboard.writeText(message);
showNotification('پیام کپی شد');
});
actionsDiv.appendChild(copyBtn);
const editBtn = document.createElement('button');
editBtn.className = 'message-action-btn edit-message-btn';
editBtn.innerHTML = '<i class="fas fa-edit"></i>';
editBtn.setAttribute('aria-label', 'ویرایش پیام');
editBtn.addEventListener('click', () => editAIMessage(messageContainer.dataset.messageIndex));
actionsDiv.appendChild(editBtn);
const regenerateBtn = document.createElement('button');
regenerateBtn.className = 'message-action-btn regenerate-message-btn';
regenerateBtn.innerHTML = '<i class="fas fa-redo text-white"></i>';
regenerateBtn.setAttribute('aria-label', 'بازتولید پاسخ');
regenerateBtn.addEventListener('click', () => regenerateAIResponse(messageContainer.dataset.messageIndex));
actionsDiv.appendChild(regenerateBtn);
messageDiv.appendChild(actionsDiv);
messageContainer.appendChild(messageDiv);
elements.chatDisplay.appendChild(messageContainer);
state.currentAIMessageDiv = messageDiv;
scrollToBottom();
return messageDiv;
}
function finalizeAIMessage(content) {
if (!state.currentAIMessageDiv) return;
const messageContainer = state.currentAIMessageDiv.parentElement;
const blocks = extractCodeBlocks(content);
state.currentAIMessageDiv.innerHTML = '';
blocks.forEach(block => {
const blockDiv = document.createElement('div');
if (block.type === 'text') {
const isPersian = isPersianText(block.content);
blockDiv.className = isPersian ? 'rtl-text' : 'ltr-text';
let processedContent = block.content;
const latexRegex = /(\$\$[\s\S]+?\$\$|\$[\s\S]+?\$|\\\([\s\S]+?\\\)|\\\[[\s\S]+?\\\])/g;
let lastIndex = 0;
let htmlContent = '';
let match;
while ((match = latexRegex.exec(block.content)) !== null) {
const latex = match[0];
const startIndex = match.index;
if (startIndex > lastIndex) {
const textBefore = block.content.slice(lastIndex, startIndex);
htmlContent += sanitizeHTML(marked.parse(textBefore));
}
htmlContent += `<span class="latex-expression">${latex}</span>`;
lastIndex = latexRegex.lastIndex;
}
if (lastIndex < block.content.length) {
const textAfter = block.content.slice(lastIndex);
htmlContent += sanitizeHTML(marked.parse(textAfter));
}
blockDiv.innerHTML = htmlContent;
} else if (block.type === 'code') {
const codeBlockDiv = document.createElement('div');
codeBlockDiv.className = 'code-block';
const codeHeader = document.createElement('div');
codeHeader.className = 'code-header';
const languageSpan = document.createElement('span');
languageSpan.textContent = block.language || 'Code';
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
copyBtn.setAttribute('aria-label', 'کپی کد');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(block.content).then(() => {
copyBtn.classList.add('copied');
copyBtn.innerHTML = '<i class="fas fa-check"></i> Copied';
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
}, 2000);
});
});
codeHeader.appendChild(languageSpan);
codeHeader.appendChild(copyBtn);
const codeContent = document.createElement('div');
codeContent.className = 'code-content';
codeContent.innerHTML = `<pre><code class="language-${block.language}">${sanitizeHTML(block.content)}</code></pre>`;
codeBlockDiv.appendChild(codeHeader);
codeBlockDiv.appendChild(codeContent);
blockDiv.appendChild(codeBlockDiv);
}
state.currentAIMessageDiv.appendChild(blockDiv);
});
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'message-action-btn copy-message-btn';
copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
copyBtn.setAttribute('aria-label', 'کپی پیام');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(content);
showNotification('پیام کپی شد');
});
actionsDiv.appendChild(copyBtn);
const editBtn = document.createElement('button');
editBtn.className = 'message-action-btn edit-message-btn';
editBtn.innerHTML = '<i class="fas fa-edit"></i>';
editBtn.setAttribute('aria-label', 'ویرایش پیام');
editBtn.addEventListener('click', () => editAIMessage(messageContainer.dataset.messageIndex));
actionsDiv.appendChild(editBtn);
const regenerateBtn = document.createElement('button');
regenerateBtn.className = 'message-action-btn regenerate-message-btn';
regenerateBtn.innerHTML = '<i class="fas fa-redo"></i>';
regenerateBtn.setAttribute('aria-label', 'بازتولید پاسخ');
regenerateBtn.addEventListener('click', () => regenerateAIResponse(messageContainer.dataset.messageIndex));
actionsDiv.appendChild(regenerateBtn);
state.currentAIMessageDiv.appendChild(actionsDiv);
MathJax.typesetPromise().then(() => {
hljs.highlightAll();
scrollToBottom();
}).catch(err => console.error('MathJax Error:', err));
if (state.currentChatId) {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (chat) {
chat.messages.push({
role: 'assistant',
content,
timestamp: new Date().toISOString()
});
saveChats();
}
}
state.currentAIMessageDiv = null;
}
function showTypingIndicator() {
const messageContainer = document.createElement('div');
messageContainer.className = 'message-container';
const typingDiv = document.createElement('div');
typingDiv.className = 'message-box ai-message';
typingDiv.id = 'typingIndicator';
typingDiv.innerHTML = `
<div class="typing-indicator" aria-label="در حال تایپ">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>
`;
messageContainer.appendChild(typingDiv);
elements.chatDisplay.appendChild(messageContainer);
scrollToBottom();
}
function hideTypingIndicator() {
const typingIndicator = document.getElementById('typingIndicator');
if (typingIndicator) typingIndicator.parentElement.remove();
}
function scrollToBottom() {
elements.chatDisplay.scrollTo({
top: elements.chatDisplay.scrollHeight,
behavior: 'smooth'
});
}
function updateSendButtonState(isProcessing) {
elements.sendButton.classList.toggle('stop', isProcessing);
elements.sendButton.classList.toggle('loading', isProcessing);
elements.sendButton.innerHTML = isProcessing ? '<i class="fas fa-stop"></i>' : '<i class="fas fa-paper-plane"></i>';
elements.sendButton.setAttribute('aria-label', isProcessing ? 'توقف درخواست' : 'ارسال پیام');
}
function stopAIResponse() {
if (state.abortController) {
state.abortController.abort();
state.abortController = null;
}
hideTypingIndicator();
if (state.currentAIMessageDiv && state.currentAIResponse) {
finalizeAIMessage(state.currentAIResponse);
}
state.isAIResponding = false;
updateSendButtonState(false);
showNotification('درخواست متوقف شد');
}
function handleSendButtonClick() {
state.isAIResponding ? stopAIResponse() : sendMessage();
}
async function sendMessage() {
const message = elements.messageInput.value.trim();
const file = elements.fileInput.files[0];
if (!message && !file) return;
elements.messageInput.blur();
elements.sendButton.classList.add('loading');
if (file && !MODEL_CONFIGS[state.currentModel]["supports_images"]) {
showNotification("این مدل از تصاویر پشتیبانی نمی‌کند");
elements.fileInput.value = '';
elements.sendButton.classList.remove('loading');
return;
}
try {
let content;
if (file) {
const reader = new FileReader();
const fileLoadPromise = new Promise((resolve, reject) => {
reader.onload = () => resolve(reader.result);
reader.onerror = () => {
reject(new Error('خطا در بارگذاری فایل'));
showNotification('خطا در بارگذاری فایل');
};
});
reader.readAsDataURL(file);
const base64Image = await fileLoadPromise;
// Remove the data:image/... prefix if it exists
const base64Content = base64Image.split(',')[1] || base64Image;
// Check file size (max ~20MB for Hugging Face)
const fileSizeInMB = (base64Content.length * 3/4) / (1024*1024);
if (fileSizeInMB > 20) {
throw new Error('اندازه تصویر باید کمتر از 20 مگابایت باشد');
}
content = [
{ type: 'text', text: message || 'این تصویر را توصیف کنید' },
{
type: 'image_url',
image_url: {
url: `data:image/jpeg;base64,${base64Content}`,
detail: 'auto' // Can be 'low', 'high', or 'auto'
}
}
];
} else {
content = message;
}
addUserMessage(content);
elements.messageInput.value = '';
elements.fileInput.value = '';
await getAIResponse(content);
} catch (error) {
console.error('Error sending message:', error);
showNotification(error.message);
} finally {
elements.sendButton.classList.remove('loading');
adjustInputHeight();
}
}
async function getAIResponse(content) {
state.isAIResponding = true;
updateSendButtonState(true);
showTypingIndicator();
try {
state.abortController = new AbortController();
const modelConfig = MODEL_CONFIGS[state.currentModel];
const messages = state.chats.find(c => c.id === state.currentChatId).messages.map(msg => ({
role: msg.role,
content: msg.content
}));
messages.push({ role: 'user', content });
const messagesForAPI = messages.map(message => {
if (Array.isArray(message.content)) {
// Handle multimodal messages
return {
role: message.role,
content: message.content.map(contentItem => {
if (contentItem.type === 'image_url') {
// Convert data URLs to base64 if needed
if (contentItem.image_url.url.startsWith('data:')) {
return {
type: 'image_url',
image_url: contentItem.image_url
};
} else {
// For external URLs, keep as is
return {
type: 'image_url',
image_url: {
url: contentItem.image_url.url,
detail: 'auto'
}
};
}
}
return contentItem;
})
};
}
// Regular text messages
return message;
});
const requestBody = {
model: state.currentModel,
messages: messagesForAPI,
stream: true
};
// For debugging
console.log('Sending request:', JSON.stringify(requestBody, null, 2));
const response = await fetch(modelConfig["url"], {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer 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',
'Accept': 'text/event-stream'
},
body: JSON.stringify(requestBody),
signal: state.abortController.signal
});
if (!response.ok) {
throw new Error(`خطا در ارتباط با سرور: کد ${response.status}`);
}
hideTypingIndicator();
const messageDiv = initAIMessageStreaming();
state.currentAIResponse = '';
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') break;
try {
const json = JSON.parse(data);
const delta = json.choices[0].delta.content;
if (delta) {
state.currentAIResponse += delta;
const isPersian = isPersianText(state.currentAIResponse);
messageDiv.innerHTML = `<div class="${isPersian ? 'rtl-text' : 'ltr-text'}">${sanitizeHTML(marked.parse(state.currentAIResponse))}</div>`;
MathJax.typesetPromise().catch(err => console.error('MathJax Error:', err));
// Remove scrollToBottom() from here to prevent scroll lock
}
} catch (e) {
console.error('Error parsing stream chunk:', e);
}
}
}
}
finalizeAIMessage(state.currentAIResponse);
MathJax.typesetPromise().then(() => {
hljs.highlightAll();
scrollToBottom();
}).catch(err => console.error('MathJax Error:', err));
if (state.currentChatId) {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (!chat.model) {
chat.model = state.currentModel;
saveChats();
renderChatList();
}
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request aborted');
} else {
console.error('Error:', error);
hideTypingIndicator();
addAIMessage(`خطا: ${error.message}`, true);
}
} finally {
state.isAIResponding = false;
updateSendButtonState(false);
state.abortController = null;
state.currentAIResponse = '';
}
}
function showNotification(message, duration = 3000) {
elements.notificationText.textContent = message;
elements.notification.classList.add('show');
setTimeout(() => elements.notification.classList.remove('show'), duration);
}
function openSidebar() {
elements.mobileSidebar.classList.add('show');
elements.sidebarOverlay.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeSidebar() {
elements.mobileSidebar.classList.remove('show');
elements.sidebarOverlay.classList.remove('show');
document.body.style.overflow = '';
}
function openModelSelector() {
elements.mobileModelSelector.classList.add('show');
document.body.style.overflow = 'hidden';
}
function closeModelSelector() {
elements.mobileModelSelector.classList.remove('show');
document.body.style.overflow = '';
}
function getModelDisplayName(model) {
const modelNames = {
"MiniMaxAI/MiniMax-M2:novita": "MiniMax-M2",
"deepseek-ai/DeepSeek-V3.2-Exp:novita": "DeepSeek",
"zai-org/GLM-4.6:novita": "DeepSeek Turbo",
"openai/gpt-oss-120b:novita": "gpt-oss-120b",
"meta-llama/llama-4-scout-17b-16e-instruct": "Llama 4 Scout",
"Llama-4-Maverick-17B-128E-Instruct": "Llama 4 Maverick"
};
return modelNames[model] || model;
}
function addAIMessage(content, save = true, messageIndex = null) {
const messageContainer = document.createElement('div');
messageContainer.className = 'message-container';
messageContainer.dataset.messageIndex = messageIndex !== null ? messageIndex : state.chats.find(c => c.id === state.currentChatId).messages.length;
const messageDiv = document.createElement('div');
messageDiv.className = 'message-box ai-message';
const blocks = extractCodeBlocks(content);
blocks.forEach(block => {
const blockDiv = document.createElement('div');
if (block.type === 'text') {
const isPersian = isPersianText(block.content);
blockDiv.className = isPersian ? 'rtl-text' : 'ltr-text';
blockDiv.innerHTML = sanitizeHTML(marked.parse(block.content));
} else if (block.type === 'code') {
const codeBlockDiv = document.createElement('div');
codeBlockDiv.className = 'code-block';
const codeHeader = document.createElement('div');
codeHeader.className = 'code-header';
const languageSpan = document.createElement('span');
languageSpan.textContent = block.language || 'Code';
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn';
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
copyBtn.setAttribute('aria-label', 'کپی کد');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(block.content).then(() => {
copyBtn.classList.add('copied');
copyBtn.innerHTML = '<i class="fas fa-check"></i> Copied';
setTimeout(() => {
copyBtn.classList.remove('copied');
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
}, 2000);
});
});
codeHeader.appendChild(languageSpan);
codeHeader.appendChild(copyBtn);
const codeContent = document.createElement('div');
codeContent.className = 'code-content';
codeContent.innerHTML = `<pre><code class="language-${block.language}">${sanitizeHTML(block.content)}</code></pre>`;
codeBlockDiv.appendChild(codeHeader);
codeBlockDiv.appendChild(codeContent);
blockDiv.appendChild(codeBlockDiv);
}
messageDiv.appendChild(blockDiv);
});
const actionsDiv = document.createElement('div');
actionsDiv.className = 'message-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'message-action-btn copy-message-btn';
copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
copyBtn.setAttribute('aria-label', 'کپی پیام');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(content);
showNotification('پیام کپی شد');
});
actionsDiv.appendChild(copyBtn);
const editBtn = document.createElement('button');
editBtn.className = 'message-action-btn edit-message-btn';
editBtn.innerHTML = '<i class="fas fa-edit"></i>';
editBtn.setAttribute('aria-label', 'ویرایش پیام');
editBtn.addEventListener('click', () => editAIMessage(messageContainer.dataset.messageIndex));
actionsDiv.appendChild(editBtn);
const regenerateBtn = document.createElement('button');
regenerateBtn.className = 'message-action-btn regenerate-message-btn';
regenerateBtn.innerHTML = '<i class="fas fa-redo"></i>';
regenerateBtn.setAttribute('aria-label', 'بازتولید پاسخ');
regenerateBtn.addEventListener('click', () => regenerateAIResponse(messageContainer.dataset.messageIndex));
actionsDiv.appendChild(regenerateBtn);
messageDiv.appendChild(actionsDiv);
messageContainer.appendChild(messageDiv);
elements.chatDisplay.appendChild(messageContainer);
MathJax.typesetPromise().then(() => {
hljs.highlightAll();
scrollToBottom();
}).catch(err => console.error('MathJax Error:', err));
if (save && state.currentChatId) {
const chat = state.chats.find(c => c.id === state.currentChatId);
if (chat) {
chat.messages.push({
role: 'assistant',
content,
timestamp: new Date().toISOString()
});
saveChats();
}
}
}
init();
});
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'93cf54597dfabfbb',t:'MTc0Njc3NDEyNy4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
</body>
</html>