Nive-AI-0.1beta-lite / index.html
AInive's picture
Create index.html
e3ff546 verified
Raw
History Blame Contribute Delete
16.6 kB
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI-Nive Professional</title>
<style>
:root {
--bg-main: #f8f8f8; --bg-chat: #ffffff; --bg-sidebar: #f0f0f0;
--text-main: #333333; --text-muted: #666666;
--accent: #da7756; --accent-hover: #c46445;
--border: #e5e5e5; --user-msg: #f3f4f6; --ai-msg: #ffffff;
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-main: #1a1a1a; --bg-chat: #2b2b2b; --bg-sidebar: #1e1e1e;
--text-main: #e0e0e0; --text-muted: #999999;
--border: #3a3a3a; --user-msg: #3b3b3b; --ai-msg: #2b2b2b;
}
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: var(--font-main); }
body { display: flex; height: 100vh; background: var(--bg-main); color: var(--text-main); overflow: hidden; }
/* Sidebar */
#sidebar { width: 260px; background: var(--bg-sidebar); border-right: 1px solid var(--border); display: flex; flex-direction: column; transition: transform 0.3s ease; z-index: 100; }
.sidebar-header { padding: 20px; border-bottom: 1px solid var(--border); }
.new-chat-btn { width: 100%; padding: 10px; background: var(--accent); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 14px; }
.new-chat-btn:hover { background: var(--accent-hover); }
#history-list { flex: 1; overflow-y: auto; padding: 10px; }
.history-item { padding: 10px; border-radius: 6px; cursor: pointer; margin-bottom: 5px; font-size: 14px; display: flex; justify-content: space-between; align-items: center; color: var(--text-main); }
.history-item:hover, .history-item.active { background: rgba(0,0,0,0.05); }
@media (prefers-color-scheme: dark) { .history-item:hover, .history-item.active { background: rgba(255,255,255,0.05); } }
.history-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 170px; }
.history-actions button { background: none; border: none; color: var(--text-muted); cursor: pointer; padding: 2px 5px; font-size: 12px; }
.history-actions button:hover { color: var(--accent); }
/* Main Chat Area */
#main-area { flex: 1; display: flex; flex-direction: column; background: var(--bg-chat); position: relative; }
#top-bar { height: 60px; border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 20px; justify-content: space-between; }
.toggle-group { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 500; }
/* Web Search Toggle */
.switch { position: relative; display: inline-block; width: 44px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; }
.slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; }
input:checked + .slider { background-color: var(--accent); }
input:checked + .slider:before { transform: translateX(20px); }
/* Chat Container */
#chat-container { flex: 1; overflow-y: auto; padding: 20px 15%; display: flex; flex-direction: column; gap: 20px; scroll-behavior: smooth; }
.message { display: flex; gap: 15px; max-width: 100%; line-height: 1.6; }
.message.user { justify-content: flex-end; }
.message.user .msg-content { background: var(--user-msg); border-radius: 12px 12px 0 12px; padding: 12px 16px; max-width: 80%; border: 1px solid var(--border); }
.message.ai .msg-content { background: var(--ai-msg); border-radius: 12px 12px 12px 0; padding: 12px 16px; max-width: 100%; flex: 1; }
.avatar { width: 32px; height: 32px; border-radius: 4px; display: flex; align-items: center; justify-content: center; font-weight: bold; flex-shrink: 0; }
.user-av { background: #333; color: white; }
.ai-av { background: var(--accent); color: white; }
/* Code Blocks & Files */
pre { background: #1e1e1e; color: #d4d4d4; padding: 15px; border-radius: 8px; overflow-x: auto; margin: 10px 0; font-family: 'Courier New', Courier, monospace; font-size: 13px; position: relative; }
.code-header { background: #2d2d2d; padding: 5px 10px; font-size: 12px; color: #aaa; border-top-left-radius: 8px; border-top-right-radius: 8px; margin-bottom: -10px; display: flex; justify-content: space-between; }
.download-btn { background: var(--accent); color: white; border: none; border-radius: 4px; cursor: pointer; padding: 2px 8px; font-size: 11px; }
/* Input Area */
#input-area { padding: 20px 15%; border-top: 1px solid var(--border); background: var(--bg-chat); }
.input-box { border: 1px solid var(--border); border-radius: 12px; background: var(--bg-main); display: flex; flex-direction: column; padding: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
#file-preview { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 5px; }
.file-chip { background: var(--bg-chat); border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; font-size: 12px; display: flex; align-items: center; gap: 5px; }
.file-chip button { background: none; border: none; color: red; cursor: pointer; font-weight: bold; }
.input-controls { display: flex; align-items: center; gap: 10px; }
textarea { flex: 1; border: none; background: transparent; color: var(--text-main); resize: none; outline: none; padding: 8px; max-height: 150px; overflow-y: auto; font-size: 15px; }
.action-btn { background: transparent; border: none; color: var(--text-muted); cursor: pointer; padding: 8px; border-radius: 6px; display: flex; align-items: center; justify-content: center; transition: 0.2s; }
.action-btn:hover { background: rgba(0,0,0,0.05); color: var(--accent); }
.send-btn { background: var(--accent); color: white; border-radius: 8px; padding: 8px 16px; font-weight: 600; }
.send-btn:hover { background: var(--accent-hover); color: white; }
#loading { display: none; align-items: center; gap: 5px; font-size: 13px; color: var(--accent); padding: 10px; }
.dot { width: 6px; height: 6px; background: var(--accent); border-radius: 50%; animation: bounce 1.4s infinite ease-in-out both; }
.dot:nth-child(1) { animation-delay: -0.32s; }
.dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes bounce { 0%, 80%, 100% { transform: scale(0); } 40% { transform: scale(1); } }
</style>
</head>
<body>
<div id="sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="startNewChat()">+ Chat mới</button>
</div>
<div id="history-list"></div>
</div>
<div id="main-area">
<div id="top-bar">
<div style="font-weight: 600; letter-spacing: 0.5px;">AI-Nive <span style="font-size:12px; color:var(--accent)">v1.0 Pro</span></div>
<div class="toggle-group">
<label>Web Search</label>
<label class="switch">
<input type="checkbox" id="web-search-toggle" checked>
<span class="slider"></span>
</label>
</div>
</div>
<div id="chat-container">
</div>
<div id="loading">
<div class="dot"></div><div class="dot"></div><div class="dot"></div>
<span>Nive đang suy nghĩ...</span>
</div>
<div id="input-area">
<div class="input-box">
<div id="file-preview"></div>
<div class="input-controls">
<input type="file" id="file-input" style="display: none;" multiple accept=".txt,.py,.js,.html,.css,.json,.png,.jpg,.jpeg,.zip">
<button class="action-btn" onclick="document.getElementById('file-input').click()" title="Đính kèm file">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"></path></svg>
</button>
<textarea id="user-input" rows="1" placeholder="Nhắn tin cho Nive... (Enter để gửi, Shift+Enter xuống dòng)"></textarea>
<button class="action-btn send-btn" onclick="sendMessage()">Gửi ➔</button>
</div>
</div>
</div>
</div>
<script>
// System Core LocalStorage
let chatHistory = JSON.parse(localStorage.getItem('nive_chats')) || [];
let currentChatId = null;
let pendingFiles = [];
const chatContainer = document.getElementById('chat-container');
const userInput = document.getElementById('user-input');
const fileInput = document.getElementById('file-input');
const filePreview = document.getElementById('file-preview');
const historyList = document.getElementById('history-list');
// Khởi tạo
window.onload = () => {
renderHistory();
if(chatHistory.length > 0) { loadChat(chatHistory[0].id); }
else { startNewChat(); }
};
// Quản lý lịch sử
function startNewChat() {
currentChatId = Date.now().toString();
chatHistory.unshift({ id: currentChatId, title: "Cuộc hội thoại mới", messages: [], pinned: false });
saveHistory();
renderHistory();
chatContainer.innerHTML = '';
addMessage('ai', 'Xin chào! Tôi là AI-Nive. Tôi có thể đọc code, phân tích ảnh, nén/giải nén file zip và tra cứu Internet. Bạn cần giúp gì?');
}
function saveHistory() { localStorage.setItem('nive_chats', JSON.stringify(chatHistory)); }
function renderHistory() {
historyList.innerHTML = '';
// Sắp xếp: ghim lên đầu
chatHistory.sort((a, b) => (b.pinned === a.pinned) ? 0 : a.pinned ? 1 : -1);
chatHistory.forEach(chat => {
let div = document.createElement('div');
div.className = `history-item ${chat.id === currentChatId ? 'active' : ''}`;
div.innerHTML = `
<div class="history-title" onclick="loadChat('${chat.id}')">
${chat.pinned ? '📌 ' : ''}${chat.title}
</div>
<div class="history-actions">
<button onclick="togglePin('${chat.id}')">📍</button>
<button onclick="deleteChat('${chat.id}')">🗑️</button>
</div>
`;
historyList.appendChild(div);
});
}
function loadChat(id) {
currentChatId = id;
renderHistory();
const chat = chatHistory.find(c => c.id === id);
chatContainer.innerHTML = '';
chat.messages.forEach(m => renderMessageUI(m.role, m.content, false));
scrollToBottom();
}
function togglePin(id) {
let chat = chatHistory.find(c => c.id === id);
chat.pinned = !chat.pinned;
saveHistory(); renderHistory();
}
function deleteChat(id) {
chatHistory = chatHistory.filter(c => c.id !== id);
saveHistory(); renderHistory();
if(currentChatId === id) startNewChat();
}
// Xử lý Input & File
userInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight) + 'px';
});
userInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
fileInput.addEventListener('change', function(e) {
for(let file of this.files) {
pendingFiles.push(file);
let chip = document.createElement('div');
chip.className = 'file-chip';
chip.innerHTML = `📎 ${file.name} <button onclick="removeFile(this, '${file.name}')">×</button>`;
filePreview.appendChild(chip);
}
});
function removeFile(btn, name) {
pendingFiles = pendingFiles.filter(f => f.name !== name);
btn.parentElement.remove();
}
// Xử lý gửi tin nhắn
async function sendMessage() {
const text = userInput.value.trim();
if(!text && pendingFiles.length === 0) return;
let userMsgContent = text;
if(pendingFiles.length > 0) {
userMsgContent += `<br><small style="color:var(--accent)">[Đã đính kèm ${pendingFiles.length} file]</small>`;
}
addMessage('user', userMsgContent);
userInput.value = ''; userInput.style.height = 'auto';
document.getElementById('loading').style.display = 'flex';
// Chuẩn bị FormData gửi lên API FastAPI
let formData = new FormData();
formData.append("message", text);
formData.append("web_search", document.getElementById('web-search-toggle').checked);
// Lấy lịch sử ngữ cảnh
let chat = chatHistory.find(c => c.id === currentChatId);
let historyContext = chat.messages.slice(-5); // Gửi 5 tin gần nhất để tiết kiệm token
formData.append("history", JSON.stringify(historyContext));
for(let file of pendingFiles) { formData.append("files", file); }
pendingFiles = []; filePreview.innerHTML = '';
try {
// GỌI API BACKEND (Cùng domain trên Hugging Face)
const response = await fetch('/api/chat', { method: 'POST', body: formData });
const data = await response.json();
if(data.error) throw new Error(data.error);
addMessage('ai', formatAIResponse(data.reply));
// Cập nhật tiêu đề tự động
if(chat.messages.length === 2) {
chat.title = text.substring(0, 30) + "...";
saveHistory(); renderHistory();
}
} catch (error) {
addMessage('ai', `⚠️ Lỗi hệ thống: ${error.message}`);
} finally {
document.getElementById('loading').style.display = 'none';
}
}
function addMessage(role, content) {
renderMessageUI(role, content, true);
let chat = chatHistory.find(c => c.id === currentChatId);
chat.messages.push({ role: role, content: content });
saveHistory();
}
function renderMessageUI(role, content, animate) {
let div = document.createElement('div');
div.className = `message ${role}`;
let av = role === 'user' ? '<div class="avatar user-av">U</div>' : '<div class="avatar ai-av">N</div>';
div.innerHTML = role === 'user'
? `<div class="msg-content">${content}</div>${av}`
: `${av}<div class="msg-content">${content}</div>`;
chatContainer.appendChild(div);
scrollToBottom();
}
function formatAIResponse(text) {
// Tự động chuyển đổi các khối code thành UI có nút tải xuống
const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
return text.replace(codeBlockRegex, (match, lang, code) => {
lang = lang || 'text';
let ext = lang === 'python' ? 'py' : lang === 'javascript' ? 'js' : lang === 'html' ? 'html' : 'txt';
let filename = `nive_code.${ext}`;
let encodedCode = encodeURIComponent(code);
return `
<div class="code-header">
<span>${lang.toUpperCase()}</span>
<a href="data:text/plain;charset=utf-8,${encodedCode}" download="${filename}" class="download-btn">Tải file .${ext}</a>
</div>
<pre><code>${code.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>`;
}).replace(/\n/g, '<br>');
}
function scrollToBottom() { chatContainer.scrollTop = chatContainer.scrollHeight; }
</script>
</body>
</html>