Comp_App / index.html
Aigenthix's picture
Upload 5 files
a11c833 verified
Raw
History Blame Contribute Delete
23.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GroqChat</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d0d0f;
--surface: #18181b;
--surface-2: #1f1f23;
--border: #2a2a30;
--accent: #7c6af7;
--accent-glow: rgba(124,106,247,0.18);
--accent-2: #a78bfa;
--text: #e8e8ed;
--text-muted: #8b8b99;
--text-dim: #55555f;
--user-bubble: #1e1b38;
--user-border: #3d3580;
--success: #4ade80;
--radius: 14px;
--sidebar: 260px;
}
html, body { height: 100%; overflow: hidden; }
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
font-size: 14px;
line-height: 1.6;
}
/* ── Sidebar ── */
.sidebar {
width: var(--sidebar);
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
}
.sidebar-header {
padding: 20px 16px 12px;
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.logo-icon {
width: 32px; height: 32px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
border-radius: 8px;
display: grid;
place-items: center;
font-size: 16px;
flex-shrink: 0;
}
.logo-text {
font-size: 16px;
font-weight: 600;
letter-spacing: -0.3px;
color: var(--text);
}
.logo-text span { color: var(--accent-2); }
.new-chat-btn {
width: 100%;
padding: 9px 12px;
background: var(--accent-glow);
border: 1px solid var(--accent);
border-radius: 8px;
color: var(--accent-2);
font-size: 13px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background 0.15s, opacity 0.15s;
font-family: inherit;
}
.new-chat-btn:hover { background: rgba(124,106,247,0.28); }
/* Model selector */
.model-section { padding: 12px 16px; border-bottom: 1px solid var(--border); }
.model-label { font-size: 11px; font-weight: 500; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 8px; }
.model-option {
padding: 9px 11px;
border-radius: 8px;
cursor: pointer;
border: 1px solid transparent;
margin-bottom: 4px;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 10px;
}
.model-option:hover { background: var(--surface-2); }
.model-option.active {
background: var(--accent-glow);
border-color: var(--accent);
}
.model-icon { font-size: 16px; flex-shrink: 0; }
.model-info { min-width: 0; }
.model-name { font-size: 12px; font-weight: 500; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.model-desc { font-size: 11px; color: var(--text-muted); }
/* Conversation history */
.history-section { flex: 1; overflow-y: auto; padding: 12px 8px; }
.history-label { font-size: 11px; font-weight: 500; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.8px; padding: 0 8px; margin-bottom: 6px; }
.history-item {
padding: 8px 10px;
border-radius: 8px;
cursor: pointer;
color: var(--text-muted);
font-size: 12px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.15s;
border: 1px solid transparent;
}
.history-item:hover { background: var(--surface-2); color: var(--text); }
.history-item.active { background: var(--surface-2); border-color: var(--border); color: var(--text); }
.history-empty { padding: 0 10px; color: var(--text-dim); font-size: 12px; font-style: italic; }
/* Sidebar footer */
.sidebar-footer {
padding: 12px 16px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--text-dim);
text-align: center;
}
.sidebar-footer a { color: var(--accent); text-decoration: none; }
/* ── Main ── */
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
/* Top bar */
.topbar {
padding: 14px 24px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface);
flex-shrink: 0;
}
.topbar-model {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: var(--text-muted);
}
.topbar-model .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--success);
animation: pulse 2s infinite;
}
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
.topbar-actions { display: flex; gap: 8px; }
.icon-btn {
background: none;
border: 1px solid var(--border);
color: var(--text-muted);
border-radius: 7px;
padding: 6px 10px;
cursor: pointer;
font-size: 12px;
font-family: inherit;
transition: all 0.15s;
}
.icon-btn:hover { background: var(--surface-2); color: var(--text); }
/* Chat window */
.chat-window {
flex: 1;
overflow-y: auto;
padding: 32px 0;
scroll-behavior: smooth;
}
.chat-inner { max-width: 740px; margin: 0 auto; padding: 0 24px; }
/* Welcome */
.welcome {
text-align: center;
padding: 60px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.welcome-icon {
width: 64px; height: 64px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
border-radius: 18px;
display: grid;
place-items: center;
font-size: 32px;
box-shadow: 0 0 40px var(--accent-glow);
}
.welcome h1 { font-size: 26px; font-weight: 600; letter-spacing: -0.5px; }
.welcome h1 span { color: var(--accent-2); }
.welcome p { color: var(--text-muted); font-size: 14px; max-width: 380px; line-height: 1.7; }
.starter-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 8px; width: 100%; max-width: 500px; }
.starter-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px;
cursor: pointer;
text-align: left;
transition: all 0.15s;
font-family: inherit;
color: var(--text);
}
.starter-card:hover { border-color: var(--accent); background: var(--accent-glow); }
.starter-card .starter-icon { font-size: 18px; margin-bottom: 6px; display: block; }
.starter-card .starter-text { font-size: 12px; color: var(--text-muted); }
/* Messages */
.message { margin-bottom: 28px; display: flex; flex-direction: column; }
.message.user { align-items: flex-end; }
.message.assistant { align-items: flex-start; }
.msg-role {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.7px;
margin-bottom: 6px;
display: flex;
align-items: center;
gap: 6px;
}
.message.user .msg-role { color: var(--accent-2); }
.message.assistant .msg-role { color: var(--text-muted); }
.msg-avatar {
width: 20px; height: 20px;
border-radius: 5px;
display: grid;
place-items: center;
font-size: 10px;
}
.message.assistant .msg-avatar { background: linear-gradient(135deg, var(--accent), var(--accent-2)); }
.message.user .msg-avatar { background: var(--user-border); }
.msg-bubble {
max-width: 85%;
padding: 14px 18px;
border-radius: var(--radius);
line-height: 1.7;
font-size: 14px;
}
.message.user .msg-bubble {
background: var(--user-bubble);
border: 1px solid var(--user-border);
border-radius: var(--radius) var(--radius) 4px var(--radius);
}
.message.assistant .msg-bubble {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius) var(--radius) var(--radius) 4px;
width: 100%;
max-width: 100%;
}
.msg-bubble code {
font-family: 'JetBrains Mono', monospace;
background: rgba(124,106,247,0.1);
border: 1px solid rgba(124,106,247,0.2);
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
color: var(--accent-2);
}
.msg-bubble pre {
background: #111117;
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px;
overflow-x: auto;
margin: 10px 0;
}
.msg-bubble pre code {
background: none;
border: none;
padding: 0;
font-size: 13px;
color: #c9d1d9;
}
.msg-bubble p { margin-bottom: 10px; }
.msg-bubble p:last-child { margin-bottom: 0; }
.msg-bubble ul, .msg-bubble ol { padding-left: 20px; margin: 8px 0; }
.msg-bubble li { margin-bottom: 4px; }
.msg-bubble strong { color: var(--text); font-weight: 600; }
.msg-bubble h1,.msg-bubble h2,.msg-bubble h3 { font-weight: 600; margin: 14px 0 6px; color: var(--text); }
/* Typing cursor */
.cursor {
display: inline-block;
width: 2px; height: 16px;
background: var(--accent-2);
vertical-align: middle;
margin-left: 2px;
animation: blink 1s infinite;
border-radius: 1px;
}
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0} }
/* Input area */
.input-area {
padding: 16px 24px 20px;
background: var(--bg);
border-top: 1px solid var(--border);
flex-shrink: 0;
}
.input-inner { max-width: 740px; margin: 0 auto; }
.input-box {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 14px 16px;
display: flex;
align-items: flex-end;
gap: 10px;
transition: border-color 0.15s;
}
.input-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
#user-input {
flex: 1;
background: none;
border: none;
color: var(--text);
font-family: 'Inter', sans-serif;
font-size: 14px;
line-height: 1.6;
resize: none;
outline: none;
max-height: 200px;
min-height: 24px;
}
#user-input::placeholder { color: var(--text-dim); }
.send-btn {
width: 34px; height: 34px;
background: var(--accent);
border: none;
border-radius: 9px;
color: white;
cursor: pointer;
display: grid;
place-items: center;
flex-shrink: 0;
transition: all 0.15s;
font-size: 14px;
}
.send-btn:hover { background: var(--accent-2); transform: scale(1.05); }
.send-btn:disabled { background: var(--border); cursor: not-allowed; transform: none; }
.input-footer {
display: flex;
justify-content: center;
margin-top: 8px;
font-size: 11px;
color: var(--text-dim);
}
/* Scrollbar */
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
/* Mobile */
@media (max-width: 640px) {
.sidebar { display: none; }
.starter-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<div class="logo">
<div class="logo-icon"></div>
<div class="logo-text">Groq<span>Chat</span></div>
</div>
<button class="new-chat-btn" onclick="newChat()">
<span></span> New conversation
</button>
</div>
<div class="model-section">
<div class="model-label">Model</div>
{% for model_id, model in models.items() %}
<div class="model-option {% if loop.first %}active{% endif %}" data-model="{{ model_id }}" onclick="selectModel('{{ model_id }}', this)">
<span class="model-icon">{{ model.icon }}</span>
<div class="model-info">
<div class="model-name">{{ model.name }}</div>
<div class="model-desc">{{ model.description }}</div>
</div>
</div>
{% endfor %}
</div>
<div class="history-section">
<div class="history-label">History</div>
<div id="history-list">
<div class="history-empty">No conversations yet</div>
</div>
</div>
<div class="sidebar-footer">
Powered by <a href="https://groq.com" target="_blank">Groq</a> · Meta Llama
</div>
</aside>
<!-- Main -->
<main class="main">
<header class="topbar">
<div class="topbar-model">
<div class="dot"></div>
<span id="current-model-label">Llama 3.1 8B Instant</span>
</div>
<div class="topbar-actions">
<button class="icon-btn" onclick="newChat()">⊕ New chat</button>
<button class="icon-btn" onclick="copyLastResponse()">⎘ Copy</button>
</div>
</header>
<div class="chat-window" id="chat-window">
<div class="chat-inner" id="chat-inner">
<div class="welcome" id="welcome-screen">
<div class="welcome-icon"></div>
<h1>Welcome to <span>GroqChat</span></h1>
<p>Ultra-fast AI conversations powered by Groq's LPU inference. Choose a model and start chatting.</p>
<div class="starter-grid">
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">💡</span>
<div><strong>Explain a concept</strong></div>
<div class="starter-text">How does transformer attention work?</div>
</button>
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">✍️</span>
<div><strong>Write for me</strong></div>
<div class="starter-text">Draft a professional email declining a meeting</div>
</button>
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">🐛</span>
<div><strong>Debug code</strong></div>
<div class="starter-text">Help me find the bug in my Python code</div>
</button>
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">🌐</span>
<div><strong>Translate text</strong></div>
<div class="starter-text">Translate this paragraph to French</div>
</button>
</div>
</div>
</div>
</div>
<div class="input-area">
<div class="input-inner">
<div class="input-box">
<textarea id="user-input" placeholder="Message GroqChat…" rows="1" onkeydown="handleKey(event)" oninput="autoResize(this)"></textarea>
<button class="send-btn" id="send-btn" onclick="sendMessage()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</button>
</div>
<div class="input-footer">GroqChat can make mistakes. Verify important information.</div>
</div>
</div>
</main>
<script>
let messages = [];
let currentModel = "llama-3.1-8b-instant";
let modelLabels = {};
let conversations = [];
let currentConvId = null;
let isStreaming = false;
// Gather model labels from DOM
document.querySelectorAll('.model-option').forEach(el => {
const id = el.dataset.model;
const name = el.querySelector('.model-name').textContent.trim();
modelLabels[id] = name;
});
function selectModel(id, el) {
currentModel = id;
document.querySelectorAll('.model-option').forEach(o => o.classList.remove('active'));
el.classList.add('active');
document.getElementById('current-model-label').textContent = modelLabels[id] || id;
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 200) + 'px';
}
function handleKey(e) {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
}
function sendStarter(btn) {
const text = btn.querySelector('.starter-text').textContent.trim();
document.getElementById('user-input').value = text;
sendMessage();
}
function renderMarkdown(text) {
return text
.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) =>
`<pre><code class="language-${lang}">${escHtml(code.trim())}</code></pre>`)
.replace(/`([^`]+)`/g, '<code>$1</code>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^### (.+)$/gm, '<h3>$1</h3>')
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
.replace(/^\- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>\n?)+/g, s => `<ul>${s}</ul>`)
.replace(/\n\n+/g, '</p><p>')
.replace(/^(?!<[hup])(.+)$/gm, (m) => m.startsWith('<') ? m : m)
.replace(/^([^<\n].+)$/gm, '$1')
.split('\n').filter(l => l.trim()).join('\n');
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function addMessage(role, content, isNew = true) {
const welcome = document.getElementById('welcome-screen');
if (welcome) welcome.remove();
const inner = document.getElementById('chat-inner');
const div = document.createElement('div');
div.className = `message ${role}`;
const avatarIcon = role === 'assistant' ? '⚡' : 'U';
const roleLabel = role === 'assistant' ? 'GroqChat' : 'You';
div.innerHTML = `
<div class="msg-role">
<div class="msg-avatar">${avatarIcon}</div>
${roleLabel}
</div>
<div class="msg-bubble" id="msg-${Date.now()}">${role === 'assistant' ? '' : escHtml(content)}</div>
`;
inner.appendChild(div);
scrollToBottom();
return div.querySelector('.msg-bubble');
}
function scrollToBottom() {
const w = document.getElementById('chat-window');
w.scrollTop = w.scrollHeight;
}
async function sendMessage() {
if (isStreaming) return;
const input = document.getElementById('user-input');
const text = input.value.trim();
if (!text) return;
input.value = '';
input.style.height = 'auto';
document.getElementById('send-btn').disabled = true;
isStreaming = true;
messages.push({ role: 'user', content: text });
addMessage('user', text);
const assistantBubble = addMessage('assistant', '');
const cursor = document.createElement('span');
cursor.className = 'cursor';
assistantBubble.appendChild(cursor);
scrollToBottom();
let fullText = '';
try {
const res = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages, model: currentModel })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') break;
try {
const json = JSON.parse(data);
if (json.content) {
fullText += json.content;
assistantBubble.innerHTML = renderMarkdown(fullText);
assistantBubble.appendChild(cursor);
scrollToBottom();
}
} catch {}
}
}
}
} catch (err) {
fullText = '⚠️ Error connecting to the API. Please check your GROQ_API_KEY.';
}
cursor.remove();
assistantBubble.innerHTML = renderMarkdown(fullText) || '<em style="color:var(--text-dim)">No response.</em>';
messages.push({ role: 'assistant', content: fullText });
// Save to history
saveToHistory(text);
isStreaming = false;
document.getElementById('send-btn').disabled = false;
scrollToBottom();
}
function saveToHistory(firstMessage) {
if (!currentConvId) {
currentConvId = Date.now();
conversations.unshift({ id: currentConvId, title: firstMessage.slice(0, 40) + (firstMessage.length > 40 ? '…' : '') });
renderHistory();
}
}
function renderHistory() {
const list = document.getElementById('history-list');
if (conversations.length === 0) {
list.innerHTML = '<div class="history-empty">No conversations yet</div>';
return;
}
list.innerHTML = conversations.map(c =>
`<div class="history-item ${c.id === currentConvId ? 'active' : ''}" onclick="loadConv(${c.id})">${escHtml(c.title)}</div>`
).join('');
}
function loadConv(id) {
// For simplicity, just highlight; full persistence would need backend
currentConvId = id;
renderHistory();
}
function newChat() {
messages = [];
currentConvId = null;
const inner = document.getElementById('chat-inner');
inner.innerHTML = `
<div class="welcome" id="welcome-screen">
<div class="welcome-icon">⚡</div>
<h1>Welcome to <span>GroqChat</span></h1>
<p>Ultra-fast AI conversations powered by Groq's LPU inference. Choose a model and start chatting.</p>
<div class="starter-grid">
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">💡</span>
<div><strong>Explain a concept</strong></div>
<div class="starter-text">How does transformer attention work?</div>
</button>
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">✍️</span>
<div><strong>Write for me</strong></div>
<div class="starter-text">Draft a professional email declining a meeting</div>
</button>
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">🐛</span>
<div><strong>Debug code</strong></div>
<div class="starter-text">Help me find the bug in my Python code</div>
</button>
<button class="starter-card" onclick="sendStarter(this)">
<span class="starter-icon">🌐</span>
<div><strong>Translate text</strong></div>
<div class="starter-text">Translate this paragraph to French</div>
</button>
</div>
</div>`;
renderHistory();
}
function copyLastResponse() {
const bubbles = document.querySelectorAll('.message.assistant .msg-bubble');
if (bubbles.length === 0) return;
const last = bubbles[bubbles.length - 1];
navigator.clipboard.writeText(last.innerText).then(() => {
const btn = document.querySelector('.topbar-actions .icon-btn:last-child');
const old = btn.textContent;
btn.textContent = '✓ Copied';
setTimeout(() => btn.textContent = old, 1500);
});
}
</script>
</body>
</html>