Skip to content

Instantly share code, notes, and snippets.

@nadavkav
Created February 22, 2026 18:09
Show Gist options
  • Select an option

  • Save nadavkav/d7aa45d1fcb0061f28b3aa968b0a6911 to your computer and use it in GitHub Desktop.

Select an option

Save nadavkav/d7aa45d1fcb0061f28b3aa968b0a6911 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moodle 4.5 AI Subsystem Architecture</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f172a;
color: #e2e8f0;
height: 100vh;
overflow: hidden;
}
.container {
display: grid;
grid-template-columns: 280px 1fr;
grid-template-rows: 1fr auto;
height: 100vh;
}
.sidebar {
background: #1e293b;
padding: 16px;
overflow-y: auto;
border-right: 1px solid #334155;
}
.sidebar h1 {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
color: #f8fafc;
}
.sidebar .subtitle {
font-size: 12px;
color: #94a3b8;
margin-bottom: 20px;
}
.section {
margin-bottom: 20px;
}
.section-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #64748b;
margin-bottom: 10px;
}
.presets {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.preset-btn {
background: #334155;
border: 1px solid #475569;
color: #e2e8f0;
padding: 6px 10px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.preset-btn:hover {
background: #475569;
}
.preset-btn.active {
background: #3b82f6;
border-color: #3b82f6;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.checkbox-item input {
accent-color: #3b82f6;
}
.color-indicator {
width: 12px;
height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.comments-section {
margin-top: 16px;
border-top: 1px solid #334155;
padding-top: 16px;
}
.comment-item {
background: #334155;
border-radius: 6px;
padding: 10px;
margin-bottom: 8px;
font-size: 12px;
}
.comment-item .target {
font-weight: 600;
color: #60a5fa;
margin-bottom: 4px;
}
.comment-item .text {
color: #cbd5e1;
line-height: 1.4;
}
.comment-item .delete-btn {
background: none;
border: none;
color: #ef4444;
cursor: pointer;
font-size: 11px;
margin-top: 6px;
}
.comment-item .delete-btn:hover {
text-decoration: underline;
}
.canvas-area {
background: #0f172a;
position: relative;
overflow: hidden;
}
svg {
width: 100%;
height: 100%;
}
.zoom-controls {
position: absolute;
top: 12px;
right: 12px;
display: flex;
gap: 4px;
}
.zoom-btn {
background: #1e293b;
border: 1px solid #334155;
color: #e2e8f0;
width: 32px;
height: 32px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
display: flex;
align-items: center;
justify-content: center;
}
.zoom-btn:hover {
background: #334155;
}
.legend {
position: absolute;
bottom: 12px;
left: 12px;
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 12px;
font-size: 11px;
}
.legend-title {
font-weight: 600;
margin-bottom: 8px;
color: #94a3b8;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.legend-line {
width: 24px;
height: 2px;
}
.prompt-area {
grid-column: 1 / -1;
background: #1e293b;
border-top: 1px solid #334155;
padding: 16px;
}
.prompt-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.prompt-header h3 {
font-size: 12px;
font-weight: 600;
color: #94a3b8;
}
.copy-btn {
background: #3b82f6;
border: none;
color: white;
padding: 6px 14px;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.copy-btn:hover {
background: #2563eb;
}
.copy-btn.copied {
background: #10b981;
}
.prompt-output {
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 12px;
line-height: 1.6;
color: #cbd5e1;
background: #0f172a;
padding: 12px;
border-radius: 6px;
max-height: 120px;
overflow-y: auto;
white-space: pre-wrap;
}
/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: #1e293b;
border: 1px solid #334155;
border-radius: 12px;
padding: 20px;
width: 400px;
max-width: 90vw;
}
.modal h3 {
font-size: 16px;
margin-bottom: 4px;
}
.modal .modal-subtitle {
font-size: 12px;
color: #64748b;
margin-bottom: 16px;
}
.modal textarea {
width: 100%;
height: 100px;
background: #0f172a;
border: 1px solid #334155;
border-radius: 6px;
color: #e2e8f0;
padding: 10px;
font-family: inherit;
font-size: 13px;
resize: vertical;
margin-bottom: 12px;
}
.modal textarea:focus {
outline: none;
border-color: #3b82f6;
}
.modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.modal-btn {
padding: 8px 16px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
border: none;
}
.modal-btn.cancel {
background: #334155;
color: #e2e8f0;
}
.modal-btn.save {
background: #3b82f6;
color: white;
}
/* Node styles */
.node {
cursor: pointer;
transition: transform 0.1s;
}
.node:hover rect {
stroke-width: 2;
}
.node.has-comment rect {
stroke: #f59e0b;
stroke-width: 2;
}
</style>
</head>
<body>
<div class="container">
<div class="sidebar">
<h1>Moodle 4.5 AI Subsystem</h1>
<p class="subtitle">Interactive Architecture Explorer</p>
<div class="section">
<div class="section-title">View Presets</div>
<div class="presets">
<button class="preset-btn active" data-preset="full">Full System</button>
<button class="preset-btn" data-preset="request-flow">Request Flow</button>
<button class="preset-btn" data-preset="providers">Providers</button>
<button class="preset-btn" data-preset="placements">Placements</button>
</div>
</div>
<div class="section">
<div class="section-title">Layers</div>
<div class="checkbox-group" id="layers">
<label class="checkbox-item">
<input type="checkbox" data-layer="placements" checked>
<span class="color-indicator" style="background: #dbeafe;"></span>
Placements (UI Integration)
</label>
<label class="checkbox-item">
<input type="checkbox" data-layer="actions" checked>
<span class="color-indicator" style="background: #dcfce7;"></span>
Actions (AI Capabilities)
</label>
<label class="checkbox-item">
<input type="checkbox" data-layer="core" checked>
<span class="color-indicator" style="background: #f3e8ff;"></span>
Core (AI Manager)
</label>
<label class="checkbox-item">
<input type="checkbox" data-layer="providers" checked>
<span class="color-indicator" style="background: #fef3c7;"></span>
Providers (LLM Backends)
</label>
<label class="checkbox-item">
<input type="checkbox" data-layer="external" checked>
<span class="color-indicator" style="background: #fce7f3;"></span>
External Services
</label>
</div>
</div>
<div class="section">
<div class="section-title">Connection Types</div>
<div class="checkbox-group" id="connections">
<label class="checkbox-item">
<input type="checkbox" data-conn="request" checked>
<span class="color-indicator" style="background: #3b82f6;"></span>
Request Flow
</label>
<label class="checkbox-item">
<input type="checkbox" data-conn="action" checked>
<span class="color-indicator" style="background: #10b981;"></span>
Action Invocation
</label>
<label class="checkbox-item">
<input type="checkbox" data-conn="provider" checked>
<span class="color-indicator" style="background: #f97316;"></span>
Provider Call
</label>
<label class="checkbox-item">
<input type="checkbox" data-conn="config" checked>
<span class="color-indicator" style="background: #6b7280;"></span>
Configuration
</label>
</div>
</div>
<div class="comments-section">
<div class="section-title">Comments (<span id="comment-count">0</span>)</div>
<div id="comments-list"></div>
<p style="font-size: 11px; color: #64748b; margin-top: 8px;">Click any component to add feedback</p>
</div>
</div>
<div class="canvas-area">
<svg id="diagram" viewBox="0 0 900 600">
<defs>
<marker id="arrow-blue" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#3b82f6"/>
</marker>
<marker id="arrow-green" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#10b981"/>
</marker>
<marker id="arrow-orange" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#f97316"/>
</marker>
<marker id="arrow-gray" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#6b7280"/>
</marker>
</defs>
<g id="connections-group"></g>
<g id="nodes-group"></g>
</svg>
<div class="zoom-controls">
<button class="zoom-btn" id="zoom-in">+</button>
<button class="zoom-btn" id="zoom-out">−</button>
<button class="zoom-btn" id="zoom-reset">⟲</button>
</div>
<div class="legend">
<div class="legend-title">Connection Types</div>
<div class="legend-item">
<div class="legend-line" style="background: #3b82f6;"></div>
<span>Request Flow</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #10b981;"></div>
<span>Action Invocation</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #f97316;"></div>
<span>Provider Call</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background: #6b7280; border-style: dotted;"></div>
<span>Configuration</span>
</div>
</div>
</div>
<div class="prompt-area">
<div class="prompt-header">
<h3>Generated Prompt</h3>
<button class="copy-btn" id="copy-btn">Copy Prompt</button>
</div>
<div class="prompt-output" id="prompt-output"></div>
</div>
</div>
<div class="modal-overlay" id="modal">
<div class="modal">
<h3 id="modal-title">Component Name</h3>
<p class="modal-subtitle" id="modal-subtitle">path/to/file.php</p>
<textarea id="modal-input" placeholder="Add your feedback or questions about this component..."></textarea>
<div class="modal-actions">
<button class="modal-btn cancel" id="modal-cancel">Cancel</button>
<button class="modal-btn save" id="modal-save">Add Comment</button>
</div>
</div>
</div>
<script>
// Moodle 4.5 AI Subsystem Architecture Data
const nodes = [
// Placements Layer (UI Integration Points)
{ id: 'courseassist', label: 'Course Assist', subtitle: 'aiplacement_courseassist', x: 80, y: 40, w: 130, h: 50, layer: 'placements', color: '#dbeafe' },
{ id: 'editor', label: 'Editor Placement', subtitle: 'aiplacement_editor', x: 240, y: 40, w: 130, h: 50, layer: 'placements', color: '#dbeafe' },
{ id: 'htmleditor', label: 'HTML Editor', subtitle: 'editor_tiny/ai', x: 400, y: 40, w: 130, h: 50, layer: 'placements', color: '#dbeafe' },
{ id: 'customplace', label: 'Custom Placement', subtitle: 'aiplacement_*', x: 560, y: 40, w: 130, h: 50, layer: 'placements', color: '#dbeafe' },
// Actions Layer (AI Capabilities)
{ id: 'generate-text', label: 'Generate Text', subtitle: 'aiaction_generatetext', x: 120, y: 150, w: 140, h: 50, layer: 'actions', color: '#dcfce7' },
{ id: 'generate-image', label: 'Generate Image', subtitle: 'aiaction_generateimage', x: 300, y: 150, w: 140, h: 50, layer: 'actions', color: '#dcfce7' },
{ id: 'summarize', label: 'Summarize', subtitle: 'aiaction_summarize', x: 480, y: 150, w: 140, h: 50, layer: 'actions', color: '#dcfce7' },
{ id: 'translate', label: 'Translate', subtitle: 'aiaction_translate', x: 660, y: 150, w: 130, h: 50, layer: 'actions', color: '#dcfce7' },
// Core Layer (AI Manager & Infrastructure)
{ id: 'ai-manager', label: 'AI Manager', subtitle: 'core_ai/manager.php', x: 320, y: 280, w: 160, h: 55, layer: 'core', color: '#f3e8ff' },
{ id: 'ai-api', label: 'AI API', subtitle: 'core_ai/api.php', x: 140, y: 280, w: 130, h: 55, layer: 'core', color: '#f3e8ff' },
{ id: 'ai-policy', label: 'AI Policy', subtitle: 'core_ai/policy.php', x: 530, y: 280, w: 130, h: 55, layer: 'core', color: '#f3e8ff' },
{ id: 'ai-admin', label: 'AI Admin Settings', subtitle: 'admin/settings/ai.php', x: 700, y: 280, w: 140, h: 55, layer: 'core', color: '#f3e8ff' },
// Providers Layer (LLM Backends)
{ id: 'openai', label: 'OpenAI Provider', subtitle: 'aiprovider_openai', x: 100, y: 410, w: 140, h: 50, layer: 'providers', color: '#fef3c7' },
{ id: 'azureai', label: 'Azure AI Provider', subtitle: 'aiprovider_azureai', x: 280, y: 410, w: 150, h: 50, layer: 'providers', color: '#fef3c7' },
{ id: 'ollama', label: 'Ollama Provider', subtitle: 'aiprovider_ollama', x: 470, y: 410, w: 140, h: 50, layer: 'providers', color: '#fef3c7' },
{ id: 'customprov', label: 'Custom Provider', subtitle: 'aiprovider_*', x: 650, y: 410, w: 140, h: 50, layer: 'providers', color: '#fef3c7' },
// External Layer (External Services)
{ id: 'openai-api', label: 'OpenAI API', subtitle: 'api.openai.com', x: 100, y: 520, w: 140, h: 45, layer: 'external', color: '#fce7f3' },
{ id: 'azure-api', label: 'Azure OpenAI', subtitle: '*.openai.azure.com', x: 280, y: 520, w: 150, h: 45, layer: 'external', color: '#fce7f3' },
{ id: 'ollama-server', label: 'Ollama Server', subtitle: 'localhost:11434', x: 470, y: 520, w: 140, h: 45, layer: 'external', color: '#fce7f3' },
{ id: 'custom-llm', label: 'Custom LLM', subtitle: 'custom endpoint', x: 650, y: 520, w: 140, h: 45, layer: 'external', color: '#fce7f3' },
];
const connections = [
// Placements → Actions
{ from: 'courseassist', to: 'generate-text', type: 'action', label: 'invoke' },
{ from: 'courseassist', to: 'summarize', type: 'action' },
{ from: 'editor', to: 'generate-text', type: 'action' },
{ from: 'editor', to: 'generate-image', type: 'action' },
{ from: 'htmleditor', to: 'generate-text', type: 'action' },
{ from: 'customplace', to: 'generate-text', type: 'action' },
{ from: 'customplace', to: 'translate', type: 'action' },
// Placements → API (request flow)
{ from: 'courseassist', to: 'ai-api', type: 'request', label: 'request' },
{ from: 'editor', to: 'ai-api', type: 'request' },
{ from: 'htmleditor', to: 'ai-api', type: 'request' },
// API → Manager
{ from: 'ai-api', to: 'ai-manager', type: 'request', label: 'route' },
// Manager → Actions (orchestration)
{ from: 'ai-manager', to: 'generate-text', type: 'action', label: 'execute' },
{ from: 'ai-manager', to: 'generate-image', type: 'action' },
{ from: 'ai-manager', to: 'summarize', type: 'action' },
{ from: 'ai-manager', to: 'translate', type: 'action' },
// Manager → Policy
{ from: 'ai-manager', to: 'ai-policy', type: 'config', label: 'check' },
// Admin → Manager (config)
{ from: 'ai-admin', to: 'ai-manager', type: 'config', label: 'configure' },
{ from: 'ai-admin', to: 'ai-policy', type: 'config' },
// Manager → Providers
{ from: 'ai-manager', to: 'openai', type: 'provider', label: 'dispatch' },
{ from: 'ai-manager', to: 'azureai', type: 'provider' },
{ from: 'ai-manager', to: 'ollama', type: 'provider' },
{ from: 'ai-manager', to: 'customprov', type: 'provider' },
// Providers → External
{ from: 'openai', to: 'openai-api', type: 'provider', label: 'HTTP' },
{ from: 'azureai', to: 'azure-api', type: 'provider' },
{ from: 'ollama', to: 'ollama-server', type: 'provider' },
{ from: 'customprov', to: 'custom-llm', type: 'provider' },
];
// State
const state = {
layers: {
placements: true,
actions: true,
core: true,
providers: true,
external: true
},
connections: {
request: true,
action: true,
provider: true,
config: true
},
zoom: 1,
pan: { x: 0, y: 0 },
comments: [],
activePreset: 'full'
};
const presets = {
full: {
layers: { placements: true, actions: true, core: true, providers: true, external: true },
connections: { request: true, action: true, provider: true, config: true }
},
'request-flow': {
layers: { placements: true, actions: false, core: true, providers: true, external: true },
connections: { request: true, action: false, provider: true, config: false }
},
providers: {
layers: { placements: false, actions: false, core: true, providers: true, external: true },
connections: { request: false, action: false, provider: true, config: true }
},
placements: {
layers: { placements: true, actions: true, core: true, providers: false, external: false },
connections: { request: true, action: true, provider: false, config: false }
}
};
// DOM Elements
const svg = document.getElementById('diagram');
const nodesGroup = document.getElementById('nodes-group');
const connectionsGroup = document.getElementById('connections-group');
const promptOutput = document.getElementById('prompt-output');
const commentsList = document.getElementById('comments-list');
const commentCount = document.getElementById('comment-count');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modal-title');
const modalSubtitle = document.getElementById('modal-subtitle');
const modalInput = document.getElementById('modal-input');
const copyBtn = document.getElementById('copy-btn');
let selectedNode = null;
// Rendering
function getNodeById(id) {
return nodes.find(n => n.id === id);
}
function drawConnection(conn) {
if (!state.connections[conn.type]) return null;
const from = getNodeById(conn.from);
const to = getNodeById(conn.to);
if (!from || !to) return null;
if (!state.layers[from.layer] || !state.layers[to.layer]) return null;
const x1 = from.x + from.w / 2;
const y1 = from.y + from.h;
const x2 = to.x + to.w / 2;
const y2 = to.y;
const colors = {
request: '#3b82f6',
action: '#10b981',
provider: '#f97316',
config: '#6b7280'
};
const dashes = {
request: '',
action: '6,3',
provider: '8,4',
config: '3,3'
};
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const midY = (y1 + y2) / 2;
const d = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
path.setAttribute('d', d);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', colors[conn.type]);
path.setAttribute('stroke-width', '2');
path.setAttribute('stroke-dasharray', dashes[conn.type]);
path.setAttribute('marker-end', `url(#arrow-${conn.type === 'request' ? 'blue' : conn.type === 'action' ? 'green' : conn.type === 'provider' ? 'orange' : 'gray'})`);
path.setAttribute('opacity', '0.7');
return path;
}
function drawNode(node) {
if (!state.layers[node.layer]) return null;
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
g.classList.add('node');
g.setAttribute('data-id', node.id);
const hasComment = state.comments.some(c => c.target === node.id);
if (hasComment) g.classList.add('has-comment');
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
rect.setAttribute('x', node.x);
rect.setAttribute('y', node.y);
rect.setAttribute('width', node.w);
rect.setAttribute('height', node.h);
rect.setAttribute('rx', '8');
rect.setAttribute('fill', node.color);
rect.setAttribute('stroke', hasComment ? '#f59e0b' : '#475569');
rect.setAttribute('stroke-width', hasComment ? '2' : '1');
const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
title.setAttribute('x', node.x + node.w / 2);
title.setAttribute('y', node.y + 20);
title.setAttribute('text-anchor', 'middle');
title.setAttribute('font-size', '12');
title.setAttribute('font-weight', '600');
title.setAttribute('fill', '#1e293b');
title.textContent = node.label;
const subtitle = document.createElementNS('http://www.w3.org/2000/svg', 'text');
subtitle.setAttribute('x', node.x + node.w / 2);
subtitle.setAttribute('y', node.y + 36);
subtitle.setAttribute('text-anchor', 'middle');
subtitle.setAttribute('font-size', '9');
subtitle.setAttribute('fill', '#64748b');
subtitle.textContent = node.subtitle;
g.appendChild(rect);
g.appendChild(title);
g.appendChild(subtitle);
g.addEventListener('click', () => openModal(node));
return g;
}
function render() {
connectionsGroup.innerHTML = '';
nodesGroup.innerHTML = '';
connections.forEach(conn => {
const path = drawConnection(conn);
if (path) connectionsGroup.appendChild(path);
});
nodes.forEach(node => {
const g = drawNode(node);
if (g) nodesGroup.appendChild(g);
});
const transform = `translate(${state.pan.x}, ${state.pan.y}) scale(${state.zoom})`;
connectionsGroup.setAttribute('transform', transform);
nodesGroup.setAttribute('transform', transform);
}
function updatePrompt() {
const visibleLayers = Object.entries(state.layers)
.filter(([_, v]) => v)
.map(([k]) => k);
const layerNames = {
placements: 'Placements',
actions: 'Actions',
core: 'Core/Manager',
providers: 'Providers',
external: 'External Services'
};
let prompt = `This is the Moodle 4.5 AI Subsystem architecture`;
if (visibleLayers.length < 5) {
prompt += `, focusing on ${visibleLayers.map(l => layerNames[l]).join(', ')}`;
}
prompt += '.\n\n';
prompt += `The AI subsystem follows a layered architecture:\n`;
prompt += `• Placements: UI integration points (editor, course assist)\n`;
prompt += `• Actions: AI capabilities (generate text/image, summarize, translate)\n`;
prompt += `• Core: AI Manager orchestrates requests and enforces policies\n`;
prompt += `• Providers: Plugin-based LLM backends (OpenAI, Azure, Ollama)\n\n`;
if (state.comments.length > 0) {
prompt += `Feedback on specific components:\n\n`;
state.comments.forEach(comment => {
prompt += `**${comment.targetLabel}** (${comment.targetFile}):\n`;
prompt += `${comment.text}\n\n`;
});
} else {
prompt += `Click components in the diagram to add feedback or questions.`;
}
promptOutput.textContent = prompt;
}
function renderComments() {
commentsList.innerHTML = '';
commentCount.textContent = state.comments.length;
state.comments.forEach(comment => {
const div = document.createElement('div');
div.className = 'comment-item';
div.innerHTML = `
<div class="target">${comment.targetLabel}</div>
<div class="text">${comment.text}</div>
<button class="delete-btn" data-id="${comment.id}">Delete</button>
`;
commentsList.appendChild(div);
});
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.id);
state.comments = state.comments.filter(c => c.id !== id);
render();
renderComments();
updatePrompt();
});
});
}
function openModal(node) {
selectedNode = node;
modalTitle.textContent = node.label;
modalSubtitle.textContent = node.subtitle;
modalInput.value = '';
modal.classList.add('active');
modalInput.focus();
}
function closeModal() {
modal.classList.remove('active');
selectedNode = null;
}
function saveComment() {
if (!selectedNode || !modalInput.value.trim()) return;
state.comments.push({
id: Date.now(),
target: selectedNode.id,
targetLabel: selectedNode.label,
targetFile: selectedNode.subtitle,
text: modalInput.value.trim()
});
closeModal();
render();
renderComments();
updatePrompt();
}
// Event Listeners
document.querySelectorAll('.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const preset = btn.dataset.preset;
state.activePreset = preset;
Object.assign(state.layers, presets[preset].layers);
Object.assign(state.connections, presets[preset].connections);
document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('[data-layer]').forEach(cb => {
cb.checked = state.layers[cb.dataset.layer];
});
document.querySelectorAll('[data-conn]').forEach(cb => {
cb.checked = state.connections[cb.dataset.conn];
});
render();
updatePrompt();
});
});
document.querySelectorAll('[data-layer]').forEach(cb => {
cb.addEventListener('change', () => {
state.layers[cb.dataset.layer] = cb.checked;
render();
updatePrompt();
});
});
document.querySelectorAll('[data-conn]').forEach(cb => {
cb.addEventListener('change', () => {
state.connections[cb.dataset.conn] = cb.checked;
render();
updatePrompt();
});
});
document.getElementById('zoom-in').addEventListener('click', () => {
state.zoom = Math.min(state.zoom * 1.2, 2);
render();
});
document.getElementById('zoom-out').addEventListener('click', () => {
state.zoom = Math.max(state.zoom / 1.2, 0.5);
render();
});
document.getElementById('zoom-reset').addEventListener('click', () => {
state.zoom = 1;
state.pan = { x: 0, y: 0 };
render();
});
document.getElementById('modal-cancel').addEventListener('click', closeModal);
document.getElementById('modal-save').addEventListener('click', saveComment);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(promptOutput.textContent).then(() => {
copyBtn.textContent = 'Copied!';
copyBtn.classList.add('copied');
setTimeout(() => {
copyBtn.textContent = 'Copy Prompt';
copyBtn.classList.remove('copied');
}, 2000);
});
});
// Initialize
render();
updatePrompt();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment