AskTheHandbook / static /index.html
Rishit Pant
Fixed table handling issues (#17)
0887e58 unverified
Raw
History Blame Contribute Delete
8.73 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AskTheHandbook — IITM BS RAG Assistant</title>
<style>
:root {
--ink: #1b1a17;
--paper: #f6f3ea;
--rule: #d8d2c2;
--accent: #8a3b2b;
--accent-soft: #f0e2dc;
--mono: "IBM Plex Mono", "SF Mono", Menlo, monospace;
--serif: "Source Serif 4", "Iowan Old Style", Georgia, serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: var(--serif);
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
border-bottom: 1px solid var(--rule);
padding: 1.1rem 1.5rem;
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.4rem;
}
header h1 {
font-size: 1.15rem;
margin: 0;
font-weight: 600;
letter-spacing: 0.01em;
}
header span {
font-family: var(--mono);
font-size: 0.7rem;
color: #6b6457;
text-transform: uppercase;
letter-spacing: 0.08em;
}
main {
flex: 1;
width: 100%;
max-width: 720px;
margin: 0 auto;
padding: 1.5rem 1.25rem 8rem;
display: flex;
flex-direction: column;
gap: 1.4rem;
}
.empty {
margin-top: 3rem;
color: #6b6457;
font-size: 0.95rem;
line-height: 1.6;
border-left: 2px solid var(--rule);
padding-left: 1rem;
}
.msg { line-height: 1.65; font-size: 1rem; }
.msg.user {
font-family: var(--mono);
font-size: 0.85rem;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.03em;
}
.msg.user::before { content: "Q. "; }
.msg.answer { white-space: normal; }
.sources {
margin-top: 0.5rem;
font-family: var(--mono);
font-size: 0.72rem;
color: #6b6457;
border-top: 1px dashed var(--rule);
padding-top: 0.5rem;
}
.sources div { margin-top: 0.2rem; }
form {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, var(--paper) 60%, transparent);
padding: 1.5rem 1.25rem 1.5rem;
display: flex;
justify-content: center;
}
.input-row {
width: 100%;
max-width: 720px;
display: flex;
gap: 0.6rem;
background: #fff;
border: 1px solid var(--rule);
border-radius: 4px;
padding: 0.5rem;
}
textarea {
flex: 1;
border: none;
resize: none;
font-family: var(--serif);
font-size: 0.95rem;
line-height: 1.4;
padding: 0.3rem 0.4rem;
background: transparent;
color: var(--ink);
outline: none;
max-height: 6rem;
}
button {
border: none;
background: var(--accent);
color: #fff;
font-family: var(--mono);
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 0 1.1rem;
border-radius: 3px;
cursor: pointer;
}
button:disabled { opacity: 0.4; cursor: default; }
.cursor {
display: inline-block;
width: 0.5em;
height: 1em;
background: var(--accent);
vertical-align: -0.15em;
animation: blink 1s step-start infinite;
}
@keyframes blink { 50% { opacity: 0; } }
/* --- markdown rendering ----------------------------------------- */
.msg.answer { white-space: normal; } /* override pre-wrap; marked handles newlines */
.msg.answer p { margin: 0 0 0.65em; }
.msg.answer p:last-child { margin-bottom: 0; }
.msg.answer ul, .msg.answer ol {
margin: 0.3em 0 0.65em 1.3em;
padding: 0;
}
.msg.answer li { margin-bottom: 0.2em; }
.msg.answer code {
font-family: var(--mono);
font-size: 0.82em;
background: #ede8dc;
padding: 0.1em 0.35em;
border-radius: 2px;
}
.msg.answer pre {
background: #ede8dc;
padding: 0.75rem 1rem;
border-radius: 3px;
overflow-x: auto;
font-family: var(--mono);
font-size: 0.82em;
line-height: 1.5;
margin: 0.5em 0;
}
.msg.answer pre code { background: none; padding: 0; }
.msg.answer strong { font-weight: 700; }
.msg.answer em { font-style: italic; }
/* pipe tables */
.msg.answer table {
border-collapse: collapse;
width: 100%;
font-size: 0.88em;
margin: 0.6em 0;
font-family: var(--serif);
}
.msg.answer th,
.msg.answer td {
border: 1px solid var(--rule);
padding: 5px 10px;
text-align: left;
vertical-align: top;
}
.msg.answer th {
background: var(--accent-soft);
font-weight: 600;
}
.msg.answer tr:nth-child(even) td { background: #faf8f2; }
</style>
<script src="https://cdn.jsdelivr.net/npm/marked@12/marked.min.js"></script>
</head>
<body>
<header>
<h1>AskTheHandbook</h1>
<span>RAG Assistant · IIT Madras BS</span>
</header>
<main id="thread">
<div class="empty" id="empty-state">
Ask about fees, grading policy, exam schedules, eligibility, or academic structure.
Answers are grounded strictly in the official student handbook and grading
document, with sources cited below each response.
</div>
</main>
<form id="form">
<div class="input-row">
<textarea id="question" rows="1" placeholder="e.g. What is the revised fee for the foundation level?" required></textarea>
<button id="submit" type="submit">Ask</button>
</div>
</form>
<script>
const thread = document.getElementById('thread');
const emptyState = document.getElementById('empty-state');
const form = document.getElementById('form');
const input = document.getElementById('question');
const submitBtn = document.getElementById('submit');
// Configure marked: GitHub-flavoured tables on, sanitize off
marked.use({ gfm: true, breaks: true });
/** Render markdown into answerDiv, then re-attach the blinking cursor. */
function renderAnswer(div, text, cursor) {
div.innerHTML = marked.parse(text);
div.appendChild(cursor);
}
input.addEventListener('input', () => {
input.style.height = 'auto';
input.style.height = Math.min(input.scrollHeight, 96) + 'px';
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const question = input.value.trim();
if (!question) return;
emptyState.remove();
input.value = '';
input.style.height = 'auto';
submitBtn.disabled = true;
const userDiv = document.createElement('div');
userDiv.className = 'msg user';
userDiv.textContent = question;
thread.appendChild(userDiv);
const answerDiv = document.createElement('div');
answerDiv.className = 'msg answer';
const cursor = document.createElement('span');
cursor.className = 'cursor';
answerDiv.appendChild(cursor);
thread.appendChild(answerDiv);
window.scrollTo(0, document.body.scrollHeight);
try {
const res = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question })
});
if (res.status === 429) {
cursor.remove();
answerDiv.textContent = "You've hit the question limit for now — try again after a minute.";
return;
}
if (!res.ok) {
cursor.remove();
answerDiv.textContent = 'Something went wrong reaching the assistant. Please try again.';
return;
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullText = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const payload = JSON.parse(line.slice(6));
if (payload.token) {
fullText += payload.token;
renderAnswer(answerDiv, fullText, cursor);
window.scrollTo(0, document.body.scrollHeight);
}
if (payload.done) {
cursor.remove();
// Final render without cursor interference
answerDiv.innerHTML = marked.parse(fullText);
if (payload.sources && payload.sources.length) {
const srcDiv = document.createElement('div');
srcDiv.className = 'sources';
payload.sources.forEach(s => {
const line = document.createElement('div');
line.textContent = `${s.source}${s.section}`;
srcDiv.appendChild(line);
});
answerDiv.appendChild(srcDiv);
}
}
}
}
} catch (err) {
cursor.remove();
answerDiv.textContent = 'Something went wrong reaching the assistant. Please try again.';
} finally {
submitBtn.disabled = false;
}
});
</script>
</body>
</html>