Files
session-viewer/session-viewer-architecture.html
teernisse b69dffc398 Add interactive architecture explorer as standalone HTML reference
Self-contained 701-line HTML document providing a visual map of the
session-viewer codebase architecture. Includes interactive node
diagrams for client, server, shared, and build layers with
dependency relationships.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 23:05:11 -05:00

702 lines
33 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Viewer — Architecture Explorer</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f1117; --surface: #181a20; --surface2: #1e2028;
--border: #2a2d37; --border-hover: #3a3d47;
--fg: #e1e3ea; --fg2: #9ca0ad; --fg3: #6b7080;
--blue: #3b82f6; --green: #10b981; --amber: #f59e0b;
--red: #ef4444; --purple: #a78bfa; --pink: #ec4899;
--cyan: #06b6d4; --orange: #f97316;
--node-client: #1e3a5f; --node-client-border: #3b82f6;
--node-server: #3d2e0a; --node-server-border: #f59e0b;
--node-shared: #2d1a4e; --node-shared-border: #a78bfa;
--node-build: #1a3332; --node-build-border: #06b6d4;
--radius: 6px;
}
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--fg); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
code, .mono { font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 0.8em; }
.layout { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 280px; min-width: 280px; border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; padding: 16px; gap: 16px; }
.canvas-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.canvas-wrap { flex: 1; overflow: hidden; position: relative; }
.canvas-wrap svg { width: 100%; height: 100%; }
.prompt-bar { border-top: 1px solid var(--border); padding: 12px 16px; display: flex; align-items: flex-start; gap: 12px; max-height: 180px; overflow-y: auto; background: var(--surface); }
.prompt-text { flex: 1; font-size: 13px; line-height: 1.5; color: var(--fg2); white-space: pre-wrap; }
.prompt-text em { color: var(--fg); font-style: normal; font-weight: 500; }
.section-title { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--fg3); margin-bottom: 6px; }
.preset-row { display: flex; flex-wrap: wrap; gap: 6px; }
.preset-btn { background: var(--surface2); border: 1px solid var(--border); color: var(--fg2); font-size: 12px; padding: 5px 10px; border-radius: var(--radius); cursor: pointer; transition: all 0.15s; }
.preset-btn:hover { border-color: var(--border-hover); color: var(--fg); }
.preset-btn.active { border-color: var(--blue); color: var(--blue); background: rgba(59,130,246,0.1); }
.check-group { display: flex; flex-direction: column; gap: 4px; }
.check-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--fg2); cursor: pointer; padding: 3px 0; }
.check-item input { accent-color: var(--blue); }
.check-item .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.conn-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--fg2); cursor: pointer; padding: 3px 0; }
.conn-item input { accent-color: var(--blue); }
.comments-list { display: flex; flex-direction: column; gap: 6px; }
.comment-card { background: var(--surface2); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 10px; font-size: 12px; position: relative; }
.comment-card .target { font-weight: 600; color: var(--fg); margin-bottom: 2px; }
.comment-card .file { color: var(--fg3); font-size: 11px; }
.comment-card .text { color: var(--fg2); margin-top: 4px; }
.comment-card .del { position: absolute; top: 6px; right: 8px; background: none; border: none; color: var(--fg3); cursor: pointer; font-size: 14px; line-height: 1; }
.comment-card .del:hover { color: var(--red); }
.no-comments { font-size: 12px; color: var(--fg3); font-style: italic; }
.copy-btn { background: var(--blue); color: #fff; border: none; padding: 6px 14px; border-radius: var(--radius); font-size: 12px; font-weight: 500; cursor: pointer; white-space: nowrap; flex-shrink: 0; transition: opacity 0.15s; }
.copy-btn:hover { opacity: 0.85; }
.copy-btn.copied { background: var(--green); }
.zoom-controls { position: absolute; bottom: 12px; right: 12px; display: flex; gap: 4px; }
.zoom-btn { background: var(--surface); border: 1px solid var(--border); color: var(--fg2); width: 30px; height: 30px; border-radius: var(--radius); cursor: pointer; font-size: 16px; display: flex; align-items: center; justify-content: center; }
.zoom-btn:hover { border-color: var(--border-hover); color: var(--fg); }
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.open { display: flex; }
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 20px; width: 400px; max-width: 90vw; }
.modal h3 { font-size: 15px; margin-bottom: 2px; }
.modal .modal-file { font-size: 12px; color: var(--fg3); margin-bottom: 12px; }
.modal textarea { width: 100%; height: 80px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); color: var(--fg); padding: 8px; font-size: 13px; resize: vertical; font-family: inherit; }
.modal textarea:focus { outline: none; border-color: var(--blue); }
.modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
.modal-actions button { padding: 6px 14px; border-radius: var(--radius); font-size: 13px; cursor: pointer; border: 1px solid var(--border); }
.modal-actions .cancel { background: var(--surface2); color: var(--fg2); }
.modal-actions .save { background: var(--blue); color: #fff; border-color: var(--blue); }
svg text { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; }
.node-group { cursor: pointer; }
.node-group:hover rect { filter: brightness(1.2); }
.node-group.commented rect { stroke-width: 2.5; }
.layer-label { fill: var(--fg3); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
</style>
</head>
<body>
<div class="layout">
<div class="sidebar">
<div>
<div class="section-title">View Presets</div>
<div class="preset-row" id="presets"></div>
</div>
<div>
<div class="section-title">Layers</div>
<div class="check-group" id="layers"></div>
</div>
<div>
<div class="section-title">Connections</div>
<div class="check-group" id="connections"></div>
</div>
<div>
<div class="section-title">Comments <span id="comment-count"></span></div>
<div class="comments-list" id="comments-list"></div>
</div>
</div>
<div class="canvas-area">
<div class="canvas-wrap" id="canvas-wrap">
<svg id="diagram" xmlns="http://www.w3.org/2000/svg"></svg>
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoom(-0.15)">&#x2212;</button>
<button class="zoom-btn" onclick="resetZoom()">&#x2302;</button>
<button class="zoom-btn" onclick="zoom(0.15)">+</button>
</div>
</div>
<div class="prompt-bar">
<div class="prompt-text" id="prompt-text"></div>
<button class="copy-btn" id="copy-btn" onclick="copyPrompt()">Copy</button>
</div>
</div>
</div>
<div class="modal-overlay" id="modal">
<div class="modal">
<h3 id="modal-title"></h3>
<div class="modal-file" id="modal-file"></div>
<textarea id="modal-input" placeholder="What would you change about this component?"></textarea>
<div class="modal-actions">
<button class="cancel" onclick="closeModal()">Cancel</button>
<button class="save" onclick="saveComment()">Add Comment</button>
</div>
</div>
</div>
<script>
// ─── DATA ───────────────────────────────────────────────────
const LAYERS = [
{ id: 'client', label: 'Client (React)', color: '#3b82f6', fill: '#1e3a5f', border: '#3b82f6' },
{ id: 'server', label: 'Server (Express)', color: '#f59e0b', fill: '#3d2e0a', border: '#f59e0b' },
{ id: 'shared', label: 'Shared', color: '#a78bfa', fill: '#2d1a4e', border: '#a78bfa' },
{ id: 'build', label: 'Build & Tooling', color: '#06b6d4', fill: '#1a3332', border: '#06b6d4' },
];
const CONN_TYPES = [
{ id: 'data', label: 'Data Flow', color: '#3b82f6', dash: '' },
{ id: 'import', label: 'Import / Uses', color: '#6b7280', dash: '4,3' },
{ id: 'render', label: 'Renders', color: '#10b981', dash: '6,3' },
{ id: 'event', label: 'Event / Callback', color: '#f97316', dash: '3,3' },
];
const NODES = [
// Client: Components
{ id: 'app', label: 'App', subtitle: 'src/client/app.tsx', x: 420, y: 50, w: 100, h: 44, layer: 'client' },
{ id: 'session-list', label: 'SessionList', subtitle: 'src/client/components/SessionList.tsx', x: 140, y: 130, w: 130, h: 44, layer: 'client' },
{ id: 'session-viewer', label: 'SessionViewer', subtitle: 'src/client/components/SessionViewer.tsx', x: 350, y: 130, w: 145, h: 44, layer: 'client' },
{ id: 'message-bubble', label: 'MessageBubble', subtitle: 'src/client/components/MessageBubble.tsx', x: 310, y: 210, w: 145, h: 44, layer: 'client' },
{ id: 'filter-panel', label: 'FilterPanel', subtitle: 'src/client/components/FilterPanel.tsx', x: 140, y: 210, w: 130, h: 44, layer: 'client' },
{ id: 'search-bar', label: 'SearchBar', subtitle: 'src/client/components/SearchBar.tsx', x: 560, y: 130, w: 120, h: 44, layer: 'client' },
{ id: 'search-minimap', label: 'SearchMinimap', subtitle: 'src/client/components/SearchMinimap.tsx', x: 530, y: 210, w: 140, h: 44, layer: 'client' },
{ id: 'agent-progress', label: 'AgentProgressView', subtitle: 'src/client/components/AgentProgressView.tsx', x: 720, y: 130, w: 165, h: 44, layer: 'client' },
{ id: 'export-btn', label: 'ExportButton', subtitle: 'src/client/components/ExportButton.tsx', x: 720, y: 210, w: 130, h: 44, layer: 'client' },
{ id: 'error-boundary', label: 'ErrorBoundary', subtitle: 'src/client/components/ErrorBoundary.tsx', x: 600, y: 50, w: 135, h: 44, layer: 'client' },
// Client: Hooks & Libs
{ id: 'use-session', label: 'useSession', subtitle: 'src/client/hooks/useSession.ts', x: 140, y: 290, w: 120, h: 44, layer: 'client' },
{ id: 'use-filters', label: 'useFilters', subtitle: 'src/client/hooks/useFilters.ts', x: 300, y: 290, w: 120, h: 44, layer: 'client' },
{ id: 'agent-parser', label: 'AgentParser', subtitle: 'src/client/lib/agent-progress-parser.ts', x: 530, y: 290, w: 130, h: 44, layer: 'client' },
{ id: 'markdown-lib', label: 'Markdown', subtitle: 'src/client/lib/markdown.ts', x: 720, y: 290, w: 120, h: 44, layer: 'client' },
// Server
{ id: 'sessions-route', label: '/api/sessions', subtitle: 'src/server/routes/sessions.ts', x: 170, y: 420, w: 135, h: 44, layer: 'server' },
{ id: 'export-route', label: '/api/export', subtitle: 'src/server/routes/export.ts', x: 370, y: 420, w: 120, h: 44, layer: 'server' },
{ id: 'session-discovery', label: 'SessionDiscovery', subtitle: 'src/server/services/session-discovery.ts', x: 100, y: 500, w: 160, h: 44, layer: 'server' },
{ id: 'session-parser', label: 'SessionParser', subtitle: 'src/server/services/session-parser.ts', x: 310, y: 500, w: 140, h: 44, layer: 'server' },
{ id: 'progress-grouper', label: 'ProgressGrouper', subtitle: 'src/server/services/progress-grouper.ts', x: 510, y: 500, w: 155, h: 44, layer: 'server' },
{ id: 'html-exporter', label: 'HtmlExporter', subtitle: 'src/server/services/html-exporter.ts', x: 720, y: 500, w: 140, h: 44, layer: 'server' },
// Shared
{ id: 'shared-types', label: 'Types', subtitle: 'src/shared/types.ts', x: 170, y: 600, w: 100, h: 44, layer: 'shared' },
{ id: 'redactor', label: 'SensitiveRedactor', subtitle: 'src/shared/sensitive-redactor.ts', x: 370, y: 600, w: 165, h: 44, layer: 'shared' },
{ id: 'escape-html', label: 'escapeHtml', subtitle: 'src/shared/escape-html.ts', x: 590, y: 600, w: 120, h: 44, layer: 'shared' },
// Build
{ id: 'vite', label: 'Vite', subtitle: 'vite.config.ts', x: 950, y: 80, w: 90, h: 44, layer: 'build' },
{ id: 'vitest', label: 'Vitest', subtitle: 'vitest.config.ts', x: 950, y: 160, w: 90, h: 44, layer: 'build' },
{ id: 'tailwind', label: 'Tailwind', subtitle: 'tailwind.config.js', x: 950, y: 240, w: 100, h: 44, layer: 'build' },
{ id: 'cli', label: 'CLI Entry', subtitle: 'bin/session-viewer.js', x: 950, y: 420, w: 110, h: 44, layer: 'build' },
];
const CONNECTIONS = [
// App renders children
{ from: 'app', to: 'session-list', type: 'render' },
{ from: 'app', to: 'session-viewer', type: 'render' },
{ from: 'app', to: 'filter-panel', type: 'render' },
{ from: 'app', to: 'search-bar', type: 'render' },
{ from: 'app', to: 'export-btn', type: 'render' },
{ from: 'app', to: 'error-boundary', type: 'render' },
// SessionViewer renders messages
{ from: 'session-viewer', to: 'message-bubble', type: 'render' },
{ from: 'session-viewer', to: 'search-minimap', type: 'render' },
{ from: 'message-bubble', to: 'agent-progress', type: 'render', label: 'agent events' },
// Hooks used by App
{ from: 'app', to: 'use-session', type: 'import' },
{ from: 'app', to: 'use-filters', type: 'import' },
// Libs used by components
{ from: 'message-bubble', to: 'markdown-lib', type: 'import' },
{ from: 'agent-progress', to: 'agent-parser', type: 'import' },
// Client to Server data flow
{ from: 'use-session', to: 'sessions-route', type: 'data', label: 'GET /api/sessions' },
{ from: 'export-btn', to: 'export-route', type: 'data', label: 'POST /api/export' },
// Server internal
{ from: 'sessions-route', to: 'session-discovery', type: 'import' },
{ from: 'sessions-route', to: 'session-parser', type: 'import' },
{ from: 'sessions-route', to: 'progress-grouper', type: 'import' },
{ from: 'export-route', to: 'html-exporter', type: 'import' },
// Server to Shared
{ from: 'session-parser', to: 'shared-types', type: 'import' },
{ from: 'html-exporter', to: 'redactor', type: 'import' },
{ from: 'html-exporter', to: 'escape-html', type: 'import' },
{ from: 'html-exporter', to: 'shared-types', type: 'import' },
{ from: 'progress-grouper', to: 'shared-types', type: 'import' },
{ from: 'sessions-route', to: 'shared-types', type: 'import' },
// Client to Shared
{ from: 'use-filters', to: 'redactor', type: 'import' },
{ from: 'use-filters', to: 'shared-types', type: 'import' },
// Events
{ from: 'session-list', to: 'app', type: 'event', label: 'onSelect' },
{ from: 'filter-panel', to: 'app', type: 'event', label: 'onFilterChange' },
{ from: 'search-bar', to: 'app', type: 'event', label: 'onSearch' },
// Build connections
{ from: 'vite', to: 'app', type: 'import', label: 'bundles' },
{ from: 'tailwind', to: 'app', type: 'import', label: 'styles' },
{ from: 'cli', to: 'sessions-route', type: 'import', label: 'starts server' },
];
const PRESETS = {
'Full System': { layers: ['client','server','shared','build'], conns: ['data','import','render','event'] },
'Data Flow': { layers: ['client','server','shared'], conns: ['data','event'] },
'Client Only': { layers: ['client'], conns: ['render','import','event'] },
'Server Only': { layers: ['server','shared'], conns: ['import','data'] },
'Render Tree': { layers: ['client'], conns: ['render'] },
};
// ─── STATE ──────────────────────────────────────────────────
const state = {
preset: 'Full System',
layers: { client: true, server: true, shared: true, build: true },
conns: { data: true, import: true, render: true, event: true },
comments: [],
zoom: 1,
panX: 0, panY: 0,
modalNode: null,
};
// ─── RENDER CONTROLS ────────────────────────────────────────
function renderPresets() {
var el = document.getElementById('presets');
var btns = [];
var names = Object.keys(PRESETS);
for (var i = 0; i < names.length; i++) {
var name = names[i];
var cls = state.preset === name ? 'preset-btn active' : 'preset-btn';
btns.push('<button class="' + cls + '" data-preset="' + name + '">' + name + '</button>');
}
el.textContent = '';
el.insertAdjacentHTML('beforeend', btns.join(''));
el.querySelectorAll('.preset-btn').forEach(function(btn) {
btn.addEventListener('click', function() { applyPreset(this.getAttribute('data-preset')); });
});
}
function renderLayerToggles() {
var el = document.getElementById('layers');
el.textContent = '';
LAYERS.forEach(function(l) {
var label = document.createElement('label');
label.className = 'check-item';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = state.layers[l.id];
cb.addEventListener('change', function() { toggleLayer(l.id, this.checked); });
var dot = document.createElement('span');
dot.className = 'dot';
dot.style.background = l.color;
label.appendChild(cb);
label.appendChild(dot);
label.appendChild(document.createTextNode(' ' + l.label));
el.appendChild(label);
});
}
function renderConnToggles() {
var el = document.getElementById('connections');
el.textContent = '';
CONN_TYPES.forEach(function(c) {
var label = document.createElement('label');
label.className = 'conn-item';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.checked = state.conns[c.id];
cb.addEventListener('change', function() { toggleConn(c.id, this.checked); });
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '24');
svg.setAttribute('height', '12');
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', '0'); line.setAttribute('y1', '6');
line.setAttribute('x2', '24'); line.setAttribute('y2', '6');
line.setAttribute('stroke', c.color); line.setAttribute('stroke-width', '2');
if (c.dash) line.setAttribute('stroke-dasharray', c.dash);
svg.appendChild(line);
label.appendChild(cb);
label.appendChild(svg);
label.appendChild(document.createTextNode(' ' + c.label));
el.appendChild(label);
});
}
function renderComments() {
var el = document.getElementById('comments-list');
var countEl = document.getElementById('comment-count');
countEl.textContent = state.comments.length ? '(' + state.comments.length + ')' : '';
el.textContent = '';
if (!state.comments.length) {
var p = document.createElement('div');
p.className = 'no-comments';
p.textContent = 'Click a component to add feedback';
el.appendChild(p);
return;
}
state.comments.forEach(function(c, i) {
var card = document.createElement('div');
card.className = 'comment-card';
var del = document.createElement('button');
del.className = 'del';
del.textContent = '\u00d7';
del.addEventListener('click', function() { deleteComment(i); });
var target = document.createElement('div');
target.className = 'target';
target.textContent = c.targetLabel;
var file = document.createElement('div');
file.className = 'file';
file.textContent = c.targetFile;
var text = document.createElement('div');
text.className = 'text';
text.textContent = c.text;
card.appendChild(del);
card.appendChild(target);
card.appendChild(file);
card.appendChild(text);
el.appendChild(card);
});
}
// ─── ACTIONS ────────────────────────────────────────────────
function applyPreset(name) {
var p = PRESETS[name];
state.preset = name;
LAYERS.forEach(function(l) { state.layers[l.id] = p.layers.indexOf(l.id) !== -1; });
CONN_TYPES.forEach(function(c) { state.conns[c.id] = p.conns.indexOf(c.id) !== -1; });
updateAll();
}
function toggleLayer(id, on) {
state.layers[id] = on;
state.preset = '';
updateAll();
}
function toggleConn(id, on) {
state.conns[id] = on;
state.preset = '';
updateAll();
}
function deleteComment(i) {
state.comments.splice(i, 1);
updateAll();
}
// ─── ZOOM & PAN ─────────────────────────────────────────────
var isPanning = false, panStartX = 0, panStartY = 0;
function zoom(delta) {
state.zoom = Math.max(0.4, Math.min(2.5, state.zoom + delta));
renderDiagram();
}
function resetZoom() {
state.zoom = 1; state.panX = 0; state.panY = 0;
renderDiagram();
}
var wrap = document.getElementById('canvas-wrap');
wrap.addEventListener('wheel', function(e) {
e.preventDefault();
zoom(e.deltaY > 0 ? -0.08 : 0.08);
}, { passive: false });
wrap.addEventListener('mousedown', function(e) {
if (e.target.closest('.node-group')) return;
isPanning = true;
panStartX = e.clientX - state.panX;
panStartY = e.clientY - state.panY;
wrap.style.cursor = 'grabbing';
});
window.addEventListener('mousemove', function(e) {
if (!isPanning) return;
state.panX = e.clientX - panStartX;
state.panY = e.clientY - panStartY;
renderDiagram();
});
window.addEventListener('mouseup', function() { isPanning = false; wrap.style.cursor = ''; });
// ─── SVG RENDERING ──────────────────────────────────────────
function getLayerDef(layerId) { return LAYERS.find(function(l) { return l.id === layerId; }); }
function escText(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function renderDiagram() {
var svg = document.getElementById('diagram');
var rect = wrap.getBoundingClientRect();
var vw = rect.width, vh = rect.height;
var cx = 540, cy = 340;
var halfW = (vw / 2) / state.zoom;
var halfH = (vh / 2) / state.zoom;
var vbX = cx - halfW - state.panX / state.zoom;
var vbY = cy - halfH - state.panY / state.zoom;
svg.setAttribute('viewBox', vbX + ' ' + vbY + ' ' + (halfW * 2) + ' ' + (halfH * 2));
var visibleNodeIds = {};
NODES.forEach(function(n) {
if (n.w > 0 && state.layers[n.layer]) visibleNodeIds[n.id] = true;
});
var seenConns = {};
var visibleConns = [];
CONNECTIONS.forEach(function(c) {
if (!state.conns[c.type] || !visibleNodeIds[c.from] || !visibleNodeIds[c.to]) return;
var k = c.from + '-' + c.to + '-' + c.type;
if (seenConns[k]) return;
seenConns[k] = true;
visibleConns.push(c);
});
var commentedIds = {};
state.comments.forEach(function(c) { commentedIds[c.target] = (commentedIds[c.target] || 0) + 1; });
// Clear and rebuild SVG using DOM methods
while (svg.firstChild) svg.removeChild(svg.firstChild);
// Defs
var defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
CONN_TYPES.forEach(function(ct) {
var marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
marker.setAttribute('id', 'arrow-' + ct.id);
marker.setAttribute('markerWidth', '8');
marker.setAttribute('markerHeight', '6');
marker.setAttribute('refX', '7');
marker.setAttribute('refY', '3');
marker.setAttribute('orient', 'auto');
var poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
poly.setAttribute('points', '0 0, 8 3, 0 6');
poly.setAttribute('fill', ct.color);
poly.setAttribute('opacity', '0.7');
marker.appendChild(poly);
defs.appendChild(marker);
});
svg.appendChild(defs);
// Layer bands
var layerBands = [
{ id: 'client', y1: 25, y2: 345, label: 'CLIENT' },
{ id: 'server', y1: 395, y2: 560, label: 'SERVER' },
{ id: 'shared', y1: 575, y2: 660, label: 'SHARED' },
{ id: 'build', y1: 55, y2: 480, label: 'BUILD' },
];
layerBands.forEach(function(band) {
if (!state.layers[band.id]) return;
var ld = getLayerDef(band.id);
var bx = band.id === 'build' ? 920 : 60;
var bw = band.id === 'build' ? 170 : 840;
var r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
r.setAttribute('x', bx); r.setAttribute('y', band.y1);
r.setAttribute('width', bw); r.setAttribute('height', band.y2 - band.y1);
r.setAttribute('rx', '8'); r.setAttribute('fill', ld.color);
r.setAttribute('opacity', '0.04'); r.setAttribute('stroke', ld.color);
r.setAttribute('stroke-opacity', '0.1');
svg.appendChild(r);
var t = document.createElementNS('http://www.w3.org/2000/svg', 'text');
t.setAttribute('x', bx + 10); t.setAttribute('y', band.y1 + 16);
t.setAttribute('class', 'layer-label');
t.textContent = band.label;
svg.appendChild(t);
});
// Connections
var nodeMap = {};
NODES.forEach(function(n) { nodeMap[n.id] = n; });
visibleConns.forEach(function(c) {
var from = nodeMap[c.from];
var to = nodeMap[c.to];
if (!from || !to) return;
var ct = CONN_TYPES.find(function(t) { return t.id === c.type; });
var x1 = from.x + from.w / 2, y1 = from.y + from.h / 2;
var x2 = to.x + to.w / 2, y2 = to.y + to.h / 2;
var sx = x1, sy = y1, ex = x2, ey = y2;
var dx = x2 - x1, dy = y2 - y1;
if (Math.abs(dy) > Math.abs(dx)) {
sy = dy > 0 ? from.y + from.h : from.y;
ey = dy > 0 ? to.y : to.y + to.h;
} else {
sx = dx > 0 ? from.x + from.w : from.x;
ex = dx > 0 ? to.x : to.x + to.w;
}
var midY = (sy + ey) / 2;
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M' + sx + ',' + sy + ' C' + sx + ',' + midY + ' ' + ex + ',' + midY + ' ' + ex + ',' + ey);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', ct.color);
path.setAttribute('stroke-width', '1.5');
path.setAttribute('stroke-opacity', '0.5');
if (ct.dash) path.setAttribute('stroke-dasharray', ct.dash);
path.setAttribute('marker-end', 'url(#arrow-' + ct.id + ')');
svg.appendChild(path);
if (c.label) {
var lx = (sx + ex) / 2 + (Math.abs(dx) < 20 ? 8 : 0);
var ly = midY - 4;
var lt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
lt.setAttribute('x', lx); lt.setAttribute('y', ly);
lt.setAttribute('fill', ct.color);
lt.setAttribute('font-size', '9');
lt.setAttribute('text-anchor', 'middle');
lt.setAttribute('opacity', '0.6');
lt.textContent = c.label;
svg.appendChild(lt);
}
});
// Nodes
NODES.forEach(function(n) {
if (n.w === 0 || !state.layers[n.layer]) return;
var ld = getLayerDef(n.layer);
var hasComment = !!commentedIds[n.id];
var commentCount = commentedIds[n.id] || 0;
var g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.setAttribute('class', 'node-group' + (hasComment ? ' commented' : ''));
g.addEventListener('click', function() { openModal(n.id); });
var r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
r.setAttribute('x', n.x); r.setAttribute('y', n.y);
r.setAttribute('width', n.w); r.setAttribute('height', n.h);
r.setAttribute('rx', '6'); r.setAttribute('fill', ld.fill);
r.setAttribute('stroke', hasComment ? '#f59e0b' : ld.border);
r.setAttribute('stroke-width', hasComment ? '2.5' : '1');
r.setAttribute('stroke-opacity', hasComment ? '1' : '0.5');
g.appendChild(r);
var t1 = document.createElementNS('http://www.w3.org/2000/svg', 'text');
t1.setAttribute('x', n.x + n.w / 2); t1.setAttribute('y', n.y + 18);
t1.setAttribute('fill', ld.color); t1.setAttribute('font-size', '12');
t1.setAttribute('font-weight', '600'); t1.setAttribute('text-anchor', 'middle');
t1.textContent = n.label;
g.appendChild(t1);
var t2 = document.createElementNS('http://www.w3.org/2000/svg', 'text');
t2.setAttribute('x', n.x + n.w / 2); t2.setAttribute('y', n.y + 32);
t2.setAttribute('fill', ld.color); t2.setAttribute('font-size', '9');
t2.setAttribute('text-anchor', 'middle'); t2.setAttribute('opacity', '0.5');
t2.textContent = n.subtitle.replace('src/', '');
g.appendChild(t2);
if (hasComment) {
var circ = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circ.setAttribute('cx', n.x + n.w - 6); circ.setAttribute('cy', n.y + 6);
circ.setAttribute('r', '5'); circ.setAttribute('fill', '#f59e0b');
g.appendChild(circ);
var ct = document.createElementNS('http://www.w3.org/2000/svg', 'text');
ct.setAttribute('x', n.x + n.w - 6); ct.setAttribute('y', n.y + 9.5);
ct.setAttribute('fill', '#000'); ct.setAttribute('font-size', '8');
ct.setAttribute('font-weight', '700'); ct.setAttribute('text-anchor', 'middle');
ct.textContent = String(commentCount);
g.appendChild(ct);
}
svg.appendChild(g);
});
}
// ─── MODAL ──────────────────────────────────────────────────
function openModal(nodeId) {
var node = NODES.find(function(n) { return n.id === nodeId && n.w > 0; });
if (!node) return;
state.modalNode = node;
document.getElementById('modal-title').textContent = node.label;
document.getElementById('modal-file').textContent = node.subtitle;
document.getElementById('modal-input').value = '';
document.getElementById('modal').classList.add('open');
setTimeout(function() { document.getElementById('modal-input').focus(); }, 50);
}
function closeModal() {
document.getElementById('modal').classList.remove('open');
state.modalNode = null;
}
function saveComment() {
var text = document.getElementById('modal-input').value.trim();
if (!text || !state.modalNode) return;
state.comments.push({
id: Date.now(),
target: state.modalNode.id,
targetLabel: state.modalNode.label,
targetFile: state.modalNode.subtitle,
text: text
});
closeModal();
updateAll();
}
document.getElementById('modal').addEventListener('click', function(e) {
if (e.target === document.getElementById('modal')) closeModal();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeModal();
if (e.key === 'Enter' && e.metaKey) saveComment();
});
// ─── PROMPT ─────────────────────────────────────────────────
function updatePrompt() {
var el = document.getElementById('prompt-text');
var activeLayers = LAYERS.filter(function(l) { return state.layers[l.id]; }).map(function(l) { return l.label; });
if (!state.comments.length) {
var layerStr = activeLayers.length === LAYERS.length ? 'full system' : activeLayers.join(', ');
el.textContent = 'Viewing session-viewer architecture (' + layerStr + '). Click any component to add feedback.';
return;
}
var parts = [];
var layerNote = activeLayers.length === LAYERS.length
? '' : ', focusing on the ' + activeLayers.join(' and ') + ' layer' + (activeLayers.length > 1 ? 's' : '');
parts.push('This is the session-viewer architecture' + layerNote + '.\n');
parts.push('Architecture feedback:\n');
state.comments.forEach(function(c) {
parts.push(c.targetLabel + ' (' + c.targetFile + '):\n' + c.text + '\n');
});
el.textContent = parts.join('\n');
}
// ─── COPY ───────────────────────────────────────────────────
function copyPrompt() {
var el = document.getElementById('prompt-text');
var text = el.textContent;
navigator.clipboard.writeText(text).then(function() {
var btn = document.getElementById('copy-btn');
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(function() { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1500);
});
}
// ─── UPDATE ALL ─────────────────────────────────────────────
function updateAll() {
renderPresets();
renderLayerToggles();
renderConnToggles();
renderComments();
renderDiagram();
updatePrompt();
}
// ─── INIT ───────────────────────────────────────────────────
updateAll();
window.addEventListener('resize', function() { renderDiagram(); });
</script>
</body>
</html>