Spaces:
Running
Running
| <html lang="vi"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Chat - Transformers.js</title> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #6366f1; | |
| --primary-dark: #4f46e5; | |
| --bg-dark: #0f172a; | |
| --bg-card: #1e293b; | |
| --bg-input: #334155; | |
| --text-primary: #f1f5f9; | |
| --text-secondary: #94a3b8; | |
| --border: #334155; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| background: linear-gradient(135deg, var(--bg-dark) 0%, #1a1f3a 100%); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| /* Header */ | |
| #header { | |
| background: var(--bg-card); | |
| padding: 15px 20px; | |
| border-bottom: 2px solid var(--border); | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | |
| } | |
| .header-content { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| gap: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 1.3rem; | |
| font-weight: 700; | |
| color: var(--primary); | |
| } | |
| .logo i { | |
| font-size: 1.8rem; | |
| } | |
| .model-selector { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| flex: 1; | |
| max-width: 400px; | |
| } | |
| .model-selector select { | |
| background: var(--bg-input) ; | |
| color: var(--text-primary) ; | |
| border: 1px solid var(--border) ; | |
| padding: 8px 12px; | |
| border-radius: 8px; | |
| font-size: 0.9rem; | |
| } | |
| .model-selector select:focus { | |
| border-color: var(--primary) ; | |
| box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1) ; | |
| } | |
| .status-badge { | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| } | |
| .status-loading { | |
| background: #fbbf24; | |
| color: #78350f; | |
| } | |
| .status-ready { | |
| background: #10b981; | |
| color: #064e3b; | |
| } | |
| .status-error { | |
| background: #ef4444; | |
| color: #7f1d1d; | |
| } | |
| /* Chat Container */ | |
| #chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 30px 20px; | |
| max-width: 1000px; | |
| width: 100%; | |
| margin: 0 auto; | |
| } | |
| #chat-container::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #chat-container::-webkit-scrollbar-track { | |
| background: var(--bg-dark); | |
| } | |
| #chat-container::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 4px; | |
| } | |
| .message { | |
| margin-bottom: 20px; | |
| padding: 16px 20px; | |
| border-radius: 16px; | |
| max-width: 80%; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| animation: slideIn 0.3s ease-out; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .user { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); | |
| margin-left: auto; | |
| border-bottom-right-radius: 4px; | |
| } | |
| .ai { | |
| background: var(--bg-card); | |
| margin-right: auto; | |
| border: 1px solid var(--border); | |
| border-bottom-left-radius: 4px; | |
| } | |
| .message-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| font-size: 0.9rem; | |
| opacity: 0.9; | |
| } | |
| .typing-indicator { | |
| display: inline-flex; | |
| gap: 4px; | |
| align-items: center; | |
| } | |
| .typing-indicator span { | |
| width: 6px; | |
| height: 6px; | |
| background: var(--text-secondary); | |
| border-radius: 50%; | |
| animation: typing 1.4s infinite; | |
| } | |
| .typing-indicator span:nth-child(2) { animation-delay: 0.2s; } | |
| .typing-indicator span:nth-child(3) { animation-delay: 0.4s; } | |
| @keyframes typing { | |
| 0%, 60%, 100% { transform: translateY(0); opacity: 0.5; } | |
| 30% { transform: translateY(-8px); opacity: 1; } | |
| } | |
| /* Input Area */ | |
| #input-area { | |
| background: var(--bg-card); | |
| padding: 20px; | |
| border-top: 2px solid var(--border); | |
| box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.3); | |
| } | |
| .input-container { | |
| max-width: 1000px; | |
| margin: 0 auto; | |
| } | |
| .input-group { | |
| position: relative; | |
| display: flex; | |
| gap: 12px; | |
| } | |
| #user-input { | |
| background: var(--bg-input) ; | |
| color: var(--text-primary) ; | |
| border: 2px solid var(--border) ; | |
| padding: 14px 20px ; | |
| border-radius: 12px ; | |
| font-size: 1rem; | |
| transition: all 0.3s ease; | |
| } | |
| #user-input:focus { | |
| border-color: var(--primary) ; | |
| box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1) ; | |
| outline: none ; | |
| } | |
| #send-btn { | |
| background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); | |
| border: none; | |
| padding: 14px 28px; | |
| border-radius: 12px; | |
| font-weight: 600; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); | |
| } | |
| #send-btn:hover:not(:disabled) { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4); | |
| } | |
| #send-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .model-info { | |
| margin-top: 12px; | |
| font-size: 0.85rem; | |
| color: var(--text-secondary); | |
| text-align: center; | |
| } | |
| /* Welcome Screen */ | |
| .welcome-screen { | |
| text-align: center; | |
| padding: 40px 20px; | |
| max-width: 600px; | |
| margin: auto; | |
| } | |
| .welcome-screen h2 { | |
| font-size: 2rem; | |
| margin-bottom: 16px; | |
| background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .welcome-screen p { | |
| color: var(--text-secondary); | |
| margin-bottom: 30px; | |
| line-height: 1.6; | |
| } | |
| .example-prompts { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 12px; | |
| margin-top: 20px; | |
| } | |
| .example-prompt { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| padding: 12px 16px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-size: 0.9rem; | |
| } | |
| .example-prompt:hover { | |
| background: var(--bg-input); | |
| border-color: var(--primary); | |
| transform: translateY(-2px); | |
| } | |
| @media (max-width: 768px) { | |
| .header-content { | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .model-selector { | |
| max-width: 100%; | |
| width: 100%; | |
| } | |
| .message { | |
| max-width: 90%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="header"> | |
| <div class="header-content"> | |
| <div class="logo"> | |
| <i class="bi bi-robot"></i> | |
| <span>AI Chat</span> | |
| </div> | |
| <div class="model-selector"> | |
| <label for="model-select" class="text-secondary" style="white-space: nowrap;">Model:</label> | |
| <select id="model-select" class="form-select"> | |
| <option value="onnx-community/Qwen2.5-0.5B-Instruct">Qwen2.5 0.5B (Nhanh - 300MB)</option> | |
| <option value="onnx-community/Qwen2.5-1.5B-Instruct">Qwen2.5 1.5B (Cân bằng - 800MB)</option> | |
| <option value="onnx-community/Phi-3.5-mini-instruct">Phi-3.5 Mini (Thông minh - 2.4GB)</option> | |
| <option value="onnx-community/Llama-3.2-1B-Instruct">Llama 3.2 1B (Meta - 650MB)</option> | |
| <option value="onnx-community/SmolLM2-360M-Instruct">SmolLM2 360M (Siêu nhẹ - 200MB)</option> | |
| </select> | |
| </div> | |
| <div id="status" class="status-badge status-loading"> | |
| <i class="bi bi-hourglass-split"></i> Đang khởi tạo... | |
| </div> | |
| </div> | |
| </div> | |
| <div id="chat-container" class="d-flex flex-column"> | |
| <div class="welcome-screen"> | |
| <h2><i class="bi bi-stars"></i> Chào mừng đến với AI Chat</h2> | |
| <p>Trải nghiệm AI chạy hoàn toàn trên trình duyệt của bạn. Không cần server, dữ liệu được bảo mật 100%.</p> | |
| <div class="example-prompts"> | |
| <div class="example-prompt" data-prompt="Giải thích về machine learning"> | |
| <i class="bi bi-book"></i> Machine Learning | |
| </div> | |
| <div class="example-prompt" data-prompt="Viết một bài thơ về mùa thu"> | |
| <i class="bi bi-pen"></i> Viết thơ | |
| </div> | |
| <div class="example-prompt" data-prompt="Giải bài toán: 15% của 240 là bao nhiêu?"> | |
| <i class="bi bi-calculator"></i> Giải toán | |
| </div> | |
| <div class="example-prompt" data-prompt="Cho tôi một công thức nấu ăn đơn giản"> | |
| <i class="bi bi-egg-fried"></i> Công thức nấu ăn | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="input-area"> | |
| <div class="input-container"> | |
| <div class="input-group"> | |
| <input type="text" id="user-input" class="form-control" placeholder="Nhập câu hỏi của bạn..." disabled> | |
| <button class="btn btn-primary" type="button" id="send-btn" disabled> | |
| <i class="bi bi-send-fill"></i> Gửi | |
| </button> | |
| </div> | |
| <div class="model-info" id="model-info">Chọn model và chờ tải xong để bắt đầu</div> | |
| </div> | |
| </div> | |
| <script type="module"> | |
| import { | |
| pipeline, | |
| env, | |
| TextStreamer | |
| } from 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.0'; | |
| env.allowLocalModels = false; | |
| env.useBrowserCache = true; | |
| const chatContainer = document.getElementById('chat-container'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const status = document.getElementById('status'); | |
| const modelSelect = document.getElementById('model-select'); | |
| const modelInfo = document.getElementById('model-info'); | |
| let generator = null; | |
| let isGenerating = false; | |
| const modelDescriptions = { | |
| 'onnx-community/Qwen2.5-0.5B-Instruct': 'Qwen2.5 0.5B - Mô hình nhanh, phù hợp cho chat đơn giản', | |
| 'onnx-community/Qwen2.5-1.5B-Instruct': 'Qwen2.5 1.5B - Cân bằng giữa tốc độ và chất lượng', | |
| 'onnx-community/Phi-3.5-mini-instruct': 'Phi-3.5 Mini - Microsoft, rất thông minh nhưng cần nhiều RAM', | |
| 'onnx-community/Llama-3.2-1B-Instruct': 'Llama 3.2 1B - Meta AI, hiệu suất tốt', | |
| 'onnx-community/SmolLM2-360M-Instruct': 'SmolLM2 360M - Siêu nhẹ, tải nhanh nhất' | |
| }; | |
| async function loadModel(modelName) { | |
| if (generator) { | |
| generator = null; | |
| } | |
| status.className = 'status-badge status-loading'; | |
| status.innerHTML = '<i class="bi bi-hourglass-split"></i> Đang tải model...'; | |
| userInput.disabled = true; | |
| sendBtn.disabled = true; | |
| modelSelect.disabled = true; | |
| try { | |
| generator = await pipeline('text-generation', modelName, { | |
| device: 'wasm', | |
| dtype: 'q4', | |
| progress_callback: (progress) => { | |
| if (progress.status === 'progress') { | |
| const percent = Math.round(progress.progress); | |
| status.innerHTML = `<i class="bi bi-download"></i> Đang tải: ${percent}%`; | |
| } | |
| } | |
| }); | |
| status.className = 'status-badge status-ready'; | |
| status.innerHTML = '<i class="bi bi-check-circle-fill"></i> Sẵn sàng'; | |
| userInput.disabled = false; | |
| sendBtn.disabled = false; | |
| modelSelect.disabled = false; | |
| modelInfo.textContent = modelDescriptions[modelName]; | |
| userInput.focus(); | |
| } catch (e) { | |
| status.className = 'status-badge status-error'; | |
| status.innerHTML = '<i class="bi bi-exclamation-triangle-fill"></i> Lỗi'; | |
| modelInfo.textContent = 'Lỗi: ' + e.message; | |
| console.error(e); | |
| modelSelect.disabled = false; | |
| } | |
| } | |
| async function chat(text) { | |
| if (!text || !generator || isGenerating) return; | |
| isGenerating = true; | |
| sendBtn.disabled = true; | |
| // Remove welcome screen | |
| const welcome = chatContainer.querySelector('.welcome-screen'); | |
| if (welcome) welcome.remove(); | |
| // User message | |
| const userDiv = document.createElement('div'); | |
| userDiv.className = 'message user'; | |
| userDiv.innerHTML = `<div class="message-header"><i class="bi bi-person-circle"></i> Bạn</div>${text}`; | |
| chatContainer.appendChild(userDiv); | |
| userInput.value = ''; | |
| // AI message | |
| const aiDiv = document.createElement('div'); | |
| aiDiv.className = 'message ai'; | |
| aiDiv.innerHTML = '<div class="message-header"><i class="bi bi-robot"></i> AI <span class="typing-indicator"><span></span><span></span><span></span></span></div>'; | |
| const contentDiv = document.createElement('div'); | |
| aiDiv.appendChild(contentDiv); | |
| chatContainer.appendChild(aiDiv); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| const streamer = new TextStreamer(generator.tokenizer, { | |
| skip_prompt: true, | |
| callback_function: (token) => { | |
| // Remove typing indicator on first token | |
| const typingIndicator = aiDiv.querySelector('.typing-indicator'); | |
| if (typingIndicator) typingIndicator.remove(); | |
| contentDiv.textContent += token; | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| }, | |
| }); | |
| const messages = [{ role: "user", content: text }]; | |
| try { | |
| await generator(messages, { | |
| max_new_tokens: 512, | |
| streamer: streamer, | |
| temperature: 0.7, | |
| top_p: 0.95, | |
| }); | |
| } catch (err) { | |
| contentDiv.textContent = '❌ Lỗi khi tạo phản hồi: ' + err.message; | |
| } | |
| isGenerating = false; | |
| sendBtn.disabled = false; | |
| userInput.focus(); | |
| } | |
| // Event listeners | |
| sendBtn.addEventListener('click', () => chat(userInput.value.trim())); | |
| userInput.addEventListener('keypress', (e) => { | |
| if(e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| chat(userInput.value.trim()); | |
| } | |
| }); | |
| modelSelect.addEventListener('change', () => { | |
| loadModel(modelSelect.value); | |
| }); | |
| // Example prompts | |
| document.addEventListener('click', (e) => { | |
| if (e.target.closest('.example-prompt')) { | |
| const prompt = e.target.closest('.example-prompt').dataset.prompt; | |
| userInput.value = prompt; | |
| chat(prompt); | |
| } | |
| }); | |
| // Auto-load default model | |
| loadModel(modelSelect.value); | |
| </script> | |
| </body> | |
| </html> |