scn-consultation-api / src /template /system_design /lld_code_template.html
SS-2005's picture
Upload 10 files
49f5735 verified
Raw
History Blame Contribute Delete
28.6 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<!-- Prism.js for syntax highlighting (Tomorrow Night matches old_lld_template) -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #0d1117;
color: #c9d1d9;
height: 100vh;
overflow: hidden;
}
.container {
display: flex;
height: 100vh;
flex-direction: column;
}
.header {
background: #161b22;
padding: 22px 34px;
border-bottom: 1px solid #30363d;
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
gap: 22px;
}
.logo {
height: 45px;
width: auto;
}
.header-title {
font-size: 28px;
color: #f0f6fc;
font-weight: 600;
letter-spacing: 0.7px;
}
.header-subtitle {
font-size: 19px;
color: #8b949e;
margin-top: 3px;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 390px;
background: #0d1117;
border-right: 1px solid #30363d;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 17px 22px;
background: #161b22;
font-size: 17px;
text-transform: uppercase;
letter-spacing: 1.4px;
color: #8b949e;
font-weight: 700;
border-bottom: 1px solid #30363d;
}
.file-tree {
flex: 1;
overflow-y: auto;
padding: 11px 0;
}
.file-item {
padding: 11px 17px 11px 34px;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
gap: 11px;
transition: background 0.15s;
}
.file-item:hover {
background: #2a2d2e;
}
.file-item.active {
background: #37373d;
color: #58a6ff;
}
.file-icon {
width: 22px;
height: 22px;
font-size: 20px;
display: flex;
align-items: center;
}
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
}
.tabs {
background: #161b22;
border-bottom: 1px solid #30363d;
display: flex;
padding: 0 11px;
overflow-x: auto;
}
.tab {
padding: 14px 22px;
font-size: 18px;
cursor: pointer;
border-right: 1px solid #30363d;
background: #161b22;
color: #8b949e;
transition: all 0.15s ease;
white-space: nowrap;
display: flex;
align-items: center;
gap: 8px;
}
.tab:hover {
color: #c9d1d9;
}
.tab.active {
background: #0d1117;
color: #58a6ff;
border-bottom: 3px solid #58a6ff;
}
.editor-container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.code-editor {
flex: 1;
background: #0d1117;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
}
.code-content {
flex: 1;
overflow: auto;
padding: 28px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 21px;
line-height: 1.7;
}
.code-content pre {
margin: 0;
padding: 0;
background: transparent;
border: none;
}
.code-content code {
color: #c9d1d9;
text-shadow: none;
}
.line {
display: flex;
min-height: 31px;
}
.line-number {
width: 70px;
text-align: right;
padding-right: 22px;
color: #6e7681;
user-select: none;
flex-shrink: 0;
}
.line-content {
flex: 1;
white-space: pre;
padding-right: 22px;
color: #c9d1d9;
/* Explicit light color for code */
}
/* Ensure Prism.js tokens are visible */
.line-content pre {
color: #c9d1d9;
margin: 0;
}
.terminal {
height: 250px;
background: #0d1117;
border-top: 1px solid #30363d;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.terminal-header {
padding: 11px 22px;
background: #161b22;
font-size: 17px;
display: flex;
align-items: center;
gap: 17px;
border-bottom: 1px solid #30363d;
}
.terminal-tab {
padding: 6px 17px;
cursor: pointer;
border-radius: 6px;
}
.terminal-tab.active {
background: #1e1e1e;
color: #58a6ff;
}
.terminal-content {
flex: 1;
overflow-y: auto;
padding: 17px 28px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
font-size: 18px;
line-height: 1.7;
}
.terminal-line {
margin-bottom: 6px;
}
.terminal-prompt {
color: #4ec9b0;
}
.terminal-success {
color: #4ec9b0;
}
.terminal-info {
color: #4fc3f7;
}
.terminal-error {
color: #f48771;
}
.terminal-output {
color: #dcdcaa;
padding-left: 0;
}
/* Scrollbar styling */
.code-content::-webkit-scrollbar,
.terminal-content::-webkit-scrollbar,
.file-tree::-webkit-scrollbar {
width: 14px;
}
.code-content::-webkit-scrollbar-track,
.terminal-content::-webkit-scrollbar-track,
.file-tree::-webkit-scrollbar-track {
background: #0d1117;
}
.code-content::-webkit-scrollbar-thumb,
.terminal-content::-webkit-scrollbar-thumb,
.file-tree::-webkit-scrollbar-thumb {
background: #30363d;
border-radius: 7px;
}
.code-content::-webkit-scrollbar-thumb:hover,
.terminal-content::-webkit-scrollbar-thumb:hover,
.file-tree::-webkit-scrollbar-thumb:hover {
background: #484f58;
}
.typing-cursor {
display: inline-block;
color: #58a6ff;
font-weight: bold;
animation: blink 0.7s infinite;
font-size: 1.2em;
line-height: 1;
margin-left: -1px;
vertical-align: text-bottom;
z-index: 10;
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: 0;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-left">
{% if logo_path %}
<img src="{{ logo_path }}" alt="Logo" class="logo">
{% endif %}
<div>
<div class="header-title">{{ title }}</div>
<div class="header-subtitle">{{ subtitle }}</div>
</div>
</div>
</div>
<div class="main-content">
<div class="sidebar">
<div class="sidebar-header">πŸ“ Project Files</div>
<div class="file-tree" id="fileTree"></div>
</div>
<div class="editor-section">
<div class="tabs" id="tabsContainer"></div>
<div class="editor-container">
<div class="code-editor">
<div class="code-content" id="codeContent"></div>
</div>
</div>
<div class="terminal">
<div class="terminal-header">
<div class="terminal-tab active">πŸ’» Terminal Output</div>
</div>
<div class="terminal-content" id="terminalContent">
<div class="terminal-line terminal-prompt">$</div>
</div>
</div>
</div>
</div>
</div>
<!-- CRITICAL: Prism must load BEFORE our animation script -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-java.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-csharp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-go.min.js"></script>
<!-- Inline Timeline-Based Animation Engine -->
<script>
/**
* Timeline-Based Animation Engine for LLD Code Walkthrough
* Uses single audio file + timestamps for rock-solid sync
*/
class TimelineAnimator {
constructor(manifest, codeFiles) {
this.manifest = manifest;
this.codeFiles = codeFiles;
this.globalAudio = null;
this.animationFrameId = null;
this.isAnimating = false;
this.currentFile = null;
}
async start() {
if (!this.manifest || !this.manifest.steps || this.manifest.steps.length === 0) {
console.error("No manifest steps found");
return;
}
console.log("Starting Timeline-Based Animation...");
this.isAnimating = true;
// Load single audio file
const audioFile = this.manifest.audio_file || this.manifest.steps[0]?.audio_file;
if (!audioFile) {
console.error("No audio file found in manifest");
return;
}
this.globalAudio = new Audio(audioFile);
// Wait for audio to be ready
await new Promise((resolve, reject) => {
this.globalAudio.oncanplaythrough = resolve;
this.globalAudio.onerror = reject;
this.globalAudio.load();
});
// Start playback
await this.globalAudio.play();
console.log(`Audio playing: ${audioFile}`);
// Start timeline scheduler
this.scheduleTimeline(this.manifest.steps);
}
scheduleTimeline(steps) {
let currentStepIndex = 0;
const stepsState = steps.map(() => ({ started: false, completed: false }));
const tick = () => {
if (!this.globalAudio || this.globalAudio.paused || this.globalAudio.ended) {
console.log("Animation Complete");
this.isAnimating = false;
return;
}
const currentTime = this.globalAudio.currentTime;
const step = steps[currentStepIndex];
const state = stepsState[currentStepIndex];
if (!step) {
return;
}
// Start step when we reach t_start
if (currentTime >= step.t_start && !state.started) {
state.started = true;
console.log(`[${currentTime.toFixed(2)}s] Starting step ${currentStepIndex}: ${step.target_file}`);
this.executeStepVisual(step, currentStepIndex);
}
// Move to next step when we reach t_end
if (currentTime >= step.t_end && state.started && !state.completed) {
state.completed = true;
currentStepIndex++;
}
this.animationFrameId = requestAnimationFrame(tick);
};
tick();
}
async executeStepVisual(step, index) {
// 1. Switch File if needed (with pause for context)
if (step.action !== 'show_output' && this.currentFile !== step.target_file) {
this.switchFile(step.target_file);
this.currentFile = step.target_file;
// Pause for context switch
await new Promise(r => setTimeout(r, 1500));
}
// HANDLE EXECUTION OUTPUT
if (step.action === 'show_output') {
console.log("Showing Output...");
const terminalContent = document.getElementById('terminalContent');
// Clear previous prompt if last line is just a prompt
// Actually, let's just append.
const outputText = executionOutput[step.target_file] || executionOutput['Main.java'] || "Execution complete.";
// Add command line
const cmdDiv = document.createElement('div');
cmdDiv.className = 'terminal-line';
cmdDiv.innerHTML = `<span class="terminal-prompt">$</span> run ${step.target_file || 'Main'}`;
terminalContent.appendChild(cmdDiv);
// Add output lines with typing effect
const outputLines = outputText.split('\n');
for (let line of outputLines) {
const div = document.createElement('div');
div.className = 'terminal-line terminal-output';
terminalContent.appendChild(div);
// Type the line fast
let buffer = "";
for (let char of line) {
buffer += char;
div.textContent = buffer;
// small delay
await new Promise(r => setTimeout(r, 10));
}
// Scroll to bottom
terminalContent.scrollTop = terminalContent.scrollHeight;
await new Promise(r => setTimeout(r, 100)); // slight pause per line
}
// Add new prompt
const newPrompt = document.createElement('div');
newPrompt.className = 'terminal-line terminal-prompt';
newPrompt.textContent = "$";
terminalContent.appendChild(newPrompt);
terminalContent.scrollTop = terminalContent.scrollHeight;
return; // End of step
}
// 2. Type Code based on timeline (For 'type_code' action)
if (step.action === 'wait') return;
const filename = step.target_file;
const fullCode = this.codeFiles[filename];
if (!fullCode) return;
const allLines = fullCode.split('\n');
// Check undefined to allow 0
const startLine = (step.start_line !== undefined) ? step.start_line : 1;
const endLine = (step.end_line !== undefined) ? step.end_line : allLines.length;
// Get target lines
const linesToType = allLines.slice(startLine - 1, endLine);
// Calculate available time window
const availableTime = (step.t_end - step.t_start) * 1000;
// Calculate speed
let totalChars = linesToType.reduce((acc, line) => acc + line.length, 0);
let msPerChar = (availableTime * 0.9) / (totalChars || 1);
// Clamp speed to human-like limits
msPerChar = Math.max(25, Math.min(msPerChar, 90));
// Typing Loop with Drift Correction
const container = document.getElementById('codeContent');
const language = this.getLanguage(filename);
// Cursor
let cursor = document.getElementById('typing-cursor');
if (!cursor) {
cursor = document.createElement('span');
cursor.id = 'typing-cursor';
cursor.className = 'typing-cursor';
cursor.innerHTML = 'β–Ž';
}
const startTime = Date.now();
let charsTypedTotal = 0;
for (let i = 0; i < linesToType.length; i++) {
const lineIdx = startLine - 1 + i;
const lineText = linesToType[i];
let lineDiv = container.children[lineIdx];
if (!lineDiv) continue;
let lineContent = lineDiv.querySelector('.line-content');
let currentBuffer = "";
if (lineText.length === 0) {
lineContent.innerHTML = '';
lineContent.appendChild(cursor);
}
for (let char of lineText) {
currentBuffer += char;
charsTypedTotal++;
let highlighted = "";
if (Prism && Prism.highlight) {
highlighted = Prism.highlight(
currentBuffer,
Prism.languages[language] || Prism.languages.javascript,
language
);
} else {
highlighted = currentBuffer
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}
lineContent.innerHTML = `<pre style="margin:0; display:inline;">${highlighted}</pre>`;
lineContent.appendChild(cursor);
cursor.scrollIntoView({ block: "nearest", inline: "nearest" });
// DRIFT CORRECTION
const elapsed = Date.now() - startTime;
const targetTime = charsTypedTotal * msPerChar;
const delay = targetTime - elapsed;
if (delay > 0) {
await new Promise(r => setTimeout(r, delay));
} else {
await new Promise(r => setTimeout(r, 0));
}
}
}
}
switchFile(filename) {
document.querySelectorAll('.file-item').forEach(item => {
item.classList.toggle('active', item.dataset.file === filename);
});
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.file === filename);
});
// CRITICAL: Clear and re-render code editor for new file
const codeContent = document.getElementById('codeContent');
codeContent.innerHTML = '';
const fullCode = this.codeFiles[filename] || '';
const lines = fullCode.split('\n');
lines.forEach((lineText, idx) => {
const lineDiv = document.createElement('div');
lineDiv.className = 'line';
const lineNum = document.createElement('div');
lineNum.className = 'line-number';
lineNum.textContent = idx + 1;
const lineContent = document.createElement('div');
lineContent.className = 'line-content';
lineContent.innerHTML = ''; // Start empty for typing animation
lineDiv.appendChild(lineNum);
lineDiv.appendChild(lineContent);
codeContent.appendChild(lineDiv);
});
}
getLanguage(filename) {
const ext = filename.split('.').pop().toLowerCase();
return {
'js': 'javascript', 'py': 'python', 'java': 'java', 'cpp': 'cpp',
'c': 'c', 'cs': 'csharp', 'go': 'go'
}[ext] || 'javascript';
}
}
</script>
<!-- Template Variables and Initialization -->
<script>
const codeFiles = {{ code_files | tojson }};
const manifest = {{ manifest | default (none) | tojson }};
const autoRun = {{ auto_run | tojson }};
const executionOutput = {{ execution_output | tojson }};
// State
let currentFile = null;
let isAnimating = false;
let currentAudio = null;
// Progress tracking: { filename: last_line_fully_typed }
let fileLineProgress = {};
function initializeUI() {
const fileTree = document.getElementById('fileTree');
const tabsContainer = document.getElementById('tabsContainer');
fileTree.innerHTML = '';
tabsContainer.innerHTML = '';
const files = Object.keys(codeFiles);
if (files.length === 0) return;
// Initialize progress trackers
files.forEach(f => fileLineProgress[f] = 0);
// Default view
currentFile = files[0];
files.forEach((filename) => {
const ext = filename.split('.').pop().toLowerCase();
const iconMap = {
'py': '🐍', 'java': 'β˜•', 'cpp': 'βš™οΈ', 'c': 'πŸ–₯️',
'js': 'πŸ“œ', 'ts': 'πŸ“˜', 'cs': '#️⃣', 'go': 'πŸ”·'
};
const icon = iconMap[ext] || 'πŸ“„';
const isActive = filename === currentFile;
const fileItem = document.createElement('div');
fileItem.className = `file-item ${isActive ? 'active' : ''}`;
fileItem.dataset.file = filename;
fileItem.innerHTML = `<span class="file-icon">${icon}</span><span>${filename}</span>`;
fileItem.addEventListener('click', () => switchFile(filename));
fileTree.appendChild(fileItem);
const tab = document.createElement('div');
tab.className = `tab ${isActive ? 'active' : ''}`;
tab.dataset.file = filename;
tab.innerHTML = `<span>${icon}</span><span>${filename}</span>`;
tab.addEventListener('click', () => switchFile(filename));
tabsContainer.appendChild(tab);
});
}
function switchFile(filename) {
// Only allow switch if logic permits
currentFile = filename;
// Update Active UI
document.querySelectorAll('.file-item').forEach(item => {
item.classList.toggle('active', item.dataset.file === filename);
});
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.file === filename);
});
// Render content (initially empty or preserved?)
// We need to preserve content if we are switching back.
// But for animation, we usually re-render the whole buffer.
// For now, let's keep it simple: render what we have typed so far.
// (In this simple runner, we might assume sequential, but sidebar implies random access.
// The old template re-renders everything. Let's do that.)
// Actually, fileLineProgress tells us how many lines are *done*.
// But we might be mid-line. For simplicity in this fix, we just clear and re-render if needed.
// But executeStep handles rendering. Manual switching handles static view.
renderStaticView(filename);
}
// Helper to render whatever is currently "done" or initial setup
function renderStaticView(filename) {
const codeContent = document.getElementById('codeContent');
// If we are animating, we don't want to destroy the DOM unless necessary.
// But if user clicks sidebar, step logic might override.
// We'll prepare the lines as empty divs if not present.
codeContent.innerHTML = '';
const fullCode = codeFiles[filename] || '';
const lines = fullCode.split('\n');
lines.forEach((lineText, idx) => {
const lineDiv = document.createElement('div');
lineDiv.className = 'line';
const lineNum = document.createElement('div');
lineNum.className = 'line-number';
lineNum.textContent = idx + 1;
const lineContent = document.createElement('div');
lineContent.className = 'line-content';
// Initially empty or full?
// If we assume manual pipeline runs from 0 to N, we start empty.
lineContent.innerHTML = '';
lineDiv.appendChild(lineNum);
lineDiv.appendChild(lineContent);
codeContent.appendChild(lineDiv);
});
}
function getLanguage(filename) {
const ext = filename.split('.').pop().toLowerCase();
return {
'js': 'javascript', 'py': 'python', 'java': 'java', 'cpp': 'cpp',
'c': 'c', 'cs': 'csharp', 'go': 'go'
}[ext] || 'javascript';
}
// --- TIMELINE-BASED ANIMATION (See lld_timeline_animation.js) ---
document.addEventListener('DOMContentLoaded', () => {
// Init with first file
initializeUI();
if (codeFiles) {
// Pre-populate initial empty lines structure
const first = Object.keys(codeFiles)[0];
if (first) renderStaticView(first);
}
if (autoRun && manifest) {
// Use timeline-based animator
const animator = new TimelineAnimator(manifest, codeFiles);
setTimeout(() => animator.start(), 1000);
}
});
</script>
</body>
</html>