code-sketch / templates /index.html
duqing2026's picture
优化
b0fefeb
Raw
History Blame Contribute Delete
31.5 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodeSketch</title>
<!-- Split.js for Resizable Layout -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/split.js/1.6.0/split.min.js"></script>
<!-- JSON Formatter for Console -->
<script src="https://cdn.jsdelivr.net/npm/json-formatter-js@2.3.4/dist/json-formatter.umd.min.js"></script>
<style>
:root {
--bg-color: #1e1e1e;
--sidebar-bg: #252526;
--sidebar-hover: #2a2d2e;
--sidebar-active: #37373d;
--border-color: #333;
--text-color: #cccccc;
--accent-color: #007acc;
--console-bg: #1e1e1e;
--console-text: #e0e0e0;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
header {
height: 50px;
background-color: #333333;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid var(--border-color);
justify-content: space-between;
flex-shrink: 0; /* Prevent shrinking */
}
h1 {
font-size: 18px;
font-weight: 500;
color: #fff;
}
.run-btn {
background-color: #0e639c;
color: white;
border: none;
padding: 6px 16px;
border-radius: 2px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.run-btn:hover {
background-color: #1177bb;
}
/* Split.js Layout */
.main-container {
display: flex;
flex: 1;
overflow: hidden;
height: calc(100vh - 50px);
}
/* Gutter Styles for Split.js */
.gutter {
background-color: var(--bg-color);
background-repeat: no-repeat;
background-position: 50%;
cursor: col-resize;
}
.gutter:hover {
background-color: var(--accent-color);
}
/* Left Sidebar: Problem List */
.sidebar {
background-color: var(--sidebar-bg);
display: flex;
flex-direction: column;
overflow: hidden;
/* width is handled by Split.js */
}
.sidebar-header {
padding: 10px 15px;
font-size: 12px;
text-transform: uppercase;
font-weight: 600;
color: #888;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.add-btn {
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 18px;
line-height: 1;
}
.add-btn:hover { color: #fff; }
.problem-list {
flex: 1;
overflow-y: auto;
list-style: none;
}
.problem-item {
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border-left: 3px solid transparent;
display: flex;
align-items: center;
position: relative; /* For absolute positioning of actions */
height: 32px; /* Fixed height for consistency */
}
.problem-title {
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 5px; /* Spacing from potential actions */
}
.item-actions {
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
display: flex;
gap: 4px;
opacity: 0; /* Hidden by default */
transition: opacity 0.2s ease;
background-color: var(--sidebar-bg); /* Cover text behind */
padding-left: 8px; /* Fade effect spacer if using gradient, here just solid bg */
box-shadow: -10px 0 10px -5px var(--sidebar-bg); /* Soft edge */
}
.problem-item:hover .item-actions {
opacity: 1;
}
.problem-item.active .item-actions {
background-color: var(--sidebar-active); /* Match active bg */
box-shadow: -10px 0 10px -5px var(--sidebar-active);
}
.problem-item:hover .item-actions {
background-color: var(--sidebar-hover);
box-shadow: -10px 0 10px -5px var(--sidebar-hover);
}
.action-btn {
background: none;
border: none;
color: #888;
cursor: pointer;
padding: 4px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
}
.action-btn:hover {
color: #e0e0e0;
background-color: rgba(255, 255, 255, 0.1);
}
.action-btn svg {
width: 14px;
height: 14px;
fill: currentColor;
}
.problem-item.active {
background-color: var(--sidebar-active);
border-left-color: var(--accent-color);
color: #fff;
}
/* Center: Editor */
.editor-container {
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
#monaco-editor {
width: 100%;
height: 100%;
}
/* Right: Console Output */
.output-panel {
background-color: var(--console-bg);
display: flex;
flex-direction: column;
overflow: hidden;
}
.output-header {
padding: 10px 15px;
background-color: #252526;
border-bottom: 1px solid var(--border-color);
font-size: 12px;
text-transform: uppercase;
font-weight: 600;
display: flex;
justify-content: space-between;
}
.clear-btn {
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 12px;
}
.clear-btn:hover { color: #fff; }
#console-output {
flex: 1;
padding: 10px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
overflow-y: auto;
/* white-space: pre-wrap; Removed to allow JSONFormatter to handle layout */
color: var(--console-text);
}
.log-line {
margin-bottom: 4px;
border-bottom: 1px solid #2a2a2a;
padding-bottom: 2px;
display: flex; /* For alignment */
align-items: flex-start;
}
.log-error { color: #f48771; background-color: rgba(255, 0, 0, 0.1); }
.log-warn { color: #cca700; background-color: rgba(255, 255, 0, 0.1); }
.log-info { color: #e0e0e0; }
/* JSON Formatter Tweaks for Dark Mode */
.json-formatter-row {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
.json-formatter-row .json-formatter-key {
color: #9cdcfe !important;
}
.json-formatter-row .json-formatter-string {
color: #ce9178 !important;
}
.json-formatter-row .json-formatter-number {
color: #b5cea8 !important;
}
.json-formatter-row .json-formatter-boolean {
color: #569cd6 !important;
}
.json-formatter-row .json-formatter-null {
color: #569cd6 !important;
}
.log-content {
display: flex;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
/* Loading Overlay */
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
color: #888;
}
/* Category Group Styles */
.category-group {
margin-bottom: 5px;
}
.category-header {
padding: 0 15px; /* Taller */
font-size: 12px;
font-weight: bold;
color: #aaa;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between; /* Push add button to right */
user-select: none;
text-transform: uppercase;
border-bottom: 1px solid rgba(255,255,255,0.05); /* Subtle separator */
}
.category-header:hover {
color: #fff;
background-color: rgba(255,255,255,0.05);
}
.category-header svg {
width: 14px; /* Slightly larger */
height: 14px;
margin-right: 8px;
transition: transform 0.2s;
fill: currentColor;
}
.category-header .add-btn {
background: none;
border: none;
color: #888;
cursor: pointer;
font-size: 20px; /* Larger */
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
line-height: 1;
padding: 0;
margin-left: 10px;
}
.category-header .add-btn:hover {
color: #fff;
background-color: rgba(255, 255, 255, 0.2);
}
.category-header.collapsed svg {
transform: rotate(-90deg);
}
.category-list {
list-style: none;
padding-left: 0;
display: block;
}
.category-list.collapsed {
display: none;
}
/* Pinned Item Styles */
.pinned-icon {
color: #d7ba7d; /* Gold-ish for pinned */
margin-right: 5px;
width: 12px;
height: 12px;
fill: currentColor;
display: inline-block;
vertical-align: middle;
}
.action-btn.pinned {
color: #d7ba7d;
}
</style>
</head>
<body>
<header>
<h1>CodeSketch</h1>
<button class="run-btn" onclick="runCode()">
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M4 2.5L13.5 8L4 13.5V2.5Z"/>
</svg>
运行代码 (Run)
</button>
</header>
<div class="main-container">
<div id="split-0" class="sidebar">
<div class="sidebar-header">
题目列表
</div>
<ul class="problem-list" id="problem-list">
<!-- Items will be injected here -->
<div class="loading">Loading...</div>
</ul>
</div>
<div id="split-1" class="editor-container">
<div id="monaco-editor"></div>
</div>
<div id="split-2" class="output-panel">
<div class="output-header">
<span>Console</span>
<button class="clear-btn" onclick="clearConsole()">Clear</button>
</div>
<div id="console-output">
<div class="log-info">
<span class="log-content">Click 'Run' to see output...</span>
</div>
</div>
</div>
</div>
<!-- Monaco Editor Loader -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"></script>
<script>
let editor;
let currentProblemId = null;
let autoSaveTimer = null;
let isInternalUpdate = false;
// Initialize Split.js
Split(['#split-0', '#split-1', '#split-2'], {
sizes: [20, 50, 30], // Initial percentages
minSize: [150, 200, 200],
gutterSize: 6,
cursor: 'col-resize',
onDragEnd: function() {
if (editor) editor.layout(); // Resize editor when layout changes
}
});
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs' }});
require(['vs/editor/editor.main'], function() {
// Enhance JS/TS IntelliSense
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES2020,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.CommonJS,
noEmit: true,
// Enable type checking for better suggestions
checkJs: true
});
// Add some basic lib definitions if needed (optional)
// monaco.languages.typescript.javascriptDefaults.addExtraLib(...)
editor = monaco.editor.create(document.getElementById('monaco-editor'), {
value: '// Select a problem to start coding...',
language: 'javascript',
theme: 'vs-dark',
automaticLayout: true, // Auto resize with container
minimap: { enabled: false },
fontSize: 14,
scrollBeyondLastLine: false,
// IntelliSense features
suggest: {
showWords: false // Prefer intelligent suggestions over text matches
},
quickSuggestions: true,
parameterHints: { enabled: true }
});
// Initialize
fetchProblems();
// Auto Save Listener
editor.onDidChangeModelContent(() => {
if (currentProblemId && !isInternalUpdate) {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(saveProblem, 1000);
}
});
});
async function fetchProblems(selectId = null) {
try {
const res = await fetch('/api/problems');
const problems = await res.json();
renderProblemList(problems);
if (problems.length > 0) {
const target = selectId
? problems.find(p => p.id === selectId)
: (!currentProblemId ? problems[0] : problems.find(p => p.id === currentProblemId));
if (target) loadProblem(target);
else if (problems.length > 0) loadProblem(problems[0]);
}
} catch (e) {
console.error("Failed to fetch problems:", e);
}
}
const CATEGORIES = ['练习题', '面试题', '手撕题', '算法题'];
function renderProblemList(problems) {
const listEl = document.getElementById('problem-list');
listEl.innerHTML = '';
// Group problems
const grouped = {};
CATEGORIES.forEach(c => grouped[c] = []);
problems.forEach(p => {
const targetCat = CATEGORIES.includes(p.category) ? p.category : '练习题';
grouped[targetCat].push(p);
});
CATEGORIES.forEach(category => {
const items = grouped[category];
// Sort: Pinned first, then by ctime desc (backend sent ctime desc)
items.sort((a, b) => {
if (!!a.pinned === !!b.pinned) return 0; // Stable sort for same pin status
return a.pinned ? -1 : 1;
});
// Create Category Group
const groupDiv = document.createElement('div');
groupDiv.className = 'category-group';
// Header
const header = document.createElement('div');
header.className = 'category-header';
// Header Content
const headerContent = document.createElement('div');
headerContent.style.display = 'flex';
headerContent.style.alignItems = 'center';
headerContent.innerHTML = `
<svg viewBox="0 0 16 16"><path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/></svg>
${category} (${items.length})
`;
header.appendChild(headerContent);
// Add Button in Header
const addBtn = document.createElement('button');
addBtn.className = 'add-btn';
addBtn.innerHTML = '+';
addBtn.title = '新建题目';
// Removed inline styles to let CSS take over
addBtn.onclick = (e) => {
e.stopPropagation();
createProblem(category);
};
header.appendChild(addBtn);
// List
const ul = document.createElement('ul');
ul.className = 'category-list';
// Toggle Handler
header.onclick = (e) => {
if (e.target === addBtn) return;
header.classList.toggle('collapsed');
ul.classList.toggle('collapsed');
};
groupDiv.appendChild(header);
items.forEach((p, index) => {
const li = document.createElement('li');
li.className = 'problem-item';
li.dataset.id = p.id;
li.onclick = () => loadProblem(p);
// Title Span
const titleSpan = document.createElement('span');
titleSpan.className = 'problem-title';
// Clean title: remove existing leading numbering (e.g. "1. ", "01 ", "1、")
const cleanTitle = p.title.replace(/^\d+[\.\s、]+/, '');
const displayTitle = `${index + 1}. ${cleanTitle}`;
// Pinned Icon in Title
if (p.pinned) {
titleSpan.innerHTML = `<svg class="pinned-icon" viewBox="0 0 16 16"><path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5a.5.5 0 0 1-1 0V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A5.921 5.921 0 0 1 5 6.708V2.277a2.77 2.77 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354z"/></svg>`;
titleSpan.appendChild(document.createTextNode(displayTitle));
} else {
titleSpan.textContent = displayTitle;
}
li.appendChild(titleSpan);
// Actions Div
const actionsDiv = document.createElement('div');
actionsDiv.className = 'item-actions';
// Pin Button
const pinBtn = document.createElement('button');
pinBtn.className = `action-btn ${p.pinned ? 'pinned' : ''}`;
pinBtn.title = p.pinned ? '取消置顶' : '置顶';
pinBtn.innerHTML = '<svg viewBox="0 0 16 16"><path d="M4.146.146A.5.5 0 0 1 4.5 0h7a.5.5 0 0 1 .5.5c0 .68-.342 1.174-.646 1.479-.126.125-.25.224-.354.298v4.431l.078.048c.203.127.476.314.751.555C12.36 7.775 13 8.527 13 9.5a.5.5 0 0 1-.5.5h-4v4.5a.5.5 0 0 1-1 0V10h-4a.5.5 0 0 1-.5-.5c0-.973.64-1.725 1.17-2.189A5.921 5.921 0 0 1 5 6.708V2.277a2.77 2.77 0 0 1-.354-.298C4.342 1.674 4 1.179 4 .5a.5.5 0 0 1 .146-.354z"/></svg>';
pinBtn.onclick = (e) => {
e.stopPropagation();
togglePin(p);
};
actionsDiv.appendChild(pinBtn);
// Rename Button
const renameBtn = document.createElement('button');
renameBtn.className = 'action-btn';
renameBtn.title = '重命名';
// Edit Icon (Pencil)
renameBtn.innerHTML = '<svg viewBox="0 0 16 16"><path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/></svg>';
renameBtn.onclick = (e) => {
e.stopPropagation();
renameProblem(p.id);
};
actionsDiv.appendChild(renameBtn);
// Delete Button
const deleteBtn = document.createElement('button');
deleteBtn.className = 'action-btn';
deleteBtn.title = '删除';
// Trash Icon
deleteBtn.innerHTML = '<svg viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteProblem(p.id);
};
actionsDiv.appendChild(deleteBtn);
li.appendChild(actionsDiv);
ul.appendChild(li);
});
groupDiv.appendChild(ul);
listEl.appendChild(groupDiv);
});
if (currentProblemId) {
const activeEl = listEl.querySelector(`.problem-item[data-id="${currentProblemId}"]`);
if (activeEl) activeEl.classList.add('active');
}
}
async function togglePin(problem) {
try {
const res = await fetch(`/api/problems/${problem.id}`);
const content = await res.text();
let newContent;
// Check if @pinned exists
const hasPinned = /@pinned\s+(true|false)/i.test(content);
const isPinned = problem.pinned;
if (hasPinned) {
// Replace existing
newContent = content.replace(/@pinned\s+(true|false)/i, `@pinned ${!isPinned}`);
} else {
// Add after @title or @category
if (content.includes('@category')) {
newContent = content.replace(/(@category\s+.+)/, `$1\n * @pinned ${!isPinned}`);
} else if (content.includes('@title')) {
newContent = content.replace(/(@title\s+.+)/, `$1\n * @pinned ${!isPinned}`);
} else {
// Just prepend
newContent = `/**\n * @pinned ${!isPinned}\n */\n` + content;
}
}
// Save
const saveRes = await fetch(`/api/problems/${problem.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: newContent })
});
if (saveRes.ok) {
// Refresh list
await fetchProblems(currentProblemId);
}
} catch (e) {
console.error("Toggle pin failed", e);
}
}
async function renameProblem(id) {
// Strip extension for display if you want, but keep it simple for now
const currentName = id.replace(/\.(js|ts)$/, '');
const newName = prompt("请输入新文件名 (不含后缀):", currentName);
if (!newName || newName === currentName) return;
try {
const res = await fetch(`/api/problems/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ new_filename: newName })
});
if (res.ok) {
const newMeta = await res.json();
// If we renamed the currently open problem, update the ID
if (currentProblemId === id) {
currentProblemId = newMeta.id;
}
await fetchProblems(currentProblemId);
logToConsole(['Renamed to:', newMeta.title], 'info');
} else {
const err = await res.json();
alert(`Error: ${err.error}`);
}
} catch (e) {
alert(`Error: ${e.message}`);
}
}
async function deleteProblem(id) {
if (!confirm(`确定要删除 "${id}" 吗? 此操作无法撤销。`)) return;
try {
const res = await fetch(`/api/problems/${id}`, {
method: 'DELETE'
});
if (res.ok) {
logToConsole(['Deleted:', id], 'warn');
if (currentProblemId === id) {
currentProblemId = null;
if (editor) editor.setValue('// Select a problem to start coding...');
}
await fetchProblems();
} else {
const err = await res.json();
alert(`Error: ${err.error}`);
}
} catch (e) {
alert(`Error: ${e.message}`);
}
}
async function loadProblem(problem) {
if (currentProblemId === problem.id && editor.getValue() === problem.content) return;
currentProblemId = problem.id;
document.querySelectorAll('.problem-item').forEach(el => {
el.classList.toggle('active', el.dataset.id === problem.id);
});
if (editor) {
isInternalUpdate = true;
editor.setValue(problem.content);
isInternalUpdate = false;
}
clearConsole();
logToConsole(['Loaded:', problem.title], 'info');
}
async function createProblem(category = '练习题') {
const filename = prompt(`[${category}] 请输入新题目文件名 (例如: 03_new_problem):`, "new_problem");
if (!filename) return;
try {
const res = await fetch('/api/problems', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename, category })
});
if (res.ok) {
const newProblem = await res.json();
await fetchProblems(newProblem.id);
logToConsole(['Created:', newProblem.title], 'info');
} else {
const err = await res.json();
alert(`Error: ${err.error}`);
}
} catch (e) {
alert(`Error: ${e.message}`);
}
}
async function saveProblem() {
if (!currentProblemId || !editor) return;
const content = editor.getValue();
try {
const res = await fetch(`/api/problems/${currentProblemId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
if (res.ok) {
const updatedMeta = await res.json();
const listItem = document.querySelector(`.problem-item[data-id="${currentProblemId}"]`);
if (listItem && listItem.textContent !== updatedMeta.title) {
listItem.textContent = updatedMeta.title;
}
}
} catch (e) {
console.error("Save failed:", e);
logToConsole(["Auto-save failed"], 'error');
}
}
function runCode() {
if (!editor) return;
const code = editor.getValue();
clearConsole();
logToConsole(["Running..."], 'info');
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
try {
console.log = (...args) => {
logToConsole(args, 'info');
};
console.error = (...args) => {
logToConsole(args, 'error');
};
console.warn = (...args) => {
logToConsole(args, 'warn');
};
// Using new Function is safer than eval for scope isolation, but we want quick results
// Note: Objects printed will be live references if we don't clone them,
// but for a console that's usually desired or acceptable.
// If async is needed:
const wrappedCode = `(async () => { \n${code}\n })();`;
eval(code);
} catch (e) {
logToConsole([e], 'error');
} finally {
console.log = originalLog;
console.error = originalError;
console.warn = originalWarn;
}
}
// New Log Function supporting Objects
function logToConsole(args, type = 'info') {
const outputEl = document.getElementById('console-output');
const lineDiv = document.createElement('div');
lineDiv.className = `log-line log-${type}`;
const contentDiv = document.createElement('div');
contentDiv.className = 'log-content';
args.forEach(arg => {
if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean' || arg === null || arg === undefined) {
const span = document.createElement('span');
span.textContent = String(arg);
span.style.marginRight = '5px';
contentDiv.appendChild(span);
} else {
// Use JSONFormatter for objects/arrays
try {
const formatter = new JSONFormatter(arg, 1, { // 1 = open depth
theme: 'dark',
hoverPreviewEnabled: true
});
contentDiv.appendChild(formatter.render());
} catch (e) {
const span = document.createElement('span');
span.textContent = String(arg);
contentDiv.appendChild(span);
}
}
});
lineDiv.appendChild(contentDiv);
outputEl.appendChild(lineDiv);
outputEl.scrollTop = outputEl.scrollHeight;
}
function clearConsole() {
const outputEl = document.getElementById('console-output');
outputEl.innerHTML = '';
}
</script>
</body>
</html>