// ───────────────────────────────────────────── // DOM REFS // ───────────────────────────────────────────── const $ = (id) => document.getElementById(id); const els = { repoInput: $('repoInput'), analyseBtn: $('analyseBtn'), landingPage: $('landingPage'), processingPage: $('processingPage'), chatPage: $('chatPage'), stepsContainer: $('stepsContainer'), procSubtitle: $('procSubtitle'), footer: $('footer'), chatInput: $('chatInput'), sendBtn: $('sendBtn'), chatMessages: $('chatMessages'), warningBanner: $('warningBanner'), dismissWarning: $('dismissWarning'), statusDot: $('statusDot'), statusLabel: $('statusLabel'), }; let IS_BIG_REPO = false; let activeStepBox = null; let hadError = false; let hadWarning = false; // ───────────────────────────────────────────── // MARKED.JS SETUP // ───────────────────────────────────────────── if (typeof marked !== 'undefined') { marked.setOptions({ breaks: true, gfm: true }); } // ───────────────────────────────────────────── // UTILITIES // ───────────────────────────────────────────── const delay = (ms) => new Promise((r) => setTimeout(r, ms)); function setStatus(mode, label) { els.statusDot.className = 'status-dot ' + mode; els.statusLabel.textContent = label; } function showPage(pageEl) { ['landingPage', 'processingPage', 'chatPage'].forEach((id) => { $(id).classList.remove('active'); }); pageEl.classList.add('active'); } function formatTime(d) { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } // ───────────────────────────────────────────── // AUTO-GROW TEXTAREA // ───────────────────────────────────────────── els.chatInput.addEventListener('input', () => { els.chatInput.style.height = 'auto'; els.chatInput.style.height = Math.min(els.chatInput.scrollHeight, 160) + 'px'; }); // ───────────────────────────────────────────── // COLLAPSE A COMPLETED STEP BOX // ───────────────────────────────────────────── function collapseBox(box, status) { return new Promise((resolve) => { // Lock height at current value so we can animate down const currentH = box.offsetHeight; box.style.height = currentH + 'px'; box.style.overflow = 'hidden'; // Badge text const badge = document.createElement('span'); badge.className = 'step-collapsed-badge'; badge.textContent = status === 'ERROR' ? '· Error' : status === 'WARNING' ? '· Warning' : '· Done'; box.querySelector('.step-title').appendChild(badge); // Hide sub-items right away so they don't flash during resize const subItems = box.querySelector('.sub-items'); if (subItems) subItems.style.opacity = '0'; // Animate to collapsed height next frame requestAnimationFrame(() => { requestAnimationFrame(() => { box.style.height = '46px'; box.style.paddingTop = '0'; box.style.paddingBottom = '0'; }); }); box.addEventListener('transitionend', function handler(e) { if (e.propertyName !== 'height') return; box.removeEventListener('transitionend', handler); // Reveal the badge now that height is settled const b = box.querySelector('.step-collapsed-badge'); if (b) b.classList.add('visible'); resolve(); }); }); } // ───────────────────────────────────────────── // LANDING → PROCESSING // ───────────────────────────────────────────── els.analyseBtn.addEventListener('click', startAnalysis); els.repoInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') startAnalysis(); }); // Welcome chip clicks document.querySelectorAll('.welcome-chip').forEach((chip) => { chip.addEventListener('click', () => { els.chatInput.value = chip.textContent; els.chatInput.focus(); }); }); function startAnalysis() { const repoUrl = els.repoInput.value.trim(); if (!repoUrl) return; setStatus('active', 'analysing'); showPage(els.processingPage); els.stepsContainer.innerHTML = ''; activeStepBox = null; IS_BIG_REPO = false; hadError = false; hadWarning = false; connectToBackend(repoUrl); } // ───────────────────────────────────────────── // BACKEND: INIT-REPO STREAM // ───────────────────────────────────────────── async function connectToBackend(repoUrl) { try { //const response = await fetch('http://localhost:8000/init-repo', { const response = await fetch('/init-repo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: repoUrl }), }); if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; 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 event of lines) { if (!event.trim()) continue; const dataLine = event.split('\n')[0]; if (!dataLine.startsWith('data: ')) continue; try { const data = JSON.parse(dataLine.slice(6).trim()); await processBackendEvent(data); } catch (e) { console.error('Parse error:', e); } } } } catch (error) { console.error('Fetch error:', error); if (activeStepBox) { await finishActiveBox(activeStepBox, 'ERROR'); } else { const errorBox = createNewStepBox(`Error: ${error.message}`); await finishActiveBox(errorBox, 'ERROR'); } showProcessingAction('error'); } } async function processBackendEvent(data) { const { status, task } = data; if (status === 'FINISHED') { if (activeStepBox) await finishActiveBox(activeStepBox, 'SUCCESS'); if (hadError) { showProcessingAction('error'); } else if (hadWarning) { showProcessingAction('warning'); } else { await delay(700); setStatus('chat', 'ready'); transitionToChat(); } return; } if (status === 'START') { if (activeStepBox) await finishActiveBox(activeStepBox, 'SUCCESS'); activeStepBox = createNewStepBox(task); if (els.procSubtitle) els.procSubtitle.textContent = task; } if (status === 'WARNING' && task && task.includes('Repo is large')) { IS_BIG_REPO = true; } if (status === 'WARNING') hadWarning = true; if (status === 'ERROR') hadError = true; if (status === 'SUCCESS' || status === 'WARNING' || status === 'ERROR') { if (activeStepBox) { const titleText = activeStepBox.querySelector('.title-text'); if (titleText) titleText.textContent = task; await finishActiveBox(activeStepBox, status); activeStepBox = null; } else { const box = createNewStepBox(task); await finishActiveBox(box, status); } } } // ───────────────────────────────────────────── // PROCESSING ACTION BUTTON (error / warning gate) // ───────────────────────────────────────────── function showProcessingAction(type) { // Remove any existing action card const existing = els.stepsContainer.querySelector('.proc-action-card'); if (existing) existing.remove(); const card = document.createElement('div'); card.className = `proc-action-card ${type === 'error' ? 'proc-action-error' : 'proc-action-warning'}`; if (type === 'error') { card.innerHTML = `
Analysis failed
An error occurred while analysing this repository. Please check the URL and try again.
Analysis complete with warnings
Some steps raised warnings. Results may be partial, but you can still explore the repository.
blocks with header + copy button
bubble.querySelectorAll('pre').forEach((pre) => {
if (pre.parentElement && pre.parentElement.classList.contains('code-block-wrap')) return; // already wrapped
const codeEl = pre.querySelector('code');
const rawLang = codeEl ? codeEl.className.replace(/^language-/, '').trim() : '';
const lang = rawLang || 'code';
const wrap = document.createElement('div');
wrap.className = 'code-block-wrap';
const header = document.createElement('div');
header.className = 'code-block-header';
const langLabel = document.createElement('span');
langLabel.className = 'code-lang-label';
langLabel.textContent = lang;
const copyBtn = document.createElement('button');
copyBtn.className = 'code-copy-btn';
copyBtn.innerHTML = `Copy`;
copyBtn.addEventListener('click', () => {
const content = codeEl ? codeEl.textContent : pre.textContent;
navigator.clipboard.writeText(content).then(() => {
copyBtn.querySelector('span').textContent = 'Copied!';
setTimeout(() => { copyBtn.querySelector('span').textContent = 'Copy'; }, 1800);
}).catch(() => {});
});
header.appendChild(langLabel);
header.appendChild(copyBtn);
pre.parentNode.insertBefore(wrap, pre);
wrap.appendChild(header);
wrap.appendChild(pre);
});
}
function buildCopyResponseBtn(markdownText, bubble) {
const btn = document.createElement('button');
btn.className = 'msg-copy-btn';
btn.innerHTML = ``;
btn.addEventListener('click', () => {
// Copy plain text from the rendered bubble
navigator.clipboard.writeText(bubble.innerText).then(() => {
btn.querySelector('span').textContent = 'Copied!';
setTimeout(() => { btn.querySelector('span').textContent = 'Copy response'; }, 1800);
}).catch(() => {});
});
return btn;
}
// ─────────────────────────────────────────────
// MESSAGE UI HELPERS
// ─────────────────────────────────────────────
function appendUserMessage(text) {
const group = document.createElement('div');
group.className = 'msg-group user';
group.innerHTML = `
You
${escapeHtml(text)}
`;
els.chatMessages.appendChild(group);
group.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
function createBotBubble(container) {
const bubble = document.createElement('div');
bubble.className = 'msg-bubble bot';
container.appendChild(bubble);
return bubble;
}
// ─────────────────────────────────────────────
// THINKING UI — Collapsible (Gemini-style)
// ─────────────────────────────────────────────
function createThinkingUI(container) {
const wrap = document.createElement('div');
wrap.className = 'thinking-wrap';
wrap.innerHTML = `
`;
container.appendChild(wrap);
// Toggle click
const toggle = wrap.querySelector('.thinking-toggle');
const panel = wrap.querySelector('.thinking-steps-panel');
toggle.addEventListener('click', () => {
const isOpen = panel.classList.contains('open');
panel.classList.toggle('open', !isOpen);
toggle.classList.toggle('open', !isOpen);
toggle.setAttribute('aria-expanded', String(!isOpen));
});
return {
wrap,
toggle,
panel,
inner: wrap.querySelector('.thinking-steps-inner'),
};
}
function addThinkingStep(inner, text, previousStepEl) {
// Mark previous as done
if (previousStepEl) {
previousStepEl.classList.remove('current');
previousStepEl.classList.add('done-step');
}
const step = document.createElement('div');
step.className = 'thinking-step current';
// Preserve newlines from backend
const formatted = escapeHtml(text).replace(/\n/g, '
');
step.innerHTML = `${formatted}`;
inner.appendChild(step);
// Scroll the panel
inner.scrollTop = inner.scrollHeight;
return step;
}
function updateThinkingToggleLabel(wrap, count, isDone) {
const label = wrap.querySelector('.thinking-label');
const spinner = wrap.querySelector('.thinking-spinner');
const toggle = wrap.querySelector('.thinking-toggle');
if (isDone) {
label.textContent = `Thought for ${count} step${count !== 1 ? 's' : ''}`;
spinner.classList.add('done');
spinner.innerHTML = ``;
toggle.classList.remove('active');
} else {
label.textContent = `Thinking… (${count} step${count !== 1 ? 's' : ''})`;
}
}
function closeThinkingPanel(wrap) {
const panel = wrap.querySelector('.thinking-steps-panel');
const toggle = wrap.querySelector('.thinking-toggle');
// Auto-collapse once done so the focus is on the answer
panel.classList.remove('open');
toggle.classList.remove('open');
toggle.setAttribute('aria-expanded', 'false');
}
// ─────────────────────────────────────────────
// ESCAPE
// ─────────────────────────────────────────────
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}