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 { | |
| --sidebar-width: 260px; | |
| --primary: #10a37f; | |
| --primary-hover: #0d8a6a; | |
| --bg-primary: #fff; | |
| --bg-secondary: #f7f7f8; | |
| --bg-dark: #202123; | |
| --bg-darker: #171717; | |
| --text-primary: #353740; | |
| --text-secondary: #6e6e80; | |
| --border: #e5e5e5; | |
| --sidebar-bg: #171717; | |
| --sidebar-hover: #2a2b32; | |
| } | |
| [data-theme="dark"] { | |
| --bg-primary: #343541; | |
| --bg-secondary: #444654; | |
| --text-primary: #ececf1; | |
| --text-secondary: #c5c5d2; | |
| --border: #565869; | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| } | |
| /* Layout */ | |
| .app-container { | |
| display: flex; | |
| height: 100vh; | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| width: var(--sidebar-width); | |
| background: var(--sidebar-bg); | |
| display: flex; | |
| flex-direction: column; | |
| border-right: 1px solid rgba(255,255,255,0.1); | |
| transition: transform 0.3s ease; | |
| } | |
| .sidebar.collapsed { | |
| transform: translateX(-100%); | |
| } | |
| .sidebar-header { | |
| padding: 12px; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .new-chat-btn { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: transparent; | |
| border: 1px solid rgba(255,255,255,0.2); | |
| color: #fff; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| font-size: 14px; | |
| } | |
| .new-chat-btn:hover { | |
| background: rgba(255,255,255,0.1); | |
| } | |
| .chat-history { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 8px; | |
| } | |
| .chat-history::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .chat-history::-webkit-scrollbar-thumb { | |
| background: rgba(255,255,255,0.2); | |
| border-radius: 2px; | |
| } | |
| .history-group { | |
| margin-bottom: 20px; | |
| } | |
| .history-group-title { | |
| color: rgba(255,255,255,0.5); | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| padding: 8px 12px; | |
| letter-spacing: 0.5px; | |
| } | |
| .history-item { | |
| padding: 10px 12px; | |
| margin: 2px 0; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| color: rgba(255,255,255,0.8); | |
| font-size: 14px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| gap: 8px; | |
| position: relative; | |
| } | |
| .history-item:hover { | |
| background: var(--sidebar-hover); | |
| } | |
| .history-item.active { | |
| background: var(--sidebar-hover); | |
| color: #fff; | |
| } | |
| .history-item-text { | |
| flex: 1; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .history-item-actions { | |
| display: none; | |
| gap: 4px; | |
| } | |
| .history-item:hover .history-item-actions { | |
| display: flex; | |
| } | |
| .history-action-btn { | |
| padding: 4px 6px; | |
| background: transparent; | |
| border: none; | |
| color: rgba(255,255,255,0.6); | |
| cursor: pointer; | |
| border-radius: 4px; | |
| font-size: 14px; | |
| } | |
| .history-action-btn:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: #fff; | |
| } | |
| .sidebar-footer { | |
| padding: 12px; | |
| border-top: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .sidebar-footer-btn { | |
| width: 100%; | |
| padding: 10px 12px; | |
| background: transparent; | |
| border: none; | |
| color: rgba(255,255,255,0.8); | |
| text-align: left; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 14px; | |
| margin-bottom: 4px; | |
| } | |
| .sidebar-footer-btn:hover { | |
| background: var(--sidebar-hover); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| } | |
| .top-bar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 12px 16px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg-primary); | |
| } | |
| .top-bar-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .toggle-sidebar-btn { | |
| padding: 8px 12px; | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| border-radius: 6px; | |
| color: var(--text-primary); | |
| font-size: 18px; | |
| } | |
| .toggle-sidebar-btn:hover { | |
| background: var(--bg-secondary); | |
| } | |
| .model-selector-compact { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 12px; | |
| background: var(--bg-secondary); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 500; | |
| } | |
| .model-selector-compact:hover { | |
| background: var(--border); | |
| } | |
| .top-bar-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .top-bar-btn { | |
| padding: 8px 12px; | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| border-radius: 6px; | |
| color: var(--text-primary); | |
| font-size: 16px; | |
| } | |
| .top-bar-btn:hover { | |
| background: var(--bg-secondary); | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 6px 12px; | |
| border-radius: 16px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| } | |
| .status-indicator.loading { | |
| background: #fef3c7; | |
| color: #92400e; | |
| } | |
| .status-indicator.ready { | |
| background: #d1fae5; | |
| color: #065f46; | |
| } | |
| .status-indicator.error { | |
| background: #fee2e2; | |
| color: #991b1b; | |
| } | |
| .status-dot { | |
| width: 6px; | |
| height: 6px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| } | |
| /* Chat Area */ | |
| #chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| background: var(--bg-primary); | |
| } | |
| #chat-container::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #chat-container::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| #chat-container::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 4px; | |
| } | |
| .messages-wrapper { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| } | |
| .message { | |
| margin-bottom: 24px; | |
| animation: slideIn 0.3s ease-out; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(10px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .message-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 8px; | |
| } | |
| .message-avatar { | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 4px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| } | |
| .user-avatar { | |
| background: var(--primary); | |
| color: white; | |
| } | |
| .ai-avatar { | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| } | |
| .message-author { | |
| font-weight: 600; | |
| font-size: 14px; | |
| } | |
| .message-content { | |
| margin-left: 44px; | |
| line-height: 1.7; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| .typing-indicator { | |
| display: inline-flex; | |
| gap: 4px; | |
| align-items: center; | |
| margin-left: 44px; | |
| } | |
| .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.4; } | |
| 30% { transform: translateY(-6px); opacity: 1; } | |
| } | |
| .message-actions { | |
| margin-left: 44px; | |
| margin-top: 8px; | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .message-action-btn { | |
| padding: 4px 8px; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| border-radius: 4px; | |
| font-size: 13px; | |
| display: flex; | |
| align-items: center; | |
| gap: 4px; | |
| } | |
| .message-action-btn:hover { | |
| background: var(--bg-secondary); | |
| color: var(--text-primary); | |
| } | |
| /* Welcome Screen */ | |
| .welcome-screen { | |
| text-align: center; | |
| padding: 60px 20px; | |
| max-width: 700px; | |
| margin: auto; | |
| } | |
| .welcome-logo { | |
| font-size: 48px; | |
| margin-bottom: 16px; | |
| } | |
| .welcome-screen h1 { | |
| font-size: 32px; | |
| margin-bottom: 12px; | |
| font-weight: 600; | |
| } | |
| .welcome-screen p { | |
| color: var(--text-secondary); | |
| margin-bottom: 40px; | |
| font-size: 16px; | |
| } | |
| .example-prompts { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); | |
| gap: 12px; | |
| margin-top: 30px; | |
| } | |
| .example-prompt { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| padding: 16px; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| text-align: left; | |
| } | |
| .example-prompt:hover { | |
| background: var(--border); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| } | |
| .example-prompt-title { | |
| font-weight: 600; | |
| margin-bottom: 6px; | |
| font-size: 14px; | |
| } | |
| .example-prompt-text { | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| } | |
| /* Input Area */ | |
| #input-area { | |
| padding: 20px; | |
| background: var(--bg-primary); | |
| border-top: 1px solid var(--border); | |
| } | |
| .input-wrapper { | |
| max-width: 800px; | |
| margin: 0 auto; | |
| } | |
| .input-container { | |
| position: relative; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| padding: 12px 16px; | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 12px; | |
| transition: all 0.2s; | |
| } | |
| .input-container:focus-within { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 3px rgba(16, 163, 127, 0.1); | |
| } | |
| #user-input { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| resize: none; | |
| font-size: 15px; | |
| line-height: 1.5; | |
| max-height: 200px; | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| } | |
| #user-input::placeholder { | |
| color: var(--text-secondary); | |
| } | |
| #send-btn { | |
| padding: 8px; | |
| background: var(--primary); | |
| border: none; | |
| border-radius: 8px; | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 36px; | |
| height: 36px; | |
| flex-shrink: 0; | |
| } | |
| #send-btn:hover:not(:disabled) { | |
| background: var(--primary-hover); | |
| } | |
| #send-btn:disabled { | |
| opacity: 0.4; | |
| cursor: not-allowed; | |
| } | |
| .input-footer { | |
| margin-top: 12px; | |
| text-align: center; | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| } | |
| /* Modal */ | |
| .modal-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .modal-overlay.show { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: var(--bg-primary); | |
| border-radius: 12px; | |
| padding: 24px; | |
| max-width: 500px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .modal-title { | |
| font-size: 20px; | |
| font-weight: 600; | |
| } | |
| .modal-close { | |
| background: transparent; | |
| border: none; | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| padding: 4px; | |
| } | |
| .model-option { | |
| padding: 16px; | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| margin-bottom: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .model-option:hover { | |
| background: var(--bg-secondary); | |
| border-color: var(--primary); | |
| } | |
| .model-option.selected { | |
| border-color: var(--primary); | |
| background: rgba(16, 163, 127, 0.05); | |
| } | |
| .model-option-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| } | |
| .model-option-name { | |
| font-weight: 600; | |
| font-size: 15px; | |
| } | |
| .model-option-size { | |
| font-size: 12px; | |
| color: var(--text-secondary); | |
| background: var(--bg-secondary); | |
| padding: 3px 8px; | |
| border-radius: 4px; | |
| } | |
| .model-option-desc { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| /* Mobile Responsive */ | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: fixed; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| z-index: 100; | |
| box-shadow: 2px 0 8px rgba(0,0,0,0.3); | |
| } | |
| .example-prompts { | |
| grid-template-columns: 1fr; | |
| } | |
| .messages-wrapper { | |
| padding: 12px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Sidebar --> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <button class="new-chat-btn" id="new-chat-btn"> | |
| <i class="bi bi-plus-lg"></i> | |
| <span>Cuộc trò chuyện mới</span> | |
| </button> | |
| </div> | |
| <div class="chat-history" id="chat-history"> | |
| <div class="history-group"> | |
| <div class="history-group-title">Hôm nay</div> | |
| <div id="today-chats"></div> | |
| </div> | |
| <div class="history-group"> | |
| <div class="history-group-title">Hôm qua</div> | |
| <div id="yesterday-chats"></div> | |
| </div> | |
| <div class="history-group"> | |
| <div class="history-group-title">7 ngày trước</div> | |
| <div id="week-chats"></div> | |
| </div> | |
| <div class="history-group"> | |
| <div class="history-group-title">Cũ hơn</div> | |
| <div id="older-chats"></div> | |
| </div> | |
| </div> | |
| <div class="sidebar-footer"> | |
| <button class="sidebar-footer-btn" id="clear-history-btn"> | |
| <i class="bi bi-trash3"></i> | |
| <span>Xóa lịch sử chat</span> | |
| </button> | |
| <button class="sidebar-footer-btn" id="settings-btn"> | |
| <i class="bi bi-gear"></i> | |
| <span>Cài đặt</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <div class="top-bar"> | |
| <div class="top-bar-left"> | |
| <button class="toggle-sidebar-btn" id="toggle-sidebar"> | |
| <i class="bi bi-list"></i> | |
| </button> | |
| <div class="model-selector-compact" id="model-selector-btn"> | |
| <i class="bi bi-robot"></i> | |
| <span id="current-model-name">Qwen2.5 0.5B</span> | |
| <i class="bi bi-chevron-down" style="font-size: 12px;"></i> | |
| </div> | |
| </div> | |
| <div class="top-bar-right"> | |
| <div class="status-indicator loading" id="status"> | |
| <span class="status-dot"></span> | |
| <span>Đang khởi tạo...</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="chat-container"> | |
| <div class="messages-wrapper"> | |
| <div class="welcome-screen"> | |
| <div class="welcome-logo"> | |
| <i class="bi bi-chat-dots"></i> | |
| </div> | |
| <h1>Bạn muốn tôi giúp gì?</h1> | |
| <p>AI chạy hoàn toàn trên trình duyệt - Riêng tư & Bảo mật</p> | |
| <div class="example-prompts"> | |
| <div class="example-prompt" data-prompt="Giải thích khái niệm Machine Learning cho người mới bắt đầu"> | |
| <div class="example-prompt-title">📚 Giải thích khái niệm</div> | |
| <div class="example-prompt-text">Machine Learning là gì?</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Viết một bài thơ ngắn về mùa thu Hà Nội"> | |
| <div class="example-prompt-title">✍️ Sáng tạo nội dung</div> | |
| <div class="example-prompt-text">Viết thơ về mùa thu</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Tính toán: Nếu tôi đầu tư 10 triệu với lãi suất 8%/năm thì sau 5 năm tôi có bao nhiêu?"> | |
| <div class="example-prompt-title">🧮 Tính toán</div> | |
| <div class="example-prompt-text">Tính lãi suất kép</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Hướng dẫn cách làm món phở gà đơn giản tại nhà"> | |
| <div class="example-prompt-title">🍲 Nấu ăn</div> | |
| <div class="example-prompt-text">Công thức phở gà</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="input-area"> | |
| <div class="input-wrapper"> | |
| <div class="input-container"> | |
| <textarea | |
| id="user-input" | |
| placeholder="Nhắn tin cho AI..." | |
| rows="1" | |
| disabled | |
| ></textarea> | |
| <button id="send-btn" disabled> | |
| <i class="bi bi-arrow-up"></i> | |
| </button> | |
| </div> | |
| <div class="input-footer"> | |
| AI có thể sai. Hãy kiểm tra thông tin quan trọng. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Model Selector Modal --> | |
| <div class="modal-overlay" id="model-modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h3 class="modal-title">Chọn Model</h3> | |
| <button class="modal-close" id="modal-close">×</button> | |
| </div> | |
| <div id="model-options"> | |
| <div class="model-option selected" data-model="onnx-community/Qwen2.5-0.5B-Instruct" data-name="Qwen2.5 0.5B"> | |
| <div class="model-option-header"> | |
| <span class="model-option-name">Qwen2.5 0.5B</span> | |
| <span class="model-option-size">300MB</span> | |
| </div> | |
| <div class="model-option-desc">Nhanh nhất - Phù hợp chat đơn giản</div> | |
| </div> | |
| <div class="model-option" data-model="onnx-community/Qwen2.5-1.5B-Instruct" data-name="Qwen2.5 1.5B"> | |
| <div class="model-option-header"> | |
| <span class="model-option-name">Qwen2.5 1.5B</span> | |
| <span class="model-option-size">800MB</span> | |
| </div> | |
| <div class="model-option-desc">Cân bằng tốc độ & chất lượng</div> | |
| </div> | |
| <div class="model-option" data-model="onnx-community/Phi-3.5-mini-instruct" data-name="Phi-3.5 Mini"> | |
| <div class="model-option-header"> | |
| <span class="model-option-name">Phi-3.5 Mini</span> | |
| <span class="model-option-size">2.4GB</span> | |
| </div> | |
| <div class="model-option-desc">Microsoft - Rất thông minh, cần nhiều RAM</div> | |
| </div> | |
| <div class="model-option" data-model="onnx-community/Llama-3.2-1B-Instruct" data-name="Llama 3.2 1B"> | |
| <div class="model-option-header"> | |
| <span class="model-option-name">Llama 3.2 1B</span> | |
| <span class="model-option-size">650MB</span> | |
| </div> | |
| <div class="model-option-desc">Meta AI - Hiệu suất tốt</div> | |
| </div> | |
| <div class="model-option" data-model="onnx-community/SmolLM2-360M-Instruct" data-name="SmolLM2 360M"> | |
| <div class="model-option-header"> | |
| <span class="model-option-name">SmolLM2 360M</span> | |
| <span class="model-option-size">200MB</span> | |
| </div> | |
| <div class="model-option-desc">Siêu nhẹ - Tải nhanh nhất</div> | |
| </div> | |
| </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; | |
| // DOM Elements | |
| const sidebar = document.getElementById('sidebar'); | |
| const toggleSidebarBtn = document.getElementById('toggle-sidebar'); | |
| const newChatBtn = document.getElementById('new-chat-btn'); | |
| const chatContainer = document.getElementById('chat-container'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const status = document.getElementById('status'); | |
| const modelSelectorBtn = document.getElementById('model-selector-btn'); | |
| const currentModelName = document.getElementById('current-model-name'); | |
| const modelModal = document.getElementById('model-modal'); | |
| const modalClose = document.getElementById('modal-close'); | |
| const clearHistoryBtn = document.getElementById('clear-history-btn'); | |
| // State | |
| let generator = null; | |
| let isGenerating = false; | |
| let currentChatId = null; | |
| let chats = JSON.parse(localStorage.getItem('chats') || '[]'); | |
| let selectedModel = 'onnx-community/Qwen2.5-0.5B-Instruct'; | |
| let selectedModelName = 'Qwen2.5 0.5B'; | |
| // Initialize | |
| loadChatHistory(); | |
| loadModel(selectedModel); | |
| // Auto-resize textarea | |
| userInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 200) + 'px'; | |
| }); | |
| // Sidebar toggle | |
| toggleSidebarBtn.addEventListener('click', () => { | |
| sidebar.classList.toggle('collapsed'); | |
| }); | |
| // New chat | |
| newChatBtn.addEventListener('click', createNewChat); | |
| function createNewChat() { | |
| currentChatId = Date.now(); | |
| chatContainer.querySelector('.messages-wrapper').innerHTML = ` | |
| <div class="welcome-screen"> | |
| <div class="welcome-logo"> | |
| <i class="bi bi-chat-dots"></i> | |
| </div> | |
| <h1>Bạn muốn tôi giúp gì?</h1> | |
| <p>AI chạy hoàn toàn trên trình duyệt - Riêng tư & Bảo mật</p> | |
| <div class="example-prompts"> | |
| <div class="example-prompt" data-prompt="Giải thích khái niệm Machine Learning cho người mới bắt đầu"> | |
| <div class="example-prompt-title">📚 Giải thích khái niệm</div> | |
| <div class="example-prompt-text">Machine Learning là gì?</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Viết một bài thơ ngắn về mùa thu Hà Nội"> | |
| <div class="example-prompt-title">✍️ Sáng tạo nội dung</div> | |
| <div class="example-prompt-text">Viết thơ về mùa thu</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Tính toán: Nếu tôi đầu tư 10 triệu với lãi suất 8%/năm thì sau 5 năm tôi có bao nhiêu?"> | |
| <div class="example-prompt-title">🧮 Tính toán</div> | |
| <div class="example-prompt-text">Tính lãi suất kép</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Hướng dẫn cách làm món phở gà đơn giản tại nhà"> | |
| <div class="example-prompt-title">🍲 Nấu ăn</div> | |
| <div class="example-prompt-text">Công thức phở gà</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| attachExamplePromptListeners(); | |
| } | |
| // Model selector | |
| modelSelectorBtn.addEventListener('click', () => { | |
| modelModal.classList.add('show'); | |
| }); | |
| modalClose.addEventListener('click', () => { | |
| modelModal.classList.remove('show'); | |
| }); | |
| modelModal.addEventListener('click', (e) => { | |
| if (e.target === modelModal) { | |
| modelModal.classList.remove('show'); | |
| } | |
| }); | |
| document.querySelectorAll('.model-option').forEach(option => { | |
| option.addEventListener('click', () => { | |
| document.querySelectorAll('.model-option').forEach(o => o.classList.remove('selected')); | |
| option.classList.add('selected'); | |
| selectedModel = option.dataset.model; | |
| selectedModelName = option.dataset.name; | |
| currentModelName.textContent = selectedModelName; | |
| modelModal.classList.remove('show'); | |
| loadModel(selectedModel); | |
| }); | |
| }); | |
| // Load model | |
| async function loadModel(modelName) { | |
| if (generator) generator = null; | |
| updateStatus('loading', 'Đang tải model...'); | |
| userInput.disabled = true; | |
| sendBtn.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); | |
| updateStatus('loading', `Đang tải: ${percent}%`); | |
| } | |
| } | |
| }); | |
| updateStatus('ready', 'Sẵn sàng'); | |
| userInput.disabled = false; | |
| sendBtn.disabled = false; | |
| userInput.focus(); | |
| } catch (e) { | |
| updateStatus('error', 'Lỗi tải model'); | |
| console.error(e); | |
| } | |
| } | |
| function updateStatus(type, text) { | |
| status.className = `status-indicator ${type}`; | |
| status.innerHTML = `<span class="status-dot"></span><span>${text}</span>`; | |
| } | |
| // Chat functionality | |
| 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) { | |
| chatContainer.querySelector('.messages-wrapper').innerHTML = ''; | |
| } | |
| // Add user message | |
| addMessage('user', text); | |
| userInput.value = ''; | |
| userInput.style.height = 'auto'; | |
| // Add AI message placeholder | |
| const aiMessageEl = addMessage('ai', '', true); | |
| const contentDiv = aiMessageEl.querySelector('.message-content'); | |
| // Save to chat history | |
| if (!currentChatId) { | |
| currentChatId = Date.now(); | |
| } | |
| const streamer = new TextStreamer(generator.tokenizer, { | |
| skip_prompt: true, | |
| callback_function: (token) => { | |
| const typingIndicator = aiMessageEl.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, | |
| }); | |
| // Save chat | |
| saveChat(text, contentDiv.textContent); | |
| // Add message actions | |
| const actions = document.createElement('div'); | |
| actions.className = 'message-actions'; | |
| actions.innerHTML = ` | |
| <button class="message-action-btn copy-btn"> | |
| <i class="bi bi-clipboard"></i> Sao chép | |
| </button> | |
| <button class="message-action-btn"> | |
| <i class="bi bi-hand-thumbs-up"></i> | |
| </button> | |
| <button class="message-action-btn"> | |
| <i class="bi bi-hand-thumbs-down"></i> | |
| </button> | |
| `; | |
| aiMessageEl.appendChild(actions); | |
| // Copy functionality | |
| actions.querySelector('.copy-btn').addEventListener('click', () => { | |
| navigator.clipboard.writeText(contentDiv.textContent); | |
| actions.querySelector('.copy-btn').innerHTML = '<i class="bi bi-check"></i> Đã sao chép'; | |
| setTimeout(() => { | |
| actions.querySelector('.copy-btn').innerHTML = '<i class="bi bi-clipboard"></i> Sao chép'; | |
| }, 2000); | |
| }); | |
| } catch (err) { | |
| contentDiv.textContent = '❌ Lỗi: ' + err.message; | |
| } | |
| isGenerating = false; | |
| sendBtn.disabled = false; | |
| userInput.focus(); | |
| } | |
| function addMessage(type, content, isTyping = false) { | |
| const messageEl = document.createElement('div'); | |
| messageEl.className = 'message'; | |
| const avatar = type === 'user' | |
| ? '<i class="bi bi-person-circle"></i>' | |
| : '<i class="bi bi-robot"></i>'; | |
| const author = type === 'user' ? 'Bạn' : selectedModelName; | |
| messageEl.innerHTML = ` | |
| <div class="message-header"> | |
| <div class="message-avatar ${type}-avatar">${avatar}</div> | |
| <div class="message-author">${author}</div> | |
| </div> | |
| ${isTyping ? '<div class="typing-indicator"><span></span><span></span><span></span></div>' : ''} | |
| <div class="message-content">${content}</div> | |
| `; | |
| chatContainer.querySelector('.messages-wrapper').appendChild(messageEl); | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| return messageEl; | |
| } | |
| // Chat history | |
| function saveChat(userMessage, aiResponse) { | |
| const chat = chats.find(c => c.id === currentChatId); | |
| const title = userMessage.substring(0, 50); | |
| if (chat) { | |
| chat.messages.push( | |
| { role: 'user', content: userMessage }, | |
| { role: 'ai', content: aiResponse } | |
| ); | |
| chat.updatedAt = Date.now(); | |
| } else { | |
| chats.unshift({ | |
| id: currentChatId, | |
| title: title, | |
| messages: [ | |
| { role: 'user', content: userMessage }, | |
| { role: 'ai', content: aiResponse } | |
| ], | |
| createdAt: Date.now(), | |
| updatedAt: Date.now() | |
| }); | |
| } | |
| localStorage.setItem('chats', JSON.stringify(chats)); | |
| loadChatHistory(); | |
| } | |
| function loadChatHistory() { | |
| const now = Date.now(); | |
| const oneDay = 24 * 60 * 60 * 1000; | |
| const sevenDays = 7 * oneDay; | |
| const todayChats = []; | |
| const yesterdayChats = []; | |
| const weekChats = []; | |
| const olderChats = []; | |
| chats.forEach(chat => { | |
| const diff = now - chat.updatedAt; | |
| if (diff < oneDay) { | |
| todayChats.push(chat); | |
| } else if (diff < 2 * oneDay) { | |
| yesterdayChats.push(chat); | |
| } else if (diff < sevenDays) { | |
| weekChats.push(chat); | |
| } else { | |
| olderChats.push(chat); | |
| } | |
| }); | |
| renderChatGroup('today-chats', todayChats); | |
| renderChatGroup('yesterday-chats', yesterdayChats); | |
| renderChatGroup('week-chats', weekChats); | |
| renderChatGroup('older-chats', olderChats); | |
| } | |
| function renderChatGroup(elementId, chats) { | |
| const container = document.getElementById(elementId); | |
| container.innerHTML = ''; | |
| chats.forEach(chat => { | |
| const item = document.createElement('div'); | |
| item.className = 'history-item'; | |
| if (chat.id === currentChatId) item.classList.add('active'); | |
| item.innerHTML = ` | |
| <span class="history-item-text">${chat.title}</span> | |
| <div class="history-item-actions"> | |
| <button class="history-action-btn rename-btn" data-id="${chat.id}"> | |
| <i class="bi bi-pencil"></i> | |
| </button> | |
| <button class="history-action-btn delete-btn" data-id="${chat.id}"> | |
| <i class="bi bi-trash"></i> | |
| </button> | |
| </div> | |
| `; | |
| item.addEventListener('click', (e) => { | |
| if (!e.target.closest('.history-action-btn')) { | |
| loadChat(chat.id); | |
| } | |
| }); | |
| item.querySelector('.delete-btn').addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| deleteChat(chat.id); | |
| }); | |
| container.appendChild(item); | |
| }); | |
| } | |
| function loadChat(chatId) { | |
| const chat = chats.find(c => c.id === chatId); | |
| if (!chat) return; | |
| currentChatId = chatId; | |
| chatContainer.querySelector('.messages-wrapper').innerHTML = ''; | |
| chat.messages.forEach(msg => { | |
| addMessage(msg.role, msg.content); | |
| }); | |
| loadChatHistory(); | |
| } | |
| function deleteChat(chatId) { | |
| if (confirm('Bạn có chắc muốn xóa cuộc trò chuyện này?')) { | |
| chats = chats.filter(c => c.id !== chatId); | |
| localStorage.setItem('chats', JSON.stringify(chats)); | |
| if (currentChatId === chatId) { | |
| createNewChat(); | |
| } | |
| loadChatHistory(); | |
| } | |
| } | |
| clearHistoryBtn.addEventListener('click', () => { | |
| if (confirm('Bạn có chắc muốn xóa toàn bộ lịch sử chat?')) { | |
| chats = []; | |
| localStorage.setItem('chats', JSON.stringify(chats)); | |
| loadChatHistory(); | |
| createNewChat(); | |
| } | |
| }); | |
| // Send message | |
| sendBtn.addEventListener('click', () => chat(userInput.value.trim())); | |
| userInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && !e.shiftKey) { | |
| e.preventDefault(); | |
| chat(userInput.value.trim()); | |
| } | |
| }); | |
| // Example prompts | |
| function attachExamplePromptListeners() { | |
| document.querySelectorAll('.example-prompt').forEach(prompt => { | |
| prompt.addEventListener('click', () => { | |
| const text = prompt.dataset.prompt; | |
| chat(text); | |
| }); | |
| }); | |
| } | |
| attachExamplePromptListeners(); | |
| </script> | |
| </body> | |
| </html> |