Created
February 22, 2026 18:09
-
-
Save nadavkav/d7aa45d1fcb0061f28b3aa968b0a6911 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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