Spaces:
Running
Running
| <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 ; | |
| } | |
| </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> |