TransformerWebs / index.html
Accordic's picture
Update index.html
5604590 verified
<!DOCTYPE html>
<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>