xcvb / index.html
dzezzefezfz's picture
Update index.html
24f5035 verified
Raw
History Blame Contribute Delete
21 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Chat Assistant</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Markdown Parser -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Code Highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
gray: {
750: '#2d2d35',
850: '#1a1a20',
950: '#0d0d10',
}
}
}
}
}
</script>
<style>
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #565869;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #acacbe;
}
/* Typing Animation */
.typing-dot {
animation: typing 1.4s infinite ease-in-out both;
}
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes typing {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1); }
}
/* Markdown Styles */
.prose pre {
background-color: #0d0d10;
border-radius: 0.5rem;
padding: 1rem;
margin: 1rem 0;
overflow-x: auto;
}
.prose code {
background-color: rgba(255,255,255,0.1);
padding: 0.2rem 0.4rem;
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.9em;
}
.prose pre code {
background-color: transparent;
padding: 0;
border-radius: 0;
color: inherit;
}
.prose p {
margin-bottom: 1rem;
}
.prose p:last-child {
margin-bottom: 0;
}
.prose ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-bottom: 1rem;
}
.prose ol {
list-style-type: decimal;
padding-left: 1.5rem;
margin-bottom: 1rem;
}
</style>
</head>
<body class="bg-gray-800 text-gray-100 h-screen flex overflow-hidden font-sans antialiased">
<!-- Sidebar -->
<div id="sidebar" class="bg-gray-900 w-[260px] flex-shrink-0 flex flex-col transition-all duration-300 -ml-[260px] md:ml-0 border-r border-white/10 z-20 absolute md:relative h-full">
<!-- New Chat Button -->
<div class="p-3 flex-shrink-0">
<button onclick="startNewChat()" class="flex items-center gap-3 px-3 py-3 w-full text-sm text-white border border-white/20 rounded-md hover:bg-gray-800 transition-colors text-left">
<i class="fa-solid fa-plus"></i>
New chat
</button>
</div>
<!-- History List -->
<div class="flex-1 overflow-y-auto px-3 py-2 space-y-2" id="history-list">
<!-- History items will be injected here -->
<div class="text-xs font-medium text-gray-500 px-3 py-2">Today</div>
</div>
<!-- User / Settings -->
<div class="p-3 border-t border-white/10">
<button onclick="toggleSettings()" class="flex items-center gap-3 px-3 py-3 w-full text-sm text-white hover:bg-gray-800 rounded-md transition-colors text-left">
<i class="fa-solid fa-gear"></i>
Settings
</button>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col h-full relative bg-gray-800">
<!-- Mobile Header -->
<div class="md:hidden flex items-center p-4 border-b border-white/10 bg-gray-800 text-gray-100 z-10">
<button onclick="toggleSidebar()" class="text-gray-300 hover:text-white">
<i class="fa-solid fa-bars text-xl"></i>
</button>
<span class="ml-4 font-medium">New chat</span>
</div>
<!-- Chat Area -->
<div class="flex-1 overflow-y-auto scroll-smooth" id="chat-container">
<!-- Welcome Screen -->
<div id="welcome-screen" class="h-full flex flex-col items-center justify-center p-8 text-center">
<div class="bg-white/10 p-4 rounded-full mb-6">
<i class="fa-solid fa-robot text-4xl text-white"></i>
</div>
<h2 class="text-2xl font-semibold mb-2">How can I help you today?</h2>
</div>
<!-- Messages will be injected here -->
<div id="messages-list" class="flex flex-col pb-32 pt-4"></div>
</div>
<!-- Input Area -->
<div class="absolute bottom-0 left-0 w-full bg-gradient-to-t from-gray-800 via-gray-800 to-transparent pt-10 pb-6 px-4">
<div class="max-w-3xl mx-auto">
<div class="relative flex items-end gap-2 bg-[#40414f] border border-black/10 dark:border-gray-900/50 rounded-xl shadow-xs overflow-hidden ring-offset-2 focus-within:ring-2 ring-blue-500/50">
<textarea
id="user-input"
rows="1"
class="w-full max-h-[200px] py-3 pl-4 pr-12 bg-transparent border-0 text-white placeholder-gray-400 focus:ring-0 resize-none overflow-y-auto"
placeholder="Message ChatGPT..."
oninput="this.style.height = 'auto'; this.style.height = (this.scrollHeight) + 'px'"
onkeydown="handleEnter(event)"
></textarea>
<button
onclick="sendMessage()"
id="send-btn"
class="absolute right-2 bottom-2 p-2 rounded-md text-gray-400 hover:bg-black/50 hover:text-white disabled:opacity-40 disabled:hover:bg-transparent transition-colors"
disabled
>
<i class="fa-solid fa-paper-plane"></i>
</button>
</div>
<div class="text-center mt-2">
<p class="text-xs text-gray-500">AI can make mistakes. Consider checking important information.</p>
</div>
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settings-modal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center p-4">
<div class="bg-gray-900 rounded-xl w-full max-w-md border border-white/10 shadow-2xl transform transition-all scale-95 opacity-0" id="settings-content">
<div class="flex justify-between items-center p-4 border-b border-white/10">
<h3 class="text-lg font-medium">Settings</h3>
<button onclick="toggleSettings()" class="text-gray-400 hover:text-white">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Hugging Face API Key</label>
<input type="password" id="api-key-input" class="w-full bg-gray-800 border border-white/10 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="hf_...">
<p class="text-xs text-gray-500 mt-1">Required for the chatbot to function.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">Model ID</label>
<input type="text" id="model-input" class="w-full bg-gray-800 border border-white/10 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500" value="mistralai/Mistral-7B-Instruct-v0.2">
<p class="text-xs text-gray-500 mt-1">e.g., mistralai/Mistral-7B-Instruct-v0.2, HuggingFaceH4/zephyr-7b-beta</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-1">System Prompt</label>
<textarea id="system-prompt-input" rows="3" class="w-full bg-gray-800 border border-white/10 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500">You are a helpful AI assistant.</textarea>
</div>
</div>
<div class="p-4 border-t border-white/10 flex justify-end">
<button onclick="saveSettings()" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md transition-colors">Save</button>
</div>
</div>
</div>
<script>
// --- Configuration ---
const DEFAULT_MODEL = "mistralai/Mistral-7B-Instruct-v0.2";
let CONFIG = {
apiKey: localStorage.getItem('hf_api_key') || '',
model: localStorage.getItem('hf_model') || DEFAULT_MODEL,
systemPrompt: localStorage.getItem('hf_system_prompt') || "You are a helpful AI assistant."
};
// --- State ---
let messages = [];
let isGenerating = false;
// --- Initialization ---
document.addEventListener('DOMContentLoaded', () => {
// Load settings into inputs
document.getElementById('api-key-input').value = CONFIG.apiKey;
document.getElementById('model-input').value = CONFIG.model;
document.getElementById('system-prompt-input').value = CONFIG.systemPrompt;
// Check if API key is missing
if (!CONFIG.apiKey) {
toggleSettings();
}
// Input listener
const input = document.getElementById('user-input');
input.addEventListener('input', () => {
const btn = document.getElementById('send-btn');
btn.disabled = input.value.trim() === '';
if (input.value.trim() !== '') {
btn.classList.add('bg-green-600', 'text-white');
btn.classList.remove('text-gray-400');
} else {
btn.classList.remove('bg-green-600', 'text-white');
btn.classList.add('text-gray-400');
}
});
});
// --- UI Functions ---
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
sidebar.classList.toggle('-ml-[260px]');
}
function toggleSettings() {
const modal = document.getElementById('settings-modal');
const content = document.getElementById('settings-content');
if (modal.classList.contains('hidden')) {
modal.classList.remove('hidden');
// Small delay for animation
setTimeout(() => {
content.classList.remove('scale-95', 'opacity-0');
content.classList.add('scale-100', 'opacity-100');
}, 10);
} else {
content.classList.remove('scale-100', 'opacity-100');
content.classList.add('scale-95', 'opacity-0');
setTimeout(() => {
modal.classList.add('hidden');
}, 300);
}
}
function saveSettings() {
const newKey = document.getElementById('api-key-input').value.trim();
const newModel = document.getElementById('model-input').value.trim();
const newPrompt = document.getElementById('system-prompt-input').value.trim();
if (newKey) {
localStorage.setItem('hf_api_key', newKey);
CONFIG.apiKey = newKey;
}
if (newModel) {
localStorage.setItem('hf_model', newModel);
CONFIG.model = newModel;
}
if (newPrompt) {
localStorage.setItem('hf_system_prompt', newPrompt);
CONFIG.systemPrompt = newPrompt;
}
toggleSettings();
alert('Settings saved!');
}
function handleEnter(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function startNewChat() {
messages = [];
document.getElementById('messages-list').innerHTML = '';
document.getElementById('welcome-screen').classList.remove('hidden');
document.getElementById('user-input').value = '';
document.getElementById('user-input').style.height = 'auto';
}
function appendMessage(role, content) {
const list = document.getElementById('messages-list');
const welcome = document.getElementById('welcome-screen');
if (!welcome.classList.contains('hidden')) {
welcome.classList.add('hidden');
}
const isUser = role === 'user';
const avatar = isUser
? '<div class="w-8 h-8 bg-gray-600 rounded-sm flex items-center justify-center"><i class="fa-solid fa-user text-white"></i></div>'
: '<div class="w-8 h-8 bg-green-500 rounded-sm flex items-center justify-center"><i class="fa-solid fa-robot text-white"></i></div>';
const bgClass = isUser ? '' : 'bg-[#444654]';
const msgDiv = document.createElement('div');
msgDiv.className = `w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group ${bgClass}`;
// Render Markdown
const renderedContent = marked.parse(content);
msgDiv.innerHTML = `
<div class="text-base gap-4 md:gap-6 md:max-w-3xl lg:max-w-[40rem] xl:max-w-[48rem] p-4 md:py-6 flex lg:px-0 m-auto">
<div class="flex-shrink-0 flex flex-col relative items-end">
${avatar}
</div>
<div class="relative flex-1 prose prose-invert max-w-none break-words">
${renderedContent}
</div>
</div>
`;
list.appendChild(msgDiv);
// Highlight code blocks
msgDiv.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
// Scroll to bottom
const container = document.getElementById('chat-container');
container.scrollTop = container.scrollHeight;
return msgDiv;
}
function showTypingIndicator() {
const list = document.getElementById('messages-list');
const div = document.createElement('div');
div.id = 'typing-indicator';
div.className = 'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 bg-[#444654]';
div.innerHTML = `
<div class="text-base gap-4 md:gap-6 md:max-w-3xl lg:max-w-[40rem] xl:max-w-[48rem] p-4 md:py-6 flex lg:px-0 m-auto">
<div class="flex-shrink-0 flex flex-col relative items-end">
<div class="w-8 h-8 bg-green-500 rounded-sm flex items-center justify-center"><i class="fa-solid fa-robot text-white"></i></div>
</div>
<div class="relative flex-1 flex items-center gap-1">
<div class="w-2 h-2 bg-gray-400 rounded-full typing-dot"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full typing-dot"></div>
<div class="w-2 h-2 bg-gray-400 rounded-full typing-dot"></div>
</div>
</div>
`;
list.appendChild(div);
const container = document.getElementById('chat-container');
container.scrollTop = container.scrollHeight;
}
function removeTypingIndicator() {
const indicator = document.getElementById('typing-indicator');
if (indicator) indicator.remove();
}
// --- API Interaction ---
async function sendMessage() {
if (isGenerating) return;
const input = document.getElementById('user-input');
const text = input.value.trim();
if (!text) return;
if (!CONFIG.apiKey) {
alert('Please set your Hugging Face API Key in settings first.');
toggleSettings();
return;
}
// UI Updates
input.value = '';
input.style.height = 'auto';
document.getElementById('send-btn').disabled = true;
document.getElementById('send-btn').classList.remove('bg-green-600', 'text-white');
// Add User Message
messages.push({ role: 'user', content: text });
appendMessage('user', text);
// Show Loading
isGenerating = true;
showTypingIndicator();
try {
// Prepare prompt
let prompt = "<s>";
for (let i = 0; i < messages.length; i++) {
const msg = messages[i];
if (msg.role === 'user') {
if (i === 0) {
prompt += `[INST] ${CONFIG.systemPrompt}\n\n${msg.content} [/INST]`;
} else {
prompt += `[INST] ${msg.content} [/INST]`;
}
} else {
prompt += ` ${msg.content} </s>`;
}
}
const response = await fetch(`https://api-inference.huggingface.co/models/${CONFIG.model}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${CONFIG.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
inputs: prompt,
parameters: {
max_new_tokens: 1024,
temperature: 0.7,
top_p: 0.95,
return_full_text: false,
stream: false
}
})
});
if (!response.ok) {
let errorMsg = `API Error: ${response.status} ${response.statusText}`;
try {
const errorData = await response.json();
if (errorData.error) {
errorMsg += `\nDetails: ${errorData.error}`;
}
} catch (e) {
// Ignore JSON parse error
}
throw new Error(errorMsg);
}
const data = await response.json();
let botText = '';
if (Array.isArray(data) && data.length > 0) {
botText = data[0].generated_text;
} else if (data.generated_text) {
botText = data.generated_text;
} else {
botText = "Error: No response from model.";
}
removeTypingIndicator();
messages.push({ role: 'assistant', content: botText });
appendMessage('assistant', botText);
} catch (error) {
console.error(error);
removeTypingIndicator();
appendMessage('assistant', `**Error:** ${error.message}\n\n**Troubleshooting:**\n1. Check your API Key in Settings.\n2. Ensure your token has 'Read' permissions.\n3. Try a different model ID (e.g., 'HuggingFaceH4/zephyr-7b-beta').`);
} finally {
isGenerating = false;
}
}
</script>
</body>
</html>