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 Pro - 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: 280px; | |
| --primary: #10a37f; | |
| --primary-hover: #0d8a6a; | |
| --secondary: #8e44ad; | |
| --accent: #3498db; | |
| --danger: #e74c3c; | |
| --warning: #f39c12; | |
| --success: #27ae60; | |
| --bg-primary: #ffffff; | |
| --bg-secondary: #f7f7f8; | |
| --bg-tertiary: #ececf1; | |
| --text-primary: #353740; | |
| --text-secondary: #6e6e80; | |
| --text-tertiary: #acacbe; | |
| --border: #e5e5e5; | |
| --sidebar-bg: #171717; | |
| --sidebar-hover: #2a2b32; | |
| --sidebar-active: #343541; | |
| --shadow-sm: 0 1px 3px rgba(0,0,0,0.1); | |
| --shadow-md: 0 4px 6px rgba(0,0,0,0.1); | |
| --shadow-lg: 0 10px 20px rgba(0,0,0,0.15); | |
| } | |
| [data-theme="dark"] { | |
| --bg-primary: #343541; | |
| --bg-secondary: #444654; | |
| --bg-tertiary: #565869; | |
| --text-primary: #ececf1; | |
| --text-secondary: #c5c5d2; | |
| --text-tertiary: #9a9aaa; | |
| --border: #565869; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| height: 100vh; | |
| overflow: hidden; | |
| transition: background 0.3s, color 0.3s; | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--text-tertiary); | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--text-secondary); | |
| } | |
| /* Layout */ | |
| .app-container { | |
| display: flex; | |
| height: 100vh; | |
| position: relative; | |
| } | |
| /* Sidebar backdrop for mobile */ | |
| .sidebar-backdrop { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.5); | |
| z-index: 99; | |
| backdrop-filter: blur(2px); | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar-backdrop.show { | |
| display: block; | |
| } | |
| } | |
| /* 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 cubic-bezier(0.4, 0, 0.2, 1); | |
| z-index: 100; | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .sidebar.collapsed { | |
| transform: translateX(-100%); | |
| } | |
| .sidebar-header { | |
| padding: 16px; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .logo-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| padding: 8px; | |
| } | |
| .logo { | |
| width: 36px; | |
| height: 36px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-size: 20px; | |
| font-weight: bold; | |
| box-shadow: var(--shadow-md); | |
| } | |
| .logo-text { | |
| font-size: 18px; | |
| font-weight: 700; | |
| color: white; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .new-chat-btn { | |
| width: 100%; | |
| padding: 12px 16px; | |
| background: linear-gradient(135deg, var(--primary), var(--primary-hover)); | |
| border: none; | |
| color: white; | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| font-size: 14px; | |
| font-weight: 600; | |
| box-shadow: var(--shadow-md); | |
| } | |
| .new-chat-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .new-chat-btn:active { | |
| transform: translateY(0); | |
| } | |
| .search-box { | |
| position: relative; | |
| margin-top: 12px; | |
| } | |
| .search-box input { | |
| width: 100%; | |
| padding: 10px 36px 10px 12px; | |
| background: rgba(255,255,255,0.1); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| border-radius: 8px; | |
| color: white; | |
| font-size: 13px; | |
| transition: all 0.3s; | |
| } | |
| .search-box input:focus { | |
| outline: none; | |
| background: rgba(255,255,255,0.15); | |
| border-color: var(--primary); | |
| } | |
| .search-box input::placeholder { | |
| color: rgba(255,255,255,0.5); | |
| } | |
| .search-box i { | |
| position: absolute; | |
| right: 12px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: rgba(255,255,255,0.5); | |
| } | |
| .chat-history { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 8px; | |
| } | |
| .history-group { | |
| margin-bottom: 24px; | |
| } | |
| .history-group-title { | |
| color: rgba(255,255,255,0.5); | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| padding: 8px 12px; | |
| letter-spacing: 0.8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .history-item { | |
| padding: 12px; | |
| margin: 2px 0; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| color: rgba(255,255,255,0.8); | |
| font-size: 14px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .history-item::before { | |
| content: ''; | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| width: 3px; | |
| background: var(--primary); | |
| transform: scaleY(0); | |
| transition: transform 0.3s; | |
| } | |
| .history-item:hover::before { | |
| transform: scaleY(1); | |
| } | |
| .history-item:hover { | |
| background: var(--sidebar-hover); | |
| padding-left: 16px; | |
| } | |
| .history-item.active { | |
| background: var(--sidebar-active); | |
| color: white; | |
| } | |
| .history-item.active::before { | |
| transform: scaleY(1); | |
| } | |
| .history-item-icon { | |
| font-size: 16px; | |
| opacity: 0.7; | |
| } | |
| .history-item-content { | |
| flex: 1; | |
| overflow: hidden; | |
| } | |
| .history-item-text { | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| font-weight: 500; | |
| } | |
| .history-item-meta { | |
| font-size: 11px; | |
| color: rgba(255,255,255,0.4); | |
| margin-top: 2px; | |
| } | |
| .history-item-actions { | |
| display: none; | |
| gap: 4px; | |
| } | |
| .history-item:hover .history-item-actions { | |
| display: flex; | |
| } | |
| .history-action-btn { | |
| padding: 6px 8px; | |
| background: rgba(255,255,255,0.1); | |
| border: none; | |
| color: rgba(255,255,255,0.7); | |
| cursor: pointer; | |
| border-radius: 6px; | |
| font-size: 13px; | |
| transition: all 0.2s; | |
| } | |
| .history-action-btn:hover { | |
| background: rgba(255,255,255,0.2); | |
| color: white; | |
| } | |
| .sidebar-footer { | |
| padding: 12px; | |
| border-top: 1px solid rgba(255,255,255,0.1); | |
| background: rgba(0,0,0,0.2); | |
| } | |
| .sidebar-footer-btn { | |
| width: 100%; | |
| padding: 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: 12px; | |
| font-size: 14px; | |
| margin-bottom: 4px; | |
| } | |
| .sidebar-footer-btn:hover { | |
| background: var(--sidebar-hover); | |
| color: white; | |
| } | |
| .sidebar-footer-btn i { | |
| width: 20px; | |
| text-align: center; | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| background: var(--bg-primary); | |
| } | |
| .top-bar { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 12px 20px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg-primary); | |
| backdrop-filter: blur(10px); | |
| box-shadow: var(--shadow-sm); | |
| z-index: 10; | |
| } | |
| .top-bar-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .toggle-sidebar-btn { | |
| padding: 10px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| cursor: pointer; | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 18px; | |
| transition: all 0.2s; | |
| } | |
| .toggle-sidebar-btn:hover { | |
| background: var(--bg-tertiary); | |
| transform: scale(1.05); | |
| } | |
| .model-selector-compact { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 8px 16px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-size: 14px; | |
| font-weight: 600; | |
| transition: all 0.2s; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .model-selector-compact:hover { | |
| background: var(--bg-tertiary); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-md); | |
| } | |
| .model-badge { | |
| padding: 4px 10px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: white; | |
| border-radius: 6px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .top-bar-center { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .chat-title { | |
| font-weight: 600; | |
| font-size: 16px; | |
| color: var(--text-primary); | |
| } | |
| .top-bar-right { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .top-bar-btn { | |
| padding: 10px 12px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| cursor: pointer; | |
| border-radius: 8px; | |
| color: var(--text-primary); | |
| font-size: 16px; | |
| transition: all 0.2s; | |
| position: relative; | |
| } | |
| .top-bar-btn:hover { | |
| background: var(--bg-tertiary); | |
| transform: scale(1.05); | |
| } | |
| .top-bar-btn.active { | |
| background: var(--primary); | |
| color: white; | |
| border-color: var(--primary); | |
| } | |
| .badge-notification { | |
| position: absolute; | |
| top: -4px; | |
| right: -4px; | |
| background: var(--danger); | |
| color: white; | |
| border-radius: 50%; | |
| width: 18px; | |
| height: 18px; | |
| font-size: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 700; | |
| } | |
| .status-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 14px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .status-indicator.loading { | |
| background: linear-gradient(135deg, #fef3c7, #fde68a); | |
| color: #92400e; | |
| } | |
| .status-indicator.ready { | |
| background: linear-gradient(135deg, #d1fae5, #a7f3d0); | |
| color: #065f46; | |
| } | |
| .status-indicator.error { | |
| background: linear-gradient(135deg, #fee2e2, #fecaca); | |
| color: #991b1b; | |
| } | |
| .status-dot { | |
| width: 8px; | |
| height: 8px; | |
| border-radius: 50%; | |
| background: currentColor; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| /* Chat Area */ | |
| #chat-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| background: var(--bg-primary); | |
| position: relative; | |
| } | |
| .messages-wrapper { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 30px 20px; | |
| } | |
| .message { | |
| margin-bottom: 32px; | |
| animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .message-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| margin-bottom: 12px; | |
| } | |
| .message-avatar { | |
| width: 36px; | |
| height: 36px; | |
| border-radius: 8px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 18px; | |
| box-shadow: var(--shadow-sm); | |
| flex-shrink: 0; | |
| } | |
| .user-avatar { | |
| background: linear-gradient(135deg, var(--accent), #2980b9); | |
| color: white; | |
| } | |
| .ai-avatar { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: white; | |
| } | |
| .message-author { | |
| font-weight: 700; | |
| font-size: 15px; | |
| color: var(--text-primary); | |
| } | |
| .message-time { | |
| font-size: 12px; | |
| color: var(--text-tertiary); | |
| margin-left: auto; | |
| } | |
| .message-content { | |
| margin-left: 48px; | |
| line-height: 1.8; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| color: var(--text-primary); | |
| background: var(--bg-secondary); | |
| padding: 16px 20px; | |
| border-radius: 12px; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .user .message-content { | |
| background: linear-gradient(135deg, rgba(52, 152, 219, 0.1), rgba(41, 128, 185, 0.1)); | |
| border-left: 3px solid var(--accent); | |
| } | |
| .ai .message-content { | |
| background: var(--bg-secondary); | |
| border-left: 3px solid var(--primary); | |
| } | |
| .typing-indicator { | |
| display: inline-flex; | |
| gap: 6px; | |
| align-items: center; | |
| margin-left: 48px; | |
| padding: 16px 20px; | |
| background: var(--bg-secondary); | |
| border-radius: 12px; | |
| } | |
| .typing-indicator span { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--primary); | |
| border-radius: 50%; | |
| animation: typing 1.4s infinite; | |
| } | |
| .typing-indicator span:nth-child(1) { animation-delay: 0s; } | |
| .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; | |
| } | |
| } | |
| .message-actions { | |
| margin-left: 48px; | |
| margin-top: 10px; | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .message-action-btn { | |
| padding: 6px 12px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| cursor: pointer; | |
| border-radius: 8px; | |
| font-size: 13px; | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| transition: all 0.2s; | |
| } | |
| .message-action-btn:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| transform: translateY(-2px); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .message-action-btn.active { | |
| background: var(--primary); | |
| color: white; | |
| border-color: var(--primary); | |
| } | |
| /* Welcome Screen */ | |
| .welcome-screen { | |
| text-align: center; | |
| padding: 80px 20px; | |
| max-width: 800px; | |
| margin: auto; | |
| } | |
| .welcome-logo { | |
| font-size: 64px; | |
| margin-bottom: 24px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-10px); } | |
| } | |
| .welcome-screen h1 { | |
| font-size: 42px; | |
| margin-bottom: 16px; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, var(--text-primary), var(--text-secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .welcome-screen p { | |
| color: var(--text-secondary); | |
| margin-bottom: 48px; | |
| font-size: 18px; | |
| line-height: 1.6; | |
| } | |
| .feature-pills { | |
| display: flex; | |
| justify-content: center; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| margin-bottom: 40px; | |
| } | |
| .feature-pill { | |
| padding: 8px 16px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .example-prompts { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| gap: 16px; | |
| margin-top: 40px; | |
| } | |
| .example-prompt { | |
| background: var(--bg-secondary); | |
| border: 2px solid var(--border); | |
| padding: 20px; | |
| border-radius: 16px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-align: left; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .example-prompt::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| transform: scaleX(0); | |
| transition: transform 0.3s; | |
| } | |
| .example-prompt:hover::before { | |
| transform: scaleX(1); | |
| } | |
| .example-prompt:hover { | |
| background: var(--bg-tertiary); | |
| border-color: var(--primary); | |
| transform: translateY(-4px); | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .example-prompt-icon { | |
| font-size: 28px; | |
| margin-bottom: 12px; | |
| } | |
| .example-prompt-title { | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| font-size: 15px; | |
| color: var(--text-primary); | |
| } | |
| .example-prompt-text { | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| line-height: 1.5; | |
| } | |
| /* Input Area */ | |
| #input-area { | |
| padding: 24px; | |
| background: var(--bg-primary); | |
| border-top: 1px solid var(--border); | |
| box-shadow: 0 -4px 6px rgba(0,0,0,0.05); | |
| } | |
| .input-wrapper { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| } | |
| .input-tools { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 12px; | |
| } | |
| .input-tool-btn { | |
| padding: 8px 12px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| transition: all 0.2s; | |
| } | |
| .input-tool-btn:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| } | |
| .input-tool-btn.active { | |
| background: var(--primary); | |
| color: white; | |
| border-color: var(--primary); | |
| } | |
| .input-container { | |
| position: relative; | |
| background: var(--bg-secondary); | |
| border: 2px solid var(--border); | |
| border-radius: 16px; | |
| padding: 16px 20px; | |
| display: flex; | |
| align-items: flex-end; | |
| gap: 12px; | |
| transition: all 0.3s; | |
| box-shadow: var(--shadow-md); | |
| } | |
| .input-container:focus-within { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 4px rgba(16, 163, 127, 0.1); | |
| } | |
| #user-input { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| outline: none; | |
| resize: none; | |
| font-size: 15px; | |
| line-height: 1.6; | |
| max-height: 200px; | |
| color: var(--text-primary); | |
| font-family: inherit; | |
| } | |
| #user-input::placeholder { | |
| color: var(--text-tertiary); | |
| } | |
| .input-actions { | |
| display: flex; | |
| gap: 8px; | |
| align-items: center; | |
| } | |
| .input-action-btn { | |
| padding: 8px; | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| border-radius: 8px; | |
| color: var(--text-secondary); | |
| font-size: 18px; | |
| transition: all 0.2s; | |
| } | |
| .input-action-btn:hover { | |
| background: var(--bg-tertiary); | |
| color: var(--text-primary); | |
| } | |
| #send-btn, | |
| #stop-btn { | |
| padding: 10px 12px; | |
| background: linear-gradient(135deg, var(--primary), var(--primary-hover)); | |
| border: none; | |
| border-radius: 10px; | |
| color: white; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| min-width: 44px; | |
| height: 44px; | |
| flex-shrink: 0; | |
| box-shadow: var(--shadow-md); | |
| } | |
| #stop-btn { | |
| background: linear-gradient(135deg, var(--danger), #c0392b); | |
| display: none; | |
| } | |
| #stop-btn.show { | |
| display: flex; | |
| } | |
| #send-btn:hover:not(:disabled), | |
| #stop-btn:hover { | |
| background: linear-gradient(135deg, var(--primary-hover), var(--primary)); | |
| transform: scale(1.05); | |
| } | |
| #stop-btn:hover { | |
| background: linear-gradient(135deg, #c0392b, #a93226); | |
| } | |
| #send-btn:active:not(:disabled), | |
| #stop-btn:active { | |
| transform: scale(0.95); | |
| } | |
| #send-btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .input-footer { | |
| margin-top: 12px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .input-footer-left { | |
| font-size: 12px; | |
| color: var(--text-tertiary); | |
| } | |
| .input-footer-right { | |
| display: flex; | |
| gap: 12px; | |
| font-size: 12px; | |
| color: var(--text-tertiary); | |
| } | |
| .char-counter { | |
| font-weight: 600; | |
| } | |
| /* Modal */ | |
| .modal-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0,0,0,0.6); | |
| backdrop-filter: blur(4px); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| animation: fadeIn 0.3s; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; } | |
| to { opacity: 1; } | |
| } | |
| .modal-overlay.show { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: var(--bg-primary); | |
| border-radius: 20px; | |
| padding: 32px; | |
| max-width: 600px; | |
| width: 90%; | |
| max-height: 85vh; | |
| overflow-y: auto; | |
| box-shadow: var(--shadow-lg); | |
| animation: slideUp 0.3s; | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(40px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .modal-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 24px; | |
| padding-bottom: 16px; | |
| border-bottom: 2px solid var(--border); | |
| } | |
| .modal-title { | |
| font-size: 24px; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| .modal-close { | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| font-size: 24px; | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| padding: 8px 12px; | |
| border-radius: 10px; | |
| transition: all 0.2s; | |
| } | |
| .modal-close:hover { | |
| background: var(--danger); | |
| color: white; | |
| border-color: var(--danger); | |
| transform: rotate(90deg); | |
| } | |
| .model-option { | |
| padding: 20px; | |
| border: 2px solid var(--border); | |
| border-radius: 16px; | |
| margin-bottom: 16px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .model-option::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 4px; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| transform: scaleX(0); | |
| transition: transform 0.3s; | |
| } | |
| .model-option:hover::before { | |
| transform: scaleX(1); | |
| } | |
| .model-option:hover { | |
| background: var(--bg-secondary); | |
| border-color: var(--primary); | |
| transform: translateX(8px); | |
| } | |
| .model-option.selected { | |
| border-color: var(--primary); | |
| background: linear-gradient(135deg, rgba(16, 163, 127, 0.05), rgba(142, 68, 173, 0.05)); | |
| box-shadow: var(--shadow-md); | |
| } | |
| .model-option.selected::before { | |
| transform: scaleX(1); | |
| } | |
| .model-option-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 12px; | |
| } | |
| .model-option-name { | |
| font-weight: 700; | |
| font-size: 17px; | |
| color: var(--text-primary); | |
| } | |
| .model-option-badges { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .model-option-size { | |
| font-size: 11px; | |
| color: white; | |
| background: linear-gradient(135deg, var(--accent), #2980b9); | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| font-weight: 700; | |
| } | |
| .model-option-speed { | |
| font-size: 11px; | |
| padding: 4px 10px; | |
| border-radius: 6px; | |
| font-weight: 700; | |
| } | |
| .speed-fast { | |
| background: linear-gradient(135deg, var(--success), #229954); | |
| color: white; | |
| } | |
| .speed-medium { | |
| background: linear-gradient(135deg, var(--warning), #d68910); | |
| color: white; | |
| } | |
| .speed-slow { | |
| background: linear-gradient(135deg, var(--danger), #c0392b); | |
| color: white; | |
| } | |
| .model-option-desc { | |
| font-size: 14px; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| .model-option-features { | |
| margin-top: 12px; | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .feature-tag { | |
| padding: 4px 10px; | |
| background: var(--bg-tertiary); | |
| border-radius: 6px; | |
| font-size: 11px; | |
| color: var(--text-secondary); | |
| font-weight: 600; | |
| } | |
| /* Settings Panel */ | |
| .settings-section { | |
| margin-bottom: 28px; | |
| } | |
| .settings-section-title { | |
| font-size: 16px; | |
| font-weight: 700; | |
| margin-bottom: 16px; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .setting-item { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 16px; | |
| background: var(--bg-secondary); | |
| border-radius: 12px; | |
| margin-bottom: 12px; | |
| border: 1px solid var(--border); | |
| } | |
| .setting-item-info { | |
| flex: 1; | |
| } | |
| .setting-item-label { | |
| font-weight: 600; | |
| margin-bottom: 4px; | |
| color: var(--text-primary); | |
| } | |
| .setting-item-desc { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| .toggle-switch { | |
| position: relative; | |
| width: 52px; | |
| height: 28px; | |
| background: var(--border); | |
| border-radius: 14px; | |
| cursor: pointer; | |
| transition: background 0.3s; | |
| } | |
| .toggle-switch.active { | |
| background: var(--primary); | |
| } | |
| .toggle-switch::after { | |
| content: ''; | |
| position: absolute; | |
| top: 3px; | |
| left: 3px; | |
| width: 22px; | |
| height: 22px; | |
| background: white; | |
| border-radius: 50%; | |
| transition: transform 0.3s; | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .toggle-switch.active::after { | |
| transform: translateX(24px); | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| padding: 16px 24px; | |
| background: var(--bg-primary); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| box-shadow: var(--shadow-lg); | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| z-index: 2000; | |
| animation: slideInRight 0.3s; | |
| max-width: 400px; | |
| } | |
| @keyframes slideInRight { | |
| from { | |
| opacity: 0; | |
| transform: translateX(100px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| .toast.success { | |
| border-color: var(--success); | |
| } | |
| .toast.error { | |
| border-color: var(--danger); | |
| } | |
| .toast.warning { | |
| border-color: var(--warning); | |
| } | |
| .toast-icon { | |
| font-size: 24px; | |
| } | |
| .toast.success .toast-icon { | |
| color: var(--success); | |
| } | |
| .toast.error .toast-icon { | |
| color: var(--danger); | |
| } | |
| .toast.warning .toast-icon { | |
| color: var(--warning); | |
| } | |
| .toast-content { | |
| flex: 1; | |
| } | |
| .toast-title { | |
| font-weight: 700; | |
| margin-bottom: 4px; | |
| color: var(--text-primary); | |
| } | |
| .toast-message { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| /* Mobile Responsive */ | |
| @media (max-width: 768px) { | |
| :root { | |
| --sidebar-width: 280px; | |
| } | |
| .sidebar { | |
| position: fixed; | |
| left: 0; | |
| top: 0; | |
| bottom: 0; | |
| z-index: 200; | |
| box-shadow: var(--shadow-lg); | |
| } | |
| .sidebar-header { | |
| padding: 12px; | |
| } | |
| .logo-section { | |
| margin-bottom: 8px; | |
| } | |
| .new-chat-btn { | |
| padding: 10px 14px; | |
| font-size: 13px; | |
| } | |
| .search-box input { | |
| padding: 8px 32px 8px 10px; | |
| font-size: 12px; | |
| } | |
| .welcome-screen { | |
| padding: 40px 16px; | |
| } | |
| .welcome-screen h1 { | |
| font-size: 24px; | |
| } | |
| .welcome-screen p { | |
| font-size: 14px; | |
| } | |
| .welcome-logo { | |
| font-size: 48px; | |
| } | |
| .feature-pills { | |
| gap: 8px; | |
| } | |
| .feature-pill { | |
| font-size: 11px; | |
| padding: 6px 12px; | |
| } | |
| .example-prompts { | |
| grid-template-columns: 1fr; | |
| gap: 12px; | |
| } | |
| .example-prompt { | |
| padding: 16px; | |
| } | |
| .example-prompt-icon { | |
| font-size: 24px; | |
| } | |
| .example-prompt-title { | |
| font-size: 14px; | |
| } | |
| .example-prompt-text { | |
| font-size: 12px; | |
| } | |
| .messages-wrapper { | |
| padding: 16px 12px; | |
| } | |
| .message { | |
| margin-bottom: 24px; | |
| } | |
| .message-avatar { | |
| width: 32px; | |
| height: 32px; | |
| font-size: 16px; | |
| } | |
| .message-author { | |
| font-size: 13px; | |
| } | |
| .message-time { | |
| font-size: 11px; | |
| } | |
| .message-content { | |
| margin-left: 0; | |
| padding: 12px 16px; | |
| font-size: 14px; | |
| } | |
| .message-actions { | |
| margin-left: 0; | |
| margin-top: 8px; | |
| } | |
| .message-action-btn { | |
| padding: 6px 10px; | |
| font-size: 12px; | |
| } | |
| .typing-indicator { | |
| margin-left: 0; | |
| padding: 12px 16px; | |
| } | |
| .top-bar { | |
| padding: 10px 12px; | |
| flex-wrap: wrap; | |
| } | |
| .top-bar-left { | |
| gap: 10px; | |
| } | |
| .top-bar-center { | |
| display: none; | |
| } | |
| .top-bar-right { | |
| gap: 8px; | |
| } | |
| .toggle-sidebar-btn { | |
| padding: 8px; | |
| font-size: 20px; | |
| } | |
| .model-selector-compact { | |
| padding: 6px 12px; | |
| font-size: 12px; | |
| } | |
| .model-selector-compact span:not(.model-badge) { | |
| display: none; | |
| } | |
| .model-badge { | |
| font-size: 10px; | |
| padding: 3px 8px; | |
| } | |
| .top-bar-btn { | |
| padding: 8px 10px; | |
| font-size: 14px; | |
| } | |
| .status-indicator { | |
| font-size: 11px; | |
| padding: 5px 10px; | |
| } | |
| .status-indicator span:last-child { | |
| display: none; | |
| } | |
| #input-area { | |
| padding: 12px; | |
| } | |
| .input-tools { | |
| gap: 6px; | |
| margin-bottom: 8px; | |
| overflow-x: auto; | |
| -webkit-overflow-scrolling: touch; | |
| } | |
| .input-tool-btn { | |
| padding: 6px 10px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| } | |
| .input-container { | |
| padding: 12px 14px; | |
| } | |
| #user-input { | |
| font-size: 14px; | |
| } | |
| #send-btn { | |
| min-width: 40px; | |
| height: 40px; | |
| padding: 8px; | |
| } | |
| .input-footer { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| gap: 8px; | |
| } | |
| .input-footer-left { | |
| font-size: 11px; | |
| } | |
| .input-footer-right { | |
| font-size: 11px; | |
| } | |
| .modal-content { | |
| padding: 24px 20px; | |
| width: 95%; | |
| } | |
| .modal-title { | |
| font-size: 20px; | |
| } | |
| .model-option { | |
| padding: 16px; | |
| } | |
| .model-option-name { | |
| font-size: 15px; | |
| } | |
| .model-option-size, | |
| .model-option-speed { | |
| font-size: 10px; | |
| padding: 3px 8px; | |
| } | |
| .model-option-desc { | |
| font-size: 13px; | |
| } | |
| .feature-tag { | |
| font-size: 10px; | |
| padding: 3px 8px; | |
| } | |
| .settings-section-title { | |
| font-size: 15px; | |
| } | |
| .setting-item { | |
| padding: 14px; | |
| } | |
| .setting-item-label { | |
| font-size: 14px; | |
| } | |
| .setting-item-desc { | |
| font-size: 12px; | |
| } | |
| .toast { | |
| right: 12px; | |
| bottom: 12px; | |
| max-width: calc(100% - 24px); | |
| padding: 12px 16px; | |
| } | |
| .toast-icon { | |
| font-size: 20px; | |
| } | |
| .toast-title { | |
| font-size: 13px; | |
| } | |
| .toast-message { | |
| font-size: 12px; | |
| } | |
| .history-item { | |
| padding: 10px; | |
| font-size: 13px; | |
| } | |
| .history-item-icon { | |
| font-size: 14px; | |
| } | |
| .history-item-meta { | |
| font-size: 10px; | |
| } | |
| .sidebar-footer-btn { | |
| padding: 10px; | |
| font-size: 13px; | |
| } | |
| } | |
| /* Tablet Responsive */ | |
| @media (min-width: 769px) and (max-width: 1024px) { | |
| :root { | |
| --sidebar-width: 260px; | |
| } | |
| .messages-wrapper { | |
| max-width: 700px; | |
| } | |
| .input-wrapper { | |
| max-width: 700px; | |
| } | |
| .example-prompts { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| /* Touch improvements */ | |
| @media (hover: none) and (pointer: coarse) { | |
| .history-item, | |
| .message-action-btn, | |
| .input-tool-btn, | |
| .top-bar-btn, | |
| .toggle-sidebar-btn, | |
| .model-option, | |
| .example-prompt, | |
| .new-chat-btn, | |
| .sidebar-footer-btn, | |
| #send-btn { | |
| -webkit-tap-highlight-color: transparent; | |
| touch-action: manipulation; | |
| } | |
| .history-item:active, | |
| .message-action-btn:active, | |
| .input-tool-btn:active, | |
| .top-bar-btn:active, | |
| .example-prompt:active { | |
| transform: scale(0.95); | |
| } | |
| /* Always show actions on mobile */ | |
| .history-item-actions { | |
| display: flex; | |
| } | |
| .message-actions { | |
| display: flex; | |
| } | |
| } | |
| /* Landscape mobile */ | |
| @media (max-width: 768px) and (orientation: landscape) { | |
| .welcome-screen { | |
| padding: 20px 16px; | |
| } | |
| .welcome-logo { | |
| font-size: 36px; | |
| margin-bottom: 12px; | |
| } | |
| .welcome-screen h1 { | |
| font-size: 20px; | |
| margin-bottom: 8px; | |
| } | |
| .welcome-screen p { | |
| font-size: 13px; | |
| margin-bottom: 20px; | |
| } | |
| .feature-pills { | |
| margin-bottom: 20px; | |
| } | |
| .example-prompts { | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 8px; | |
| } | |
| .example-prompt { | |
| padding: 12px; | |
| } | |
| } | |
| /* Small mobile (< 375px) */ | |
| @media (max-width: 374px) { | |
| .top-bar-btn { | |
| padding: 6px 8px; | |
| } | |
| .model-selector-compact i { | |
| display: none; | |
| } | |
| .input-tools { | |
| gap: 4px; | |
| } | |
| .input-tool-btn span { | |
| display: none; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <!-- Sidebar Backdrop for Mobile --> | |
| <div class="sidebar-backdrop" id="sidebar-backdrop"></div> | |
| <!-- Sidebar --> | |
| <div class="sidebar" id="sidebar"> | |
| <div class="sidebar-header"> | |
| <div class="logo-section"> | |
| <div class="logo"> | |
| <i class="bi bi-stars"></i> | |
| </div> | |
| <div class="logo-text">AI Chat Pro</div> | |
| </div> | |
| <button class="new-chat-btn" id="new-chat-btn"> | |
| <i class="bi bi-plus-circle-fill"></i> | |
| <span>Cuộc trò chuyện mới</span> | |
| </button> | |
| <div class="search-box"> | |
| <input type="text" id="search-history" placeholder="Tìm kiếm lịch sử..."> | |
| <i class="bi bi-search"></i> | |
| </div> | |
| </div> | |
| <div class="chat-history" id="chat-history"> | |
| <div class="history-group"> | |
| <div class="history-group-title"> | |
| <i class="bi bi-clock-history"></i> Hôm nay | |
| </div> | |
| <div id="today-chats"></div> | |
| </div> | |
| <div class="history-group"> | |
| <div class="history-group-title"> | |
| <i class="bi bi-calendar3"></i> Hôm qua | |
| </div> | |
| <div id="yesterday-chats"></div> | |
| </div> | |
| <div class="history-group"> | |
| <div class="history-group-title"> | |
| <i class="bi bi-calendar-week"></i> 7 ngày trước | |
| </div> | |
| <div id="week-chats"></div> | |
| </div> | |
| <div class="history-group"> | |
| <div class="history-group-title"> | |
| <i class="bi bi-archive"></i> Cũ hơn | |
| </div> | |
| <div id="older-chats"></div> | |
| </div> | |
| </div> | |
| <div class="sidebar-footer"> | |
| <button class="sidebar-footer-btn" id="export-btn"> | |
| <i class="bi bi-download"></i> | |
| <span>Xuất lịch sử</span> | |
| </button> | |
| <button class="sidebar-footer-btn" id="clear-history-btn"> | |
| <i class="bi bi-trash3"></i> | |
| <span>Xóa lịch sử</span> | |
| </button> | |
| <button class="sidebar-footer-btn" id="settings-btn"> | |
| <i class="bi bi-gear-fill"></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-cpu"></i> | |
| <span id="current-model-name">Qwen2.5 0.5B</span> | |
| <span class="model-badge">AI</span> | |
| <i class="bi bi-chevron-down" style="font-size: 12px;"></i> | |
| </div> | |
| </div> | |
| <div class="top-bar-center"> | |
| <div class="chat-title" id="chat-title"></div> | |
| </div> | |
| <div class="top-bar-right"> | |
| <button class="top-bar-btn" id="voice-btn" title="Voice Mode"> | |
| <i class="bi bi-mic"></i> | |
| </button> | |
| <button class="top-bar-btn" id="share-btn" title="Chia sẻ"> | |
| <i class="bi bi-share"></i> | |
| </button> | |
| <button class="top-bar-btn" id="theme-toggle" title="Chuyển theme"> | |
| <i class="bi bi-moon-stars-fill"></i> | |
| </button> | |
| <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-stars"></i> | |
| </div> | |
| <h1>Xin chào! Tôi có thể giúp gì?</h1> | |
| <p>Trợ lý AI chạy hoàn toàn trên trình duyệt - Riêng tư, Bảo mật & Nhanh chóng</p> | |
| <div class="feature-pills"> | |
| <div class="feature-pill"> | |
| <i class="bi bi-shield-check"></i> 100% Riêng tư | |
| </div> | |
| <div class="feature-pill"> | |
| <i class="bi bi-lightning-charge"></i> Không cần Server | |
| </div> | |
| <div class="feature-pill"> | |
| <i class="bi bi-stars"></i> AI Mạnh mẽ | |
| </div> | |
| </div> | |
| <div class="example-prompts"> | |
| <div class="example-prompt" data-prompt="Giải thích Machine Learning cho người mới bắt đầu với các ví dụ dễ hiểu"> | |
| <div class="example-prompt-icon">📚</div> | |
| <div class="example-prompt-title">Giải thích khái niệm</div> | |
| <div class="example-prompt-text">Machine Learning là gì? Ứng dụng thực tế?</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Viết một bài thơ lục bát về mùa thu Hà Nội với những hình ảnh đẹp"> | |
| <div class="example-prompt-icon">✍️</div> | |
| <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 Hà Nội</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Nếu tôi đầu tư 10 triệu đồng với lãi suất kép 8%/năm, sau 5 năm tôi sẽ có bao nhiêu tiền? Giải thích chi tiết"> | |
| <div class="example-prompt-icon">🧮</div> | |
| <div class="example-prompt-title">Tính toán phức tạp</div> | |
| <div class="example-prompt-text">Tính lãi suất kép đầu tư</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Hướng dẫn cách làm món phở gà ngon tại nhà, chi tiết từng bước"> | |
| <div class="example-prompt-icon">🍲</div> | |
| <div class="example-prompt-title">Hướng dẫn nấu ăn</div> | |
| <div class="example-prompt-text">Công thức phở gà đơn giản</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Viết code Python để tạo game rắn săn mồi đơn giản với Pygame"> | |
| <div class="example-prompt-icon">💻</div> | |
| <div class="example-prompt-title">Lập trình code</div> | |
| <div class="example-prompt-text">Tạo game rắn săn mồi</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Tư vấn lộ trình học lập trình web cho người mới bắt đầu"> | |
| <div class="example-prompt-icon">🎯</div> | |
| <div class="example-prompt-title">Tư vấn học tập</div> | |
| <div class="example-prompt-text">Lộ trình học lập trình web</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="input-area"> | |
| <div class="input-wrapper"> | |
| <div class="input-tools"> | |
| <button class="input-tool-btn" id="voice-input-btn"> | |
| <i class="bi bi-mic-fill"></i> | |
| <span>Voice</span> | |
| </button> | |
| <button class="input-tool-btn" id="attach-btn"> | |
| <i class="bi bi-paperclip"></i> | |
| <span>Đính kèm</span> | |
| </button> | |
| <button class="input-tool-btn" id="code-mode-btn"> | |
| <i class="bi bi-code-slash"></i> | |
| <span>Code</span> | |
| </button> | |
| </div> | |
| <div class="input-container"> | |
| <textarea | |
| id="user-input" | |
| placeholder="Nhắn tin cho AI..." | |
| rows="1" | |
| maxlength="4000" | |
| disabled | |
| ></textarea> | |
| <div class="input-actions"> | |
| <button class="input-action-btn" id="emoji-btn" title="Emoji"> | |
| <i class="bi bi-emoji-smile"></i> | |
| </button> | |
| <button id="send-btn" disabled> | |
| <i class="bi bi-send-fill"></i> | |
| </button> | |
| <button id="stop-btn"> | |
| <i class="bi bi-stop-circle-fill"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="input-footer"> | |
| <div class="input-footer-left"> | |
| AI có thể sai. Hãy kiểm tra thông tin quan trọng. | |
| </div> | |
| <div class="input-footer-right"> | |
| <div class="char-counter"> | |
| <span id="char-count">0</span>/4000 | |
| </div> | |
| </div> | |
| </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 AI 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> | |
| <div class="model-option-badges"> | |
| <span class="model-option-size">300MB</span> | |
| <span class="model-option-speed speed-fast">Rất nhanh</span> | |
| </div> | |
| </div> | |
| <div class="model-option-desc">Model nhẹ nhất, tốc độ cực nhanh, phù hợp chat thường ngày và thiết bị yếu</div> | |
| <div class="model-option-features"> | |
| <span class="feature-tag">⚡ Tải nhanh</span> | |
| <span class="feature-tag">💬 Chat tốt</span> | |
| <span class="feature-tag">📱 Thiết bị yếu</span> | |
| </div> | |
| </div> | |
| <div class="model-option" data-model="onnx-community/Qwen3-0.6B" data-name="Qwen3 0.6B"> | |
| <div class="model-option-header"> | |
| <span class="model-option-name">✨ Qwen3 0.6B</span> | |
| <div class="model-option-badges"> | |
| <span class="model-option-size">350MB</span> | |
| <span class="model-option-speed speed-fast">Rất nhanh</span> | |
| </div> | |
| </div> | |
| <div class="model-option-desc">Phiên bản mới nhất Qwen3, cải thiện hiệu suất, hỗ trợ tiếng Việt tốt hơn</div> | |
| <div class="model-option-features"> | |
| <span class="feature-tag">🆕 Mới nhất</span> | |
| <span class="feature-tag">🇻🇳 Tiếng Việt</span> | |
| <span class="feature-tag">⚡ Nhanh</span> | |
| </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> | |
| <div class="model-option-badges"> | |
| <span class="model-option-size">800MB</span> | |
| <span class="model-option-speed speed-medium">Nhanh</span> | |
| </div> | |
| </div> | |
| <div class="model-option-desc">Cân bằng hoàn hảo giữa tốc độ và chất lượng, phù hợp đa số tác vụ</div> | |
| <div class="model-option-features"> | |
| <span class="feature-tag">🎯 Cân bằng</span> | |
| <span class="feature-tag">💡 Thông minh</span> | |
| <span class="feature-tag">📝 Viết tốt</span> | |
| </div> | |
| </div> | |
| <div class="model-option" data-model="onnx-community/LFM-2.5-1.2B-Instruct" data-name="LiquidAI LFM 1.2B"> | |
| <div class="model-option-header"> | |
| <span class="model-option-name">💧 LiquidAI LFM 1.2B</span> | |
| <div class="model-option-badges"> | |
| <span class="model-option-size">700MB</span> | |
| <span class="model-option-speed speed-medium">Nhanh</span> | |
| </div> | |
| </div> | |
| <div class="model-option-desc">LiquidAI Foundation Model, kiến trúc mới, hiệu suất cao, xử lý ngữ cảnh dài</div> | |
| <div class="model-option-features"> | |
| <span class="feature-tag">🔬 Kiến trúc mới</span> | |
| <span class="feature-tag">📚 Ngữ cảnh dài</span> | |
| <span class="feature-tag">🎯 Chính xác</span> | |
| </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> | |
| <div class="model-option-badges"> | |
| <span class="model-option-size">2.4GB</span> | |
| <span class="model-option-speed speed-slow">Chậm</span> | |
| </div> | |
| </div> | |
| <div class="model-option-desc">Microsoft, cực kỳ thông minh, xử lý phức tạp tốt, cần RAM lớn (>8GB)</div> | |
| <div class="model-option-features"> | |
| <span class="feature-tag">🚀 Microsoft</span> | |
| <span class="feature-tag">🎓 Thông minh nhất</span> | |
| <span class="feature-tag">🔬 Phức tạp</span> | |
| </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> | |
| <div class="model-option-badges"> | |
| <span class="model-option-size">650MB</span> | |
| <span class="model-option-speed speed-medium">Nhanh</span> | |
| </div> | |
| </div> | |
| <div class="model-option-desc">Meta AI, hiệu suất cực tốt, đa ngôn ngữ, phù hợp nhiều tác vụ</div> | |
| <div class="model-option-features"> | |
| <span class="feature-tag">🌍 Meta AI</span> | |
| <span class="feature-tag">🗣️ Đa ngôn ngữ</span> | |
| <span class="feature-tag">⚡ Hiệu suất cao</span> | |
| </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> | |
| <div class="model-option-badges"> | |
| <span class="model-option-size">200MB</span> | |
| <span class="model-option-speed speed-fast">Siêu nhanh</span> | |
| </div> | |
| </div> | |
| <div class="model-option-desc">Siêu nhẹ, tải nhanh nhất, phù hợp mạng chậm hoặc tác vụ đơn giản</div> | |
| <div class="model-option-features"> | |
| <span class="feature-tag">🪶 Siêu nhẹ</span> | |
| <span class="feature-tag">📶 Mạng yếu</span> | |
| <span class="feature-tag">⚡ Tải nhanh</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Modal --> | |
| <div class="modal-overlay" id="settings-modal"> | |
| <div class="modal-content"> | |
| <div class="modal-header"> | |
| <h3 class="modal-title">Cài đặt</h3> | |
| <button class="modal-close" id="settings-modal-close">×</button> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-section-title"> | |
| <i class="bi bi-palette"></i> Giao diện | |
| </div> | |
| <div class="setting-item"> | |
| <div class="setting-item-info"> | |
| <div class="setting-item-label">Dark Mode</div> | |
| <div class="setting-item-desc">Chế độ tối bảo vệ mắt</div> | |
| </div> | |
| <div class="toggle-switch" id="dark-mode-toggle"></div> | |
| </div> | |
| <div class="setting-item"> | |
| <div class="setting-item-info"> | |
| <div class="setting-item-label">Animation</div> | |
| <div class="setting-item-desc">Hiệu ứng chuyển động</div> | |
| </div> | |
| <div class="toggle-switch active" id="animation-toggle"></div> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-section-title"> | |
| <i class="bi bi-chat-dots"></i> Chat | |
| </div> | |
| <div class="setting-item"> | |
| <div class="setting-item-info"> | |
| <div class="setting-item-label">Auto-save</div> | |
| <div class="setting-item-desc">Tự động lưu lịch sử</div> | |
| </div> | |
| <div class="toggle-switch active" id="autosave-toggle"></div> | |
| </div> | |
| <div class="setting-item"> | |
| <div class="setting-item-info"> | |
| <div class="setting-item-label">Sound Effects</div> | |
| <div class="setting-item-desc">Âm thanh thông báo</div> | |
| </div> | |
| <div class="toggle-switch" id="sound-toggle"></div> | |
| </div> | |
| </div> | |
| <div class="settings-section"> | |
| <div class="settings-section-title"> | |
| <i class="bi bi-info-circle"></i> Thông tin | |
| </div> | |
| <div class="setting-item"> | |
| <div class="setting-item-info"> | |
| <div class="setting-item-label">Phiên bản</div> | |
| <div class="setting-item-desc">AI Chat Pro v2.0.0</div> | |
| </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 sidebarBackdrop = document.getElementById('sidebar-backdrop'); | |
| 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 stopBtn = document.getElementById('stop-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'); | |
| const searchHistory = document.getElementById('search-history'); | |
| const charCount = document.getElementById('char-count'); | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const settingsModalClose = document.getElementById('settings-modal-close'); | |
| const exportBtn = document.getElementById('export-btn'); | |
| const shareBtn = document.getElementById('share-btn'); | |
| const chatTitle = document.getElementById('chat-title'); | |
| // State | |
| let generator = null; | |
| let isGenerating = false; | |
| let abortController = null; | |
| 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'; | |
| let settings = JSON.parse(localStorage.getItem('settings') || '{"darkMode": false, "autoSave": true, "sound": false, "animation": true}'); | |
| // Initialize | |
| loadChatHistory(); | |
| loadModel(selectedModel); | |
| applySettings(); | |
| // Auto-resize textarea | |
| userInput.addEventListener('input', function() { | |
| this.style.height = 'auto'; | |
| this.style.height = Math.min(this.scrollHeight, 200) + 'px'; | |
| charCount.textContent = this.value.length; | |
| }); | |
| // Sidebar toggle | |
| toggleSidebarBtn.addEventListener('click', () => { | |
| sidebar.classList.toggle('collapsed'); | |
| sidebarBackdrop.classList.toggle('show'); | |
| }); | |
| // Close sidebar when clicking backdrop | |
| sidebarBackdrop.addEventListener('click', () => { | |
| sidebar.classList.add('collapsed'); | |
| sidebarBackdrop.classList.remove('show'); | |
| }); | |
| // New chat | |
| newChatBtn.addEventListener('click', createNewChat); | |
| function createNewChat() { | |
| currentChatId = Date.now(); | |
| chatTitle.textContent = ''; | |
| chatContainer.querySelector('.messages-wrapper').innerHTML = ` | |
| <div class="welcome-screen"> | |
| <div class="welcome-logo"> | |
| <i class="bi bi-stars"></i> | |
| </div> | |
| <h1>Xin chào! Tôi có thể giúp gì?</h1> | |
| <p>Trợ lý AI chạy hoàn toàn trên trình duyệt - Riêng tư, Bảo mật & Nhanh chóng</p> | |
| <div class="feature-pills"> | |
| <div class="feature-pill"> | |
| <i class="bi bi-shield-check"></i> 100% Riêng tư | |
| </div> | |
| <div class="feature-pill"> | |
| <i class="bi bi-lightning-charge"></i> Không cần Server | |
| </div> | |
| <div class="feature-pill"> | |
| <i class="bi bi-stars"></i> AI Mạnh mẽ | |
| </div> | |
| </div> | |
| <div class="example-prompts"> | |
| <div class="example-prompt" data-prompt="Giải thích Machine Learning cho người mới bắt đầu với các ví dụ dễ hiểu"> | |
| <div class="example-prompt-icon">📚</div> | |
| <div class="example-prompt-title">Giải thích khái niệm</div> | |
| <div class="example-prompt-text">Machine Learning là gì? Ứng dụng thực tế?</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Viết một bài thơ lục bát về mùa thu Hà Nội với những hình ảnh đẹp"> | |
| <div class="example-prompt-icon">✍️</div> | |
| <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 Hà Nội</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Nếu tôi đầu tư 10 triệu đồng với lãi suất kép 8%/năm, sau 5 năm tôi sẽ có bao nhiêu tiền? Giải thích chi tiết"> | |
| <div class="example-prompt-icon">🧮</div> | |
| <div class="example-prompt-title">Tính toán phức tạp</div> | |
| <div class="example-prompt-text">Tính lãi suất kép đầu tư</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Hướng dẫn cách làm món phở gà ngon tại nhà, chi tiết từng bước"> | |
| <div class="example-prompt-icon">🍲</div> | |
| <div class="example-prompt-title">Hướng dẫn nấu ăn</div> | |
| <div class="example-prompt-text">Công thức phở gà đơn giản</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Viết code Python để tạo game rắn săn mồi đơn giản với Pygame"> | |
| <div class="example-prompt-icon">💻</div> | |
| <div class="example-prompt-title">Lập trình code</div> | |
| <div class="example-prompt-text">Tạo game rắn săn mồi</div> | |
| </div> | |
| <div class="example-prompt" data-prompt="Tư vấn lộ trình học lập trình web cho người mới bắt đầu"> | |
| <div class="example-prompt-icon">🎯</div> | |
| <div class="example-prompt-title">Tư vấn học tập</div> | |
| <div class="example-prompt-text">Lộ trình học lập trình web</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); | |
| showToast('success', 'Model đã chọn', `Đang tải ${selectedModelName}...`); | |
| }); | |
| }); | |
| // 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(); | |
| showToast('success', 'Thành công', `${selectedModelName} đã sẵn sàng!`); | |
| } catch (e) { | |
| updateStatus('error', 'Lỗi tải model'); | |
| showToast('error', 'Lỗi', 'Không thể tải model. Vui lòng thử lại.'); | |
| 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; | |
| sendBtn.style.display = 'none'; | |
| stopBtn.classList.add('show'); | |
| userInput.disabled = true; | |
| // Create abort controller for stopping generation | |
| abortController = new AbortController(); | |
| // Remove welcome screen | |
| const welcome = chatContainer.querySelector('.welcome-screen'); | |
| if (welcome) { | |
| chatContainer.querySelector('.messages-wrapper').innerHTML = ''; | |
| } | |
| // Set chat title | |
| if (!chatTitle.textContent) { | |
| chatTitle.textContent = text.substring(0, 30) + (text.length > 30 ? '...' : ''); | |
| } | |
| // Add user message | |
| addMessage('user', text); | |
| userInput.value = ''; | |
| userInput.style.height = 'auto'; | |
| charCount.textContent = '0'; | |
| // Add AI message placeholder with typing indicator | |
| const aiMessageEl = addMessage('ai', '', true); | |
| const contentDiv = aiMessageEl.querySelector('.message-content'); | |
| // Ensure content div is ready | |
| contentDiv.textContent = ''; | |
| // Save to chat history | |
| if (!currentChatId) { | |
| currentChatId = Date.now(); | |
| } | |
| // Update status | |
| updateStatus('loading', 'Đang suy nghĩ...'); | |
| try { | |
| let fullResponse = ''; | |
| let isFirstToken = true; | |
| // Create streamer with proper callback | |
| const streamer = new TextStreamer(generator.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (token) => { | |
| // Check if aborted | |
| if (abortController.signal.aborted) { | |
| throw new Error('Generation stopped by user'); | |
| } | |
| // Remove typing indicator on first token | |
| if (isFirstToken) { | |
| const typingIndicator = aiMessageEl.querySelector('.typing-indicator'); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| isFirstToken = false; | |
| } | |
| // Append token to content | |
| fullResponse += token; | |
| contentDiv.textContent = fullResponse; | |
| // Auto-scroll to bottom | |
| chatContainer.scrollTop = chatContainer.scrollHeight; | |
| }, | |
| }); | |
| // Prepare messages in chat format | |
| const messages = [{ role: "user", content: text }]; | |
| // Generate with streaming | |
| await generator(messages, { | |
| max_new_tokens: 512, | |
| streamer: streamer, | |
| temperature: 0.7, | |
| top_p: 0.95, | |
| do_sample: true, | |
| }); | |
| // Check if generation was aborted | |
| if (abortController.signal.aborted) { | |
| fullResponse += '\n\n[Đã dừng bởi người dùng]'; | |
| contentDiv.textContent = fullResponse; | |
| } | |
| // Save chat after completion | |
| if (settings.autoSave && fullResponse) { | |
| saveChat(text, fullResponse); | |
| } | |
| // Update status | |
| updateStatus('ready', 'Sẵn sàng'); | |
| // 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 like-btn"> | |
| <i class="bi bi-hand-thumbs-up"></i> Thích | |
| </button> | |
| <button class="message-action-btn dislike-btn"> | |
| <i class="bi bi-hand-thumbs-down"></i> Không thích | |
| </button> | |
| <button class="message-action-btn share-msg-btn"> | |
| <i class="bi bi-share"></i> Chia sẻ | |
| </button> | |
| <button class="message-action-btn regenerate-btn"> | |
| <i class="bi bi-arrow-clockwise"></i> Tạo lại | |
| </button> | |
| `; | |
| aiMessageEl.appendChild(actions); | |
| // Copy functionality | |
| actions.querySelector('.copy-btn').addEventListener('click', function() { | |
| navigator.clipboard.writeText(fullResponse); | |
| this.innerHTML = '<i class="bi bi-check-circle-fill"></i> Đã sao chép'; | |
| showToast('success', 'Đã sao chép', 'Nội dung đã được copy!'); | |
| setTimeout(() => { | |
| this.innerHTML = '<i class="bi bi-clipboard"></i> Sao chép'; | |
| }, 2000); | |
| }); | |
| // Like/Dislike | |
| actions.querySelector('.like-btn').addEventListener('click', function() { | |
| this.classList.toggle('active'); | |
| actions.querySelector('.dislike-btn').classList.remove('active'); | |
| if (this.classList.contains('active')) { | |
| showToast('success', 'Cảm ơn!', 'Phản hồi của bạn đã được ghi nhận'); | |
| } | |
| }); | |
| actions.querySelector('.dislike-btn').addEventListener('click', function() { | |
| this.classList.toggle('active'); | |
| actions.querySelector('.like-btn').classList.remove('active'); | |
| if (this.classList.contains('active')) { | |
| showToast('success', 'Cảm ơn!', 'Chúng tôi sẽ cải thiện'); | |
| } | |
| }); | |
| // Share message | |
| actions.querySelector('.share-msg-btn').addEventListener('click', function() { | |
| const shareText = `Câu hỏi: ${text}\n\nTrả lời: ${fullResponse}`; | |
| if (navigator.share) { | |
| navigator.share({ | |
| title: 'AI Chat', | |
| text: shareText | |
| }).catch(() => { | |
| navigator.clipboard.writeText(shareText); | |
| showToast('success', 'Đã sao chép', 'Nội dung chat đã được copy'); | |
| }); | |
| } else { | |
| navigator.clipboard.writeText(shareText); | |
| showToast('success', 'Đã sao chép', 'Nội dung chat đã được copy'); | |
| } | |
| }); | |
| // Regenerate | |
| actions.querySelector('.regenerate-btn').addEventListener('click', function() { | |
| // Remove AI message and regenerate | |
| aiMessageEl.remove(); | |
| chat(text); | |
| }); | |
| } catch (err) { | |
| console.error('Generation error:', err); | |
| // Remove typing indicator if still present | |
| const typingIndicator = aiMessageEl.querySelector('.typing-indicator'); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| if (err.message === 'Generation stopped by user') { | |
| contentDiv.textContent += '\n\n⏸️ [Đã dừng bởi người dùng]'; | |
| updateStatus('ready', 'Đã dừng'); | |
| showToast('warning', 'Đã dừng', 'Tạo phản hồi đã được dừng'); | |
| // Save partial response | |
| if (settings.autoSave && contentDiv.textContent) { | |
| saveChat(text, contentDiv.textContent); | |
| } | |
| } else { | |
| contentDiv.textContent = '❌ Lỗi: ' + err.message; | |
| updateStatus('error', 'Lỗi'); | |
| showToast('error', 'Lỗi', 'Không thể tạo phản hồi. Vui lòng thử lại.'); | |
| } | |
| } | |
| isGenerating = false; | |
| sendBtn.disabled = false; | |
| sendBtn.style.display = 'flex'; | |
| stopBtn.classList.remove('show'); | |
| userInput.disabled = false; | |
| userInput.focus(); | |
| abortController = null; | |
| } | |
| // Stop generation | |
| stopBtn.addEventListener('click', () => { | |
| if (abortController) { | |
| abortController.abort(); | |
| showToast('warning', 'Đang dừng...', 'Đang dừng tạo phản hồi'); | |
| } | |
| }); | |
| function addMessage(type, content, isTyping = false) { | |
| const messageEl = document.createElement('div'); | |
| messageEl.className = `message ${type}`; | |
| const avatar = type === 'user' | |
| ? '<i class="bi bi-person-circle"></i>' | |
| : '<i class="bi bi-stars"></i>'; | |
| const author = type === 'user' ? 'Bạn' : selectedModelName; | |
| const time = new Date().toLocaleTimeString('vi-VN', { hour: '2-digit', minute: '2-digit' }); | |
| messageEl.innerHTML = ` | |
| <div class="message-header"> | |
| <div class="message-avatar ${type}-avatar">${avatar}</div> | |
| <div class="message-author">${author}</div> | |
| <div class="message-time">${time}</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, time: Date.now() }, | |
| { role: 'ai', content: aiResponse, time: Date.now() } | |
| ); | |
| chat.updatedAt = Date.now(); | |
| chat.title = title; | |
| } else { | |
| chats.unshift({ | |
| id: currentChatId, | |
| title: title, | |
| messages: [ | |
| { role: 'user', content: userMessage, time: Date.now() }, | |
| { role: 'ai', content: aiResponse, time: Date.now() } | |
| ], | |
| createdAt: Date.now(), | |
| updatedAt: Date.now(), | |
| model: selectedModelName | |
| }); | |
| } | |
| 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'); | |
| const messageCount = chat.messages.length; | |
| item.innerHTML = ` | |
| <i class="history-item-icon bi bi-chat-left-text"></i> | |
| <div class="history-item-content"> | |
| <div class="history-item-text">${chat.title}</div> | |
| <div class="history-item-meta">${messageCount} tin nhắn • ${chat.model || 'AI'}</div> | |
| </div> | |
| <div class="history-item-actions"> | |
| <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; | |
| chatTitle.textContent = chat.title; | |
| chatContainer.querySelector('.messages-wrapper').innerHTML = ''; | |
| chat.messages.forEach(msg => { | |
| addMessage(msg.role, msg.content); | |
| }); | |
| loadChatHistory(); | |
| // Close sidebar on mobile | |
| if (window.innerWidth <= 768) { | |
| sidebar.classList.add('collapsed'); | |
| sidebarBackdrop.classList.remove('show'); | |
| } | |
| } | |
| 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(); | |
| showToast('success', 'Đã xóa', 'Cuộc trò chuyện đã bị xóa'); | |
| } | |
| } | |
| 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(); | |
| showToast('success', 'Đã xóa tất cả', 'Toàn bộ lịch sử đã bị xóa'); | |
| } | |
| }); | |
| // Search history | |
| searchHistory.addEventListener('input', (e) => { | |
| const query = e.target.value.toLowerCase(); | |
| document.querySelectorAll('.history-item').forEach(item => { | |
| const text = item.querySelector('.history-item-text').textContent.toLowerCase(); | |
| item.style.display = text.includes(query) ? 'flex' : 'none'; | |
| }); | |
| }); | |
| // Export chat | |
| exportBtn.addEventListener('click', () => { | |
| const dataStr = JSON.stringify(chats, null, 2); | |
| const dataBlob = new Blob([dataStr], { type: 'application/json' }); | |
| const url = URL.createObjectURL(dataBlob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = `ai-chat-history-${Date.now()}.json`; | |
| link.click(); | |
| showToast('success', 'Đã xuất', 'Lịch sử chat đã được tải xuống'); | |
| }); | |
| // Share current chat | |
| shareBtn.addEventListener('click', () => { | |
| const chat = chats.find(c => c.id === currentChatId); | |
| if (!chat) { | |
| showToast('warning', 'Không có gì', 'Chưa có cuộc trò chuyện để chia sẻ'); | |
| return; | |
| } | |
| const text = `${chat.title}\n\n${chat.messages.map(m => `${m.role === 'user' ? 'Tôi' : 'AI'}: ${m.content}`).join('\n\n')}`; | |
| if (navigator.share) { | |
| navigator.share({ | |
| title: chat.title, | |
| text: text | |
| }); | |
| } else { | |
| navigator.clipboard.writeText(text); | |
| showToast('success', 'Đã sao chép', 'Nội dung chat đã copy vào clipboard'); | |
| } | |
| }); | |
| // Theme toggle | |
| themeToggle.addEventListener('click', () => { | |
| settings.darkMode = !settings.darkMode; | |
| localStorage.setItem('settings', JSON.stringify(settings)); | |
| applySettings(); | |
| }); | |
| function applySettings() { | |
| if (settings.darkMode) { | |
| document.documentElement.setAttribute('data-theme', 'dark'); | |
| themeToggle.querySelector('i').className = 'bi bi-sun-fill'; | |
| } else { | |
| document.documentElement.removeAttribute('data-theme'); | |
| themeToggle.querySelector('i').className = 'bi bi-moon-stars-fill'; | |
| } | |
| } | |
| // Settings modal | |
| settingsBtn.addEventListener('click', () => { | |
| settingsModal.classList.add('show'); | |
| document.getElementById('dark-mode-toggle').classList.toggle('active', settings.darkMode); | |
| document.getElementById('autosave-toggle').classList.toggle('active', settings.autoSave); | |
| document.getElementById('sound-toggle').classList.toggle('active', settings.sound); | |
| document.getElementById('animation-toggle').classList.toggle('active', settings.animation); | |
| }); | |
| settingsModalClose.addEventListener('click', () => { | |
| settingsModal.classList.remove('show'); | |
| }); | |
| settingsModal.addEventListener('click', (e) => { | |
| if (e.target === settingsModal) { | |
| settingsModal.classList.remove('show'); | |
| } | |
| }); | |
| // Toggle switches | |
| document.querySelectorAll('.toggle-switch').forEach(toggle => { | |
| toggle.addEventListener('click', function() { | |
| this.classList.toggle('active'); | |
| const id = this.id; | |
| if (id === 'dark-mode-toggle') { | |
| settings.darkMode = this.classList.contains('active'); | |
| applySettings(); | |
| } else if (id === 'autosave-toggle') { | |
| settings.autoSave = this.classList.contains('active'); | |
| } else if (id === 'sound-toggle') { | |
| settings.sound = this.classList.contains('active'); | |
| } else if (id === 'animation-toggle') { | |
| settings.animation = this.classList.contains('active'); | |
| } | |
| localStorage.setItem('settings', JSON.stringify(settings)); | |
| }); | |
| }); | |
| // Toast notifications | |
| function showToast(type, title, message) { | |
| const toast = document.createElement('div'); | |
| toast.className = `toast ${type}`; | |
| const icons = { | |
| success: 'bi-check-circle-fill', | |
| error: 'bi-x-circle-fill', | |
| warning: 'bi-exclamation-triangle-fill' | |
| }; | |
| toast.innerHTML = ` | |
| <i class="toast-icon bi ${icons[type]}"></i> | |
| <div class="toast-content"> | |
| <div class="toast-title">${title}</div> | |
| <div class="toast-message">${message}</div> | |
| </div> | |
| `; | |
| document.body.appendChild(toast); | |
| setTimeout(() => { | |
| toast.style.animation = 'slideInRight 0.3s reverse'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // 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(); | |
| // Voice input (placeholder) | |
| document.getElementById('voice-input-btn').addEventListener('click', () => { | |
| showToast('warning', 'Sắp có', 'Tính năng Voice đang phát triển'); | |
| }); | |
| document.getElementById('voice-btn').addEventListener('click', () => { | |
| showToast('warning', 'Sắp có', 'Voice Mode đang phát triển'); | |
| }); | |
| // Code mode (placeholder) | |
| document.getElementById('code-mode-btn').addEventListener('click', function() { | |
| this.classList.toggle('active'); | |
| showToast('success', this.classList.contains('active') ? 'Bật' : 'Tắt', 'Code Mode ' + (this.classList.contains('active') ? 'đã bật' : 'đã tắt')); | |
| }); | |
| // Attach file (placeholder) | |
| document.getElementById('attach-btn').addEventListener('click', () => { | |
| showToast('warning', 'Sắp có', 'Tính năng đính kèm file đang phát triển'); | |
| }); | |
| // Emoji (placeholder) | |
| document.getElementById('emoji-btn').addEventListener('click', () => { | |
| showToast('warning', 'Sắp có', 'Bộ chọn emoji đang phát triển'); | |
| }); | |
| </script> | |
| </body> | |
| </html> |