chansung/code-explainer-pilot
2
1document.addEventListener('DOMContentLoaded', () => {2 const codeSnippet = `// HTML structure for this example:3// <button id="counterBtn">Click me: 0</button>4// <p id="message"></p>5 6const button = document.getElementById('counterBtn');7let count = 0;8 9button.addEventListener('click', () => {10 count++;11 button.textContent = \`Click me: \${count}\`;12 13 if (count === 5) {14 const messageEl = document.getElementById('message');15 messageEl.textContent = "You've reached 5 clicks!";16 } else if (count > 5) {17 const msgEl = document.getElementById('message');18 msgEl.textContent = "Keep clicking!";19 }20});`;21 22 const journeyStructure = [23 {24 groupTitle: "Setup & Initialization",25 steps: [26 { category: "Setup", title: "HTML Structure", lineRanges: [[0, 3]], explanation: "This JavaScript interacts with specific HTML elements. We're assuming a button with <code>id=\"counterBtn\"</code> and a paragraph with <code>id=\"message\"</code> exist on the page. The comments at the top outline this structure." },27 { category: "DOM Query", title: "Access Button Element", lineRanges: [[4, 4]], explanation: "<code>const button = document.getElementById('counterBtn');</code><br>This line uses the DOM API to find the HTML button element with ID 'counterBtn' and stores it in the 'button' constant." },28 { category: "Variable Declaration", title: "Initialize Click Counter", lineRanges: [[5, 5]], explanation: "<code>let count = 0;</code><br>A variable 'count' is declared and initialized to 0. This variable will track clicks." }29 ]30 },31 {32 groupTitle: "Core Interaction Logic",33 steps: [34 { category: "Event Handling", title: "Attach Click Listener", lineRanges: [[7, 7], [16,16]], explanation: "<code>button.addEventListener('click', () => { ... });</code><br>An event listener is attached to the 'button'. The arrow function <code>() => { ... }</code> executes on each click." },35 { category: "State Update", title: "Increment Counter", lineRanges: [[8, 8]], explanation: "<code>count++;</code><br>Inside the click handler, 'count' is incremented by 1." },36 { category: "UI Update", title: "Update Button Text", lineRanges: [[9, 9]], explanation: "<code>button.textContent = \`Click me: \${count}\`;</code><br>The button's text is updated to show the new 'count' using template literals." }37 ]38 },39 {40 groupTitle: "Conditional Feedback",41 steps: [42 { category: "Conditional Logic", title: "Check 5-Click Milestone", lineRanges: [[11, 11],[12,12],[13,13]], explanation: "<code>if (count === 5) { ... }</code><br>If 'count' reaches 5, a special message is displayed in the 'message' paragraph. Note that line 13 is the closing brace of this `if` block." },43 { category: "Conditional Logic", title: "Handle Subsequent Clicks", lineRanges: [[13,13],[14,14],[15,15]], explanation: "<code>else if (count > 5) { ... }</code><br>If 'count' is over 5, a different message encourages continued clicking. Note that line 13 is also the start of this `else if`." }44 ]45 },46 {47 groupTitle: "Conclusion",48 steps: [49 { category: "Summary", title: "Tour Completed!", lineRanges: [], explanation: "You've completed the tour! This example showed DOM manipulation, events, variables, and conditional logic. Click 'Reset' to restart." }50 ]51 }52 ];53 54 const categoryStyles = {55 "Setup": "bg-slate-200 text-slate-700",56 "DOM Query": "bg-sky-100 text-sky-700",57 "Variable Declaration": "bg-emerald-100 text-emerald-700",58 "Event Handling": "bg-violet-100 text-violet-700",59 "State Update": "bg-amber-100 text-amber-700",60 "UI Update": "bg-indigo-100 text-indigo-700",61 "Conditional Logic": "bg-rose-100 text-rose-700",62 "Summary": "bg-cyan-100 text-cyan-700",63 "Default": "bg-gray-200 text-gray-700" // Fallback64 };65 66 const codeLinesContainer = document.getElementById('codeLinesContainer');67 const explanationStepTitle = document.getElementById('explanationStepTitle');68 const explanationMeta = document.getElementById('explanationMeta');69 const explanationText = document.getElementById('explanationText');70 const explanationStepCategoryContainer = document.getElementById('explanationStepCategoryContainer');71 const prevBtn = document.getElementById('prevBtn');72 const nextBtn = document.getElementById('nextBtn');73 const resetBtn = document.getElementById('resetBtn');74 const progressIndicator = document.getElementById('progressIndicator');75 const stepsOutlineUl = document.getElementById('stepsOutline');76 const mobileCurrentGroupTextEl = document.getElementById('mobileCurrentGroupText');77 const mobileCurrentStepTextEl = document.getElementById('mobileCurrentStepText'); // New element78 79 let currentStepIndex = 0;80 let lineElements = [];81 let flatCodeSteps = [];82 83 // Flatten the journeyStructure for sequential navigation84 journeyStructure.forEach(group => {85 group.steps.forEach(step => {86 flatCodeSteps.push(step);87 });88 });89 90 function handleCodeLineClick(event) {91 const clickedLineDiv = event.currentTarget;92 const clickedLineIndex = parseInt(clickedLineDiv.dataset.lineIndex, 10);93 94 for (let i = 0; i < flatCodeSteps.length; i++) {95 const step = flatCodeSteps[i];96 if (step.lineRanges && step.lineRanges.length > 0) {97 for (const range of step.lineRanges) {98 if (clickedLineIndex >= range[0] && clickedLineIndex <= range[1]) {99 currentStepIndex = i;100 updateStep();101 return;102 }103 }104 }105 }106 }107 108 function renderCode() {109 codeLinesContainer.innerHTML = '';110 lineElements = [];111 112 const tempCodeElement = document.createElement('code');113 tempCodeElement.className = 'language-javascript';114 tempCodeElement.textContent = codeSnippet;115 116 if (typeof Prism !== 'undefined' && Prism.highlightElement) {117 Prism.highlightElement(tempCodeElement);118 } else {119 console.warn("Prism.js not available. Syntax highlighting will be plain.");120 }121 122 const highlightedHtml = tempCodeElement.innerHTML;123 const linesHtml = highlightedHtml.split('\n');124 125 linesHtml.forEach((lineHtml, index) => {126 const lineDiv = document.createElement('div');127 lineDiv.classList.add('code-line');128 lineDiv.innerHTML = lineHtml || ' ';129 lineDiv.dataset.lineIndex = index;130 lineDiv.addEventListener('click', handleCodeLineClick);131 codeLinesContainer.appendChild(lineDiv);132 lineElements.push(lineDiv);133 });134 }135 136 function renderOutline() {137 stepsOutlineUl.innerHTML = '';138 let globalStepIndex = 0;139 140 journeyStructure.forEach((group, groupIndex) => {141 const groupLi = document.createElement('li');142 groupLi.className = `pt-3 pb-1 px-1 ${groupIndex > 0 ? 'mt-1' : ''}`;143 144 const groupTitleEl = document.createElement('h4');145 groupTitleEl.className = 'text-xs font-semibold text-slate-500 uppercase tracking-wider select-none';146 groupTitleEl.textContent = group.groupTitle;147 groupLi.appendChild(groupTitleEl);148 stepsOutlineUl.appendChild(groupLi);149 150 group.steps.forEach((step, stepIndexInGroup) => {151 const li = document.createElement('li');152 li.className = 'outline-step-item rounded-md ml-1';153 li.dataset.stepIndex = globalStepIndex;154 155 const button = document.createElement('button');156 button.className = 'group flex items-start w-full text-left p-2 focus:outline-none focus:ring-2 focus:ring-sky-300 focus:z-10 rounded-md transition-colors hover:bg-slate-100';157 158 const dotAndLineContainer = document.createElement('div');159 dotAndLineContainer.className = 'flex flex-col items-center mr-3 mt-1 flex-shrink-0';160 161 const dot = document.createElement('div');162 dot.className = 'outline-dot w-3 h-3 rounded-full bg-slate-300 border-2 border-white group-hover:bg-sky-400 transition-all duration-150 ease-in-out';163 dotAndLineContainer.appendChild(dot);164 165 if (globalStepIndex < flatCodeSteps.length - 1) {166 const isLastInGroup = stepIndexInGroup === group.steps.length - 1;167 const isLastGroup = groupIndex === journeyStructure.length - 1;168 if (isLastInGroup && !(isLastGroup && isLastInGroup) ) {169 const line = document.createElement('div');170 line.className = 'outline-connector w-0.5 h-2 bg-slate-300 mt-1 group-hover:bg-slate-400 transition-colors duration-150 ease-in-out';171 dotAndLineContainer.appendChild(line);172 } else if (!(isLastGroup && isLastInGroup)){173 const line = document.createElement('div');174 line.className = 'outline-connector w-0.5 h-5 bg-slate-300 mt-1 group-hover:bg-slate-400 transition-colors duration-150 ease-in-out';175 dotAndLineContainer.appendChild(line);176 } else {177 const placeholder = document.createElement('div');178 placeholder.className = 'h-5 mt-1';179 dotAndLineContainer.appendChild(placeholder);180 }181 } else {182 const placeholder = document.createElement('div');183 placeholder.className = 'h-5 mt-1';184 dotAndLineContainer.appendChild(placeholder);185 }186 187 188 const titleSpan = document.createElement('span');189 titleSpan.className = 'outline-title text-sm text-slate-600 group-hover:text-sky-600 transition-colors duration-150 ease-in-out';190 titleSpan.textContent = step.title;191 192 button.appendChild(dotAndLineContainer);193 button.appendChild(titleSpan);194 li.appendChild(button);195 196 button.addEventListener('click', () => {197 currentStepIndex = parseInt(li.dataset.stepIndex, 10);198 updateStep();199 });200 stepsOutlineUl.appendChild(li);201 globalStepIndex++;202 });203 });204 }205 206 function updateOutlineActiveState() {207 const outlineItems = stepsOutlineUl.querySelectorAll('.outline-step-item[data-step-index]');208 const outlineNav = stepsOutlineUl.parentElement;209 210 outlineItems.forEach((itemLi) => {211 const idx = parseInt(itemLi.dataset.stepIndex, 10);212 const button = itemLi.querySelector('button');213 const dot = itemLi.querySelector('.outline-dot');214 const title = itemLi.querySelector('.outline-title');215 const connector = itemLi.querySelector('.outline-connector');216 217 if (idx === currentStepIndex) {218 button.classList.add('bg-sky-50');219 dot.classList.remove('bg-slate-300');220 dot.classList.add('bg-sky-500', 'scale-125');221 if (connector) connector.classList.add('bg-sky-400');222 title.classList.remove('text-slate-600');223 title.classList.add('text-sky-700', 'font-semibold');224 225 if (outlineNav.scrollHeight > outlineNav.clientHeight && !outlineNav.classList.contains('hidden')) { // Only scroll if scrollable and visible226 const itemRect = itemLi.getBoundingClientRect();227 const navRect = outlineNav.getBoundingClientRect();228 if (itemRect.top < navRect.top || itemRect.bottom > navRect.bottom) {229 itemLi.scrollIntoView({ behavior: 'smooth', block: 'nearest' });230 }231 }232 } else {233 button.classList.remove('bg-sky-50');234 dot.classList.add('bg-slate-300');235 dot.classList.remove('bg-sky-500', 'scale-125');236 if (connector) connector.classList.remove('bg-sky-400');237 title.classList.add('text-slate-600');238 title.classList.remove('text-sky-700', 'font-semibold');239 }240 });241 }242 243 function updateStep() {244 if (currentStepIndex < 0 || currentStepIndex >= flatCodeSteps.length) return;245 246 const step = flatCodeSteps[currentStepIndex];247 const hasLineFocus = step.lineRanges && step.lineRanges.length > 0;248 249 // Update mobile current group and step titles250 if (mobileCurrentGroupTextEl && mobileCurrentStepTextEl) {251 let currentGroupTitleForMobile = "Loading...";252 let stepsProcessed = 0;253 for (const group of journeyStructure) {254 const stepsInGroup = group.steps.length;255 if (currentStepIndex >= stepsProcessed && currentStepIndex < stepsProcessed + stepsInGroup) {256 currentGroupTitleForMobile = group.groupTitle;257 break;258 }259 stepsProcessed += stepsInGroup;260 }261 mobileCurrentGroupTextEl.textContent = currentGroupTitleForMobile;262 mobileCurrentStepTextEl.textContent = step.title;263 }264 265 266 explanationStepCategoryContainer.innerHTML = '';267 if (step.category) {268 const badge = document.createElement('span');269 const styleClasses = categoryStyles[step.category] || categoryStyles["Default"];270 badge.className = `${styleClasses} text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full inline-block`;271 badge.textContent = step.category;272 explanationStepCategoryContainer.appendChild(badge);273 explanationStepCategoryContainer.classList.remove('hidden');274 } else {275 explanationStepCategoryContainer.classList.add('hidden');276 }277 278 explanationStepTitle.textContent = step.title;279 explanationText.innerHTML = step.explanation;280 281 lineElements.forEach(el => {282 el.classList.remove('highlighted', 'code-line-dimmed');283 });284 285 let firstHighlightedLineElement = null;286 let highlightedLineNumbersForMeta = [];287 288 if (hasLineFocus) {289 const focusedIndices = new Set();290 step.lineRanges.forEach(range => {291 for (let i = range[0]; i <= range[1]; i++) {292 focusedIndices.add(i);293 if (lineElements[i]) {294 if (!highlightedLineNumbersForMeta.includes(i + 1)) {295 highlightedLineNumbersForMeta.push(i + 1);296 }297 if (!firstHighlightedLineElement) firstHighlightedLineElement = lineElements[i];298 }299 }300 });301 302 lineElements.forEach((el, idx) => {303 if (focusedIndices.has(idx)) {304 el.classList.add('highlighted');305 } else {306 el.classList.add('code-line-dimmed');307 }308 });309 highlightedLineNumbersForMeta.sort((a,b) => a - b);310 const rangesSummary = highlightedLineNumbersForMeta.length === 1 ? `Line ${highlightedLineNumbersForMeta[0]}` : `Lines ${highlightedLineNumbersForMeta.join(', ')}`;311 explanationMeta.textContent = `Focus: ${rangesSummary}`;312 313 } else {314 explanationMeta.textContent = "General overview for this step.";315 }316 317 if (firstHighlightedLineElement) {318 const codePane = codeLinesContainer.parentElement;319 if (codePane.scrollHeight > codePane.clientHeight) {320 const lineRect = firstHighlightedLineElement.getBoundingClientRect();321 const paneRect = codePane.getBoundingClientRect();322 if (lineRect.top < paneRect.top || lineRect.bottom > paneRect.bottom) {323 firstHighlightedLineElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' });324 }325 }326 }327 328 329 prevBtn.disabled = currentStepIndex === 0;330 nextBtn.disabled = currentStepIndex === flatCodeSteps.length - 1;331 progressIndicator.textContent = `Step ${currentStepIndex + 1} / ${flatCodeSteps.length}`;332 updateOutlineActiveState();333 }334 335 nextBtn.addEventListener('click', () => {336 if (currentStepIndex < flatCodeSteps.length - 1) {337 currentStepIndex++;338 updateStep();339 }340 });341 342 prevBtn.addEventListener('click', () => {343 if (currentStepIndex > 0) {344 currentStepIndex--;345 updateStep();346 }347 });348 349 resetBtn.addEventListener('click', () => {350 currentStepIndex = 0;351 updateStep();352 const codePane = codeLinesContainer.parentElement;353 if (codePane) {354 codePane.scrollTop = 0;355 }356 const outlineNav = stepsOutlineUl.parentElement;357 if (outlineNav && !outlineNav.classList.contains('hidden')) { // Check if outlineNav is visible358 outlineNav.scrollTop = 0;359 }360 });361 362 renderCode();363 renderOutline();364 updateStep();365 });