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>
702 lines
33 KiB
HTML
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)">−</button>
|
|
<button class="zoom-btn" onclick="resetZoom()">⌂</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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
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>
|