StemGraph_AI / stem.html
Subh775's picture
sqlite added, improvements..
094af1a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>STEMbotix AI Chat</title>
<link rel="icon" type="image/png" href="assets/stem_black.png">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"></script>
<style>
:root {
/* Branding colors */
--brand-orange: #EB5A28;
--brand-orange-hover: #cf4f22;
/* Lights Out Dark Mode Palette */
--bg-main: #000000;
--bg-sidebar: #070707;
--bg-input: #121212;
--bg-hover: #1A1A1A;
--text-primary: #EDEDED;
--text-secondary: #777777;
--border-color: #222222;
--msg-user: #141414;
--msg-ai: transparent;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Montserrat', sans-serif;
background-color: var(--bg-main);
color: var(--text-primary);
display: flex;
height: 100vh;
overflow: hidden;
}
/* --- Sidebar Styles --- */
.sidebar {
width: 260px;
flex-shrink: 0;
background-color: var(--bg-sidebar);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color);
transition: margin-left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
z-index: 20;
}
.sidebar.collapsed {
margin-left: -260px;
}
.sidebar-header {
padding: 20px;
display: flex;
justify-content: center;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.brand-logo {
max-width: 80%;
height: auto;
object-fit: contain;
}
.new-chat-btn {
margin: 20px 15px;
padding: 12px;
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
font-weight: 500;
font-family: 'Montserrat', sans-serif;
transition: background-color 0.2s, border-color 0.2s;
}
.new-chat-btn:hover {
background-color: var(--bg-hover);
}
/* --- Chat History & Options Menu --- */
.chat-history {
flex-grow: 1;
overflow-y: auto;
padding: 0 15px;
display: flex;
flex-direction: column;
gap: 2px;
}
.history-item {
position: relative;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
color: var(--text-secondary);
font-size: 14px;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s, color 0.2s;
}
.history-item:hover, .history-item.active {
background-color: var(--bg-hover);
color: var(--text-primary);
}
.chat-title {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 10px;
}
.options-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 18px;
line-height: 1;
cursor: pointer;
opacity: 0;
padding: 0 4px;
transition: opacity 0.2s, color 0.2s;
}
.history-item:hover .options-btn,
.options-btn.menu-open {
opacity: 1;
}
.options-btn:hover {
color: var(--text-primary);
}
.options-menu {
position: absolute;
right: 10px;
top: 35px;
background-color: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 8px;
display: none;
flex-direction: column;
z-index: 100;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.8);
min-width: 120px;
overflow: hidden;
}
.options-menu.show {
display: flex;
}
.option-item {
padding: 10px 15px;
font-size: 13px;
color: var(--text-primary);
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: background-color 0.2s;
}
.option-item:hover {
background-color: var(--bg-hover);
}
.option-item.delete {
color: #ff4a4a;
}
.rename-input {
width: 100%;
background: transparent;
border: none;
color: var(--text-primary);
font-family: inherit;
font-size: 14px;
outline: none;
border-bottom: 1px solid var(--brand-orange);
padding: 2px 0;
}
.sidebar-footer {
padding: 15px;
border-top: 1px solid var(--border-color);
}
.settings-btn {
display: flex;
align-items: center;
gap: 12px;
color: var(--text-primary);
cursor: pointer;
padding: 10px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
transition: background-color 0.2s;
}
.settings-btn:hover {
background-color: var(--bg-hover);
}
/* --- Main Chat Area Styles --- */
.main-chat {
flex-grow: 1;
display: flex;
flex-direction: column;
position: relative;
}
/* Header for Sidebar Toggle */
.main-header {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 15px 20px;
display: flex;
align-items: center;
z-index: 10;
}
.toggle-sidebar-btn {
background: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
border-radius: 8px;
transition: background-color 0.2s, color 0.2s;
}
.toggle-sidebar-btn:hover {
background-color: var(--bg-hover);
color: var(--text-primary);
}
.chat-container {
flex-grow: 1;
overflow-y: auto;
padding: 70px 20px 40px 20px; /* Top padding accommodates the header */
display: flex;
flex-direction: column;
gap: 24px;
scroll-behavior: smooth;
}
.message-row {
display: flex;
width: 100%;
max-width: 800px;
margin: 0 auto;
animation: fadeIn 0.4s ease-out forwards;
}
.message-row.user {
justify-content: flex-end;
}
.message-row.ai {
justify-content: flex-start;
}
.message-content {
max-width: 80%;
padding: 14px 18px;
border-radius: 12px;
font-size: 15px;
line-height: 1.6;
word-wrap: break-word;
}
.user .message-content {
background-color: var(--msg-user);
border-bottom-right-radius: 4px;
}
.ai .message-content {
background-color: var(--msg-ai);
}
.ai-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background-image: url('assets/stem_black.png');
background-size: cover;
background-position: center;
margin-right: 15px;
flex-shrink: 0;
margin-top: 5px;
}
/* --- Input Area Styles --- */
.input-container {
padding: 20px 20px 10px 20px;
background: linear-gradient(180deg, transparent, var(--bg-main) 20%);
display: flex;
flex-direction: column;
align-items: center;
z-index: 10;
}
.input-box {
width: 100%;
max-width: 800px;
position: relative;
background-color: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 16px;
transition: border-color 0.3s;
}
.input-box:focus-within {
border-color: var(--brand-orange);
}
textarea {
width: 100%;
background: transparent;
border: none;
color: var(--text-primary);
padding: 16px 50px 16px 20px;
font-family: 'Montserrat', sans-serif;
font-size: 15px;
resize: none;
outline: none;
height: 56px;
max-height: 200px;
line-height: 1.5;
overflow-y: hidden;
}
textarea::placeholder {
color: var(--text-secondary);
}
.send-btn {
position: absolute;
right: 10px;
bottom: 10px;
width: 36px;
height: 36px;
border-radius: 10px;
background-color: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
transition: color 0.2s, transform 0.1s;
}
.send-btn:hover {
color: var(--text-primary);
}
.send-btn:active {
transform: scale(0.95);
}
.send-btn svg {
width: 20px;
height: 20px;
fill: currentColor;
}
.disclaimer-text {
font-size: 12px;
color: #555555;
margin-top: 12px;
text-align: center;
}
.welcome-placeholder {
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
font-family: 'Montserrat', sans-serif;
font-size: 28px;
font-weight: 600;
color: var(--text-secondary);
user-select: none;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Scrollbar Styling */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #555; }
.cursor::after {
content: '▋';
animation: blink 1s step-start infinite;
color: var(--text-secondary);
margin-left: 2px;
}
@keyframes blink { 50% { opacity: 0; } }
</style>
</head>
<body>
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<img src="assets/stembotix.png" alt="STEMbotix Logo" class="brand-logo">
</div>
<button class="new-chat-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v14M5 12h14"/>
</svg>
New Chat
</button>
<div class="chat-history" id="chatHistoryList">
</div>
<div class="sidebar-footer">
<div class="settings-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Settings
</div>
</div>
</aside>
<main class="main-chat">
<header class="main-header">
<button class="toggle-sidebar-btn" id="toggleSidebarBtn" title="Toggle Sidebar">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="9" y1="3" x2="9" y2="21"></line>
</svg>
</button>
</header>
<div class="chat-container" id="chatContainer">
<div class="welcome-placeholder" id="welcomePlaceholder">Where should we start?</div>
</div>
<div class="input-container">
<div class="input-box">
<textarea id="userInput" placeholder="Ask StemGraph AI..." rows="1"></textarea>
<button class="send-btn" id="sendBtn">
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"></path></svg>
</button>
</div>
<div class="disclaimer-text">
StemGraph is an AI agent and can make mistakes.
</div>
</div>
</main>
<script>
/* =========================================
State
========================================= */
let currentThreadId = crypto.randomUUID();
const threads = []; // {id, title}
let isSending = false;
/* =========================================
Sidebar Toggle Logic
========================================= */
const sidebar = document.getElementById('sidebar');
const toggleSidebarBtn = document.getElementById('toggleSidebarBtn');
toggleSidebarBtn.addEventListener('click', () => {
sidebar.classList.toggle('collapsed');
});
/* =========================================
Chat History Sidebar
========================================= */
const chatHistoryList = document.getElementById('chatHistoryList');
function addThreadToSidebar(id, title) {
threads.unshift({id: id, title: title});
renderHistory();
}
function renderHistory() {
chatHistoryList.innerHTML = '';
threads.forEach((thread) => {
const item = document.createElement('div');
item.className = `history-item ${thread.id === currentThreadId ? 'active' : ''}`;
item.setAttribute('data-thread-id', thread.id);
item.innerHTML = `
<span class="chat-title">${thread.title}</span>
<button class="options-btn" onclick="toggleMenu(event, this)">&#8942;</button>
<div class="options-menu">
<div class="option-item" onclick="renameChat(event, this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>
Rename
</div>
<div class="option-item delete" onclick="deleteChat(event, this)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
Delete
</div>
</div>
`;
item.addEventListener('click', () => loadThread(thread.id));
chatHistoryList.appendChild(item);
});
}
function loadThread(threadId) {
currentThreadId = threadId;
renderHistory();
chatContainer.innerHTML = '';
fetch('/history/' + threadId)
.then(res => res.json())
.then(data => {
data.messages.forEach(msg => {
const sender = msg.role === 'user' ? 'user' : 'ai';
const el = appendMessage(sender, msg.content);
if (sender === 'ai') renderMath(el);
});
});
}
/* =========================================
New Chat
========================================= */
document.querySelector('.new-chat-btn').addEventListener('click', () => {
currentThreadId = crypto.randomUUID();
chatContainer.innerHTML = '<div class="welcome-placeholder" id="welcomePlaceholder">Where should we start?</div>';
renderHistory();
});
/* =========================================
Options Menu (Rename / Delete)
========================================= */
function toggleMenu(e, btn) {
e.stopPropagation();
closeAllMenus();
const menu = btn.nextElementSibling;
menu.classList.add('show');
btn.classList.add('menu-open');
}
document.addEventListener('click', closeAllMenus);
function closeAllMenus() {
document.querySelectorAll('.options-menu.show').forEach(menu => {
menu.classList.remove('show');
});
document.querySelectorAll('.options-btn.menu-open').forEach(btn => {
btn.classList.remove('menu-open');
});
}
function deleteChat(e, optionEl) {
e.stopPropagation();
const item = optionEl.closest('.history-item');
const threadId = item.getAttribute('data-thread-id');
const idx = threads.findIndex(t => t.id === threadId);
if (idx !== -1) threads.splice(idx, 1);
item.style.opacity = '0';
setTimeout(() => { item.remove(); }, 200);
}
function renameChat(e, optionEl) {
e.stopPropagation();
const item = optionEl.closest('.history-item');
const titleSpan = item.querySelector('.chat-title');
const threadId = item.getAttribute('data-thread-id');
closeAllMenus();
const currentTitle = titleSpan.innerText;
const input = document.createElement('input');
input.type = 'text';
input.value = currentTitle;
input.className = 'rename-input';
titleSpan.replaceWith(input);
input.focus();
input.selectionStart = input.selectionEnd = input.value.length;
function saveRename() {
const newTitle = input.value.trim() || 'Untitled Chat';
titleSpan.innerText = newTitle;
input.replaceWith(titleSpan);
const thread = threads.find(t => t.id === threadId);
if (thread) thread.title = newTitle;
fetch('/rename', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({thread_id: threadId, title: newTitle})
});
}
input.addEventListener('blur', saveRename);
input.addEventListener('keydown', (evt) => {
if (evt.key === 'Enter') saveRename();
if (evt.key === 'Escape') {
titleSpan.innerText = currentTitle;
input.replaceWith(titleSpan);
}
});
input.addEventListener('click', (evt) => evt.stopPropagation());
}
/* =========================================
Chat Messaging Logic
========================================= */
const chatContainer = document.getElementById('chatContainer');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
userInput.addEventListener('input', function() {
this.style.height = '56px';
this.style.height = (this.scrollHeight) + 'px';
if (this.value.trim().length > 0) {
sendBtn.style.color = 'var(--text-primary)';
} else {
sendBtn.style.color = 'var(--text-secondary)';
}
});
userInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
sendBtn.addEventListener('click', sendMessage);
function sendMessage() {
const text = userInput.value.trim();
if (!text || isSending) return;
// Remove welcome placeholder if present
const placeholder = document.getElementById('welcomePlaceholder');
if (placeholder) placeholder.remove();
isSending = true;
appendMessage('user', text);
userInput.value = '';
userInput.style.height = '56px';
sendBtn.style.color = 'var(--text-secondary)';
// Add thread to sidebar or move existing to top
const exists = threads.find(t => t.id === currentThreadId);
if (!exists) {
const title = text.length > 30 ? text.substring(0, 30) + '...' : text;
addThreadToSidebar(currentThreadId, title);
} else {
const idx = threads.indexOf(exists);
threads.splice(idx, 1);
threads.unshift(exists);
renderHistory();
}
streamResponse(text);
}
function streamResponse(text) {
const contentElement = appendMessage('ai', '');
contentElement.classList.add('cursor');
fetch('/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: text, thread_id: currentThreadId})
}).then(response => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function read() {
reader.read().then(({done, value}) => {
if (done) {
contentElement.classList.remove('cursor');
renderMath(contentElement);
isSending = false;
return;
}
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split('\n');
buffer = lines.pop();
lines.forEach(line => {
if (!line.startsWith('data: ')) return;
const payload = line.substring(6);
if (payload === '[DONE]') {
contentElement.classList.remove('cursor');
renderMath(contentElement);
isSending = false;
return;
}
const data = JSON.parse(payload);
contentElement.textContent += data.token;
scrollToBottom();
});
read();
});
}
read();
});
}
function appendMessage(sender, text) {
const rowDiv = document.createElement('div');
rowDiv.classList.add('message-row', sender);
let contentHTML = '';
if (sender === 'ai') contentHTML += `<div class="ai-avatar"></div>`;
contentHTML += `<div class="message-content">${text}</div>`;
rowDiv.innerHTML = contentHTML;
chatContainer.appendChild(rowDiv);
scrollToBottom();
return rowDiv.querySelector('.message-content');
}
function scrollToBottom() {
chatContainer.scrollTop = chatContainer.scrollHeight;
}
/* =========================================
Math Rendering (KaTeX)
========================================= */
function renderMath(element) {
if (typeof renderMathInElement === 'undefined') return;
renderMathInElement(element, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\(', right: '\\)', display: false},
{left: '\\[', right: '\\]', display: true}
],
throwOnError: false
});
}
/* =========================================
Init: Load existing threads
========================================= */
fetch('/threads')
.then(res => res.json())
.then(data => {
data.threads.forEach(t => {
threads.push({id: t.id, title: t.title});
});
renderHistory();
});
</script>
</body>
</html>