evaluation-guidebook
/
app
/src
/content
/embeds
/smol-playbook
/model-architecture-decision-flowchart.html
| <!-- | |
| Model Architecture Decision Flowchart | |
| Usage: | |
| <HtmlEmbed src="/embeds/model-architecture-decision-flowchart.html" /> | |
| --> | |
| <div class="model-architecture-decision-flowchart"></div> | |
| <style> | |
| .model-architecture-decision-flowchart { | |
| width: 100%; | |
| min-height: 300px; | |
| position: relative; | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; | |
| } | |
| .model-architecture-decision-flowchart svg { | |
| display: block; | |
| width: 100%; | |
| } | |
| .model-architecture-decision-flowchart .node-rect { | |
| stroke-width: 2.5px; | |
| rx: 14px; | |
| ry: 14px; | |
| } | |
| .model-architecture-decision-flowchart .node-text { | |
| font-size: 18px; | |
| font-weight: 600; | |
| text-anchor: middle; | |
| pointer-events: none; | |
| fill: var(--text-color, #333); | |
| } | |
| .model-architecture-decision-flowchart .node-question { | |
| fill: oklch(from var(--primary-color) calc(l + 0.4) c h / 0.26); | |
| stroke: oklch(from var(--primary-color) calc(l + 0.15) c h / 0.5) ; | |
| } | |
| .model-architecture-decision-flowchart .node-success { | |
| fill: oklch(from var(--success-color) calc(l + 0.4) c h / 0.26); | |
| stroke: oklch(from var(--success-color) calc(l + 0.15) c h / 0.5) ; | |
| } | |
| .model-architecture-decision-flowchart .node-category { | |
| fill: oklch(from var(--danger-color) calc(l + 0.4) c h / 0.26); | |
| stroke: oklch(from var(--danger-color) calc(l + 0.15) c h / 0.5) ; | |
| } | |
| .model-architecture-decision-flowchart .node-decision { | |
| stroke: var(--border-color, #ddd) ; | |
| } | |
| /* Dark mode adjustments */ | |
| [data-theme="dark"] .model-architecture-decision-flowchart .node-question { | |
| fill: oklch(from var(--primary-color) calc(l + 0.3) c h / 0.2); | |
| stroke: oklch(from var(--primary-color) calc(l + 0.1) c h / 0.6) ; | |
| } | |
| [data-theme="dark"] .model-architecture-decision-flowchart .node-success { | |
| fill: oklch(from var(--success-color) calc(l + 0.3) c h / 0.2); | |
| stroke: oklch(from var(--success-color) calc(l + 0.1) c h / 0.6) ; | |
| } | |
| [data-theme="dark"] .model-architecture-decision-flowchart .node-category { | |
| fill: oklch(from var(--danger-color) calc(l + 0.3) c h / 0.2); | |
| stroke: oklch(from var(--danger-color) calc(l + 0.1) c h / 0.6) ; | |
| } | |
| .model-architecture-decision-flowchart .link-path { | |
| fill: none; | |
| stroke: var(--muted-color, #666); | |
| stroke-width: 2.5px; | |
| marker-end: url(#arrowhead); | |
| } | |
| .model-architecture-decision-flowchart .link-label { | |
| font-size: 14px; | |
| font-weight: 700; | |
| fill: var(--text-color, #333); | |
| text-anchor: middle; | |
| pointer-events: none; | |
| } | |
| .model-architecture-decision-flowchart .link-label-bg { | |
| fill: var(--page-bg, #ffffff); | |
| stroke: none; | |
| } | |
| </style> | |
| <script> | |
| (() => { | |
| const ensureD3 = (cb) => { | |
| if (window.d3 && typeof window.d3.select === 'function') return cb(); | |
| let s = document.getElementById('d3-cdn-script'); | |
| if (!s) { | |
| s = document.createElement('script'); | |
| s.id = 'd3-cdn-script'; | |
| s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; | |
| document.head.appendChild(s); | |
| } | |
| const onReady = () => { | |
| if (window.d3 && typeof window.d3.select === 'function') cb(); | |
| }; | |
| s.addEventListener('load', onReady, { once: true }); | |
| if (window.d3) onReady(); | |
| }; | |
| const bootstrap = () => { | |
| const scriptEl = document.currentScript; | |
| let container = scriptEl ? scriptEl.previousElementSibling : null; | |
| if (!(container && container.classList && container.classList.contains('model-architecture-decision-flowchart'))) { | |
| const candidates = Array.from(document.querySelectorAll('.model-architecture-decision-flowchart')) | |
| .filter((el) => !(el.dataset && el.dataset.mounted === 'true')); | |
| container = candidates[candidates.length - 1] || null; | |
| } | |
| if (!container) return; | |
| if (container.dataset) { | |
| if (container.dataset.mounted === 'true') return; | |
| container.dataset.mounted = 'true'; | |
| } | |
| // Define color scheme | |
| const getColors = () => { | |
| const getCSSVar = (varName, fallback) => { | |
| if (typeof getComputedStyle !== 'undefined') { | |
| const value = getComputedStyle(document.documentElement) | |
| .getPropertyValue(varName); | |
| if (value && value.trim()) { | |
| return value.trim(); | |
| } | |
| } | |
| return fallback; | |
| }; | |
| return { | |
| question: getCSSVar('--primary-color', '#0084ff'), | |
| decision: getCSSVar('--surface-bg', '#f9f9f9'), | |
| success: getCSSVar('--success-color', '#42d9b3'), | |
| category: getCSSVar('--danger-color', '#e85c42'), | |
| link: getCSSVar('--muted-color', '#666') | |
| }; | |
| }; | |
| // Define the flowchart structure - Model Architecture Decision | |
| const nodes = [ | |
| { id: 'B', label: 'Edge/Phones\nMemory-constrained environments', type: 'decision', x: 180, y: 100 }, | |
| { id: 'C', label: 'Other\nMore memory available', type: 'decision', x: 620, y: 100 }, | |
| { id: 'D', label: 'Dense (most cases)\nHybrid or other (for experienced teams)', type: 'success', x: 180, y: 320 }, | |
| { id: 'E', label: 'What\'s your team\'s expertise?', type: 'question', x: 620, y: 320 }, | |
| { id: 'F', label: 'First LLM training', type: 'decision', x: 380, y: 540 }, | |
| { id: 'G', label: 'Experienced\nComfortable with dense', type: 'decision', x: 620, y: 540 }, | |
| { id: 'H', label: 'Very experienced', type: 'decision', x: 860, y: 540 }, | |
| { id: 'I', label: 'Dense\n(Focus on basics)', type: 'success', x: 380, y: 760 }, | |
| { id: 'J', label: 'What\'s your timeline?', type: 'question', x: 620, y: 760 }, | |
| { id: 'K', label: 'Tight\nProven path required', type: 'decision', x: 480, y: 980 }, | |
| { id: 'L', label: 'Flexible\nOpen to exploration', type: 'decision', x: 760, y: 980 }, | |
| { id: 'M', label: 'Dense', type: 'success', x: 480, y: 1200 }, | |
| { id: 'N', label: 'MoE or MoE + Hybrid:\nbetter perf/compute', type: 'category', x: 760, y: 1200 }, | |
| { id: 'O', label: 'MoE or MoE + Hybrid:\nbetter perf/compute', type: 'category', x: 860, y: 760 } | |
| ]; | |
| const links = [ | |
| { source: 'B', target: 'D', label: '' }, | |
| { source: 'C', target: 'E', label: '' }, | |
| { source: 'E', target: 'F', label: '' }, | |
| { source: 'E', target: 'G', label: '' }, | |
| { source: 'E', target: 'H', label: '' }, | |
| { source: 'F', target: 'I', label: '' }, | |
| { source: 'G', target: 'J', label: '' }, | |
| { source: 'J', target: 'K', label: '' }, | |
| { source: 'J', target: 'L', label: '' }, | |
| { source: 'K', target: 'M', label: '' }, | |
| { source: 'L', target: 'N', label: '' }, | |
| { source: 'H', target: 'O', label: '' } | |
| ]; | |
| // Create SVG | |
| const svg = d3.select(container).append('svg').attr('width', '100%').style('display', 'block'); | |
| const gRoot = svg.append('g'); | |
| // Define arrowhead marker (solid triangle arrowhead) | |
| const defs = svg.append('defs'); | |
| const marker = defs.append('marker') | |
| .attr('id', 'arrowhead') | |
| .attr('viewBox', '0 0 10 10') | |
| .attr('refX', 2.5) | |
| .attr('refY', 5) | |
| .attr('markerWidth', 4) | |
| .attr('markerHeight', 4) | |
| .attr('orient', 'auto'); | |
| // Create solid arrowhead pointing right (smaller) | |
| marker.append('path') | |
| .attr('d', 'M 0 0 L 8 5 L 0 10 Z') | |
| .attr('fill', () => getColors().link); | |
| let width = 1000, height = 800; | |
| function render() { | |
| width = container.clientWidth || 1000; | |
| height = Math.max(800, Math.round(width * 1.3)); | |
| svg.attr('width', width).attr('height', height); | |
| const colors = getColors(); | |
| // Calculate scale to fit content (no padding, allow to touch edges) | |
| const nodeExtent = { | |
| minX: d3.min(nodes, d => d.x) - 160, | |
| maxX: d3.max(nodes, d => d.x) + 160, | |
| minY: d3.min(nodes, d => d.y) - 40, | |
| maxY: d3.max(nodes, d => d.y) + 80 | |
| }; | |
| const contentWidth = nodeExtent.maxX - nodeExtent.minX; | |
| const contentHeight = nodeExtent.maxY - nodeExtent.minY; | |
| const scale = Math.min(width / contentWidth, height / contentHeight); | |
| const offsetX = (width - contentWidth * scale) / 2 - nodeExtent.minX * scale; | |
| const offsetY = (height - contentHeight * scale) / 2 - nodeExtent.minY * scale; | |
| gRoot.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`); | |
| // Create a temporary text element for measuring text width | |
| const tempText = gRoot.append('text') | |
| .style('visibility', 'hidden') | |
| .style('font-size', '18px') | |
| .style('font-weight', '500'); | |
| // Word wrap function - intelligently breaks text into lines | |
| const wordWrap = (text, maxWidth, fontSize = '18px') => { | |
| const explicitLines = text.split('\n'); | |
| const wrappedLines = []; | |
| explicitLines.forEach(line => { | |
| if (!line.trim()) { | |
| wrappedLines.push(line); | |
| return; | |
| } | |
| tempText.attr('font-size', fontSize).text(line); | |
| const textWidth = tempText.node().getComputedTextLength(); | |
| // If line fits, keep it as is | |
| if (textWidth <= maxWidth) { | |
| wrappedLines.push(line); | |
| return; | |
| } | |
| // Otherwise, break into words and wrap | |
| const words = line.split(/\s+/); | |
| let currentLine = ''; | |
| words.forEach(word => { | |
| const testLine = currentLine ? `${currentLine} ${word}` : word; | |
| tempText.text(testLine); | |
| const testWidth = tempText.node().getComputedTextLength(); | |
| if (testWidth <= maxWidth && currentLine) { | |
| currentLine = testLine; | |
| } else { | |
| if (currentLine) { | |
| wrappedLines.push(currentLine); | |
| } | |
| currentLine = word; | |
| } | |
| }); | |
| if (currentLine) { | |
| wrappedLines.push(currentLine); | |
| } | |
| }); | |
| return wrappedLines.filter(line => line.trim().length > 0); | |
| }; | |
| // Calculate node dimensions with word wrapping | |
| const getNodeDimensions = (node) => { | |
| const maxWidths = { | |
| question: 240, | |
| decision: 250, | |
| success: 240, | |
| category: 260 | |
| }; | |
| const maxWidth = maxWidths[node.type] || 180; | |
| const fontSize = node.type === 'category' ? '19px' : '18px'; | |
| const wrappedLines = wordWrap(node.label, maxWidth, fontSize); | |
| node.wrappedLines = wrappedLines; | |
| tempText.attr('font-size', fontSize); | |
| const lineWidths = wrappedLines.map(line => { | |
| tempText.text(line); | |
| return tempText.node().getComputedTextLength(); | |
| }); | |
| const maxLineWidth = Math.max(...lineWidths, 0); | |
| const padding = 36; | |
| const lineHeight = node.type === 'category' ? 28 : 26; | |
| const width = Math.max(120, maxLineWidth + padding); | |
| const height = Math.max(30, wrappedLines.length * lineHeight + padding); | |
| return { width, height, wrappedLines }; | |
| }; | |
| // Pre-calculate all node dimensions with wrapping | |
| nodes.forEach(node => { | |
| const dims = getNodeDimensions(node); | |
| node.width = dims.width; | |
| node.height = dims.height; | |
| }); | |
| // Draw links first (so labels can be on top) | |
| const linkGroup = gRoot.selectAll('.link-group').data(links); | |
| const linkEnter = linkGroup.enter().append('g').attr('class', 'link-group'); | |
| linkEnter.append('path').attr('class', 'link-path'); | |
| linkEnter.append('rect').attr('class', 'link-label-bg').style('opacity', 0); | |
| linkEnter.append('text').attr('class', 'link-label').attr('dy', -5); | |
| const linkMerge = linkEnter.merge(linkGroup); | |
| linkMerge.select('.link-path') | |
| .attr('d', d => { | |
| const sourceNode = nodes.find(n => n.id === d.source); | |
| const targetNode = nodes.find(n => n.id === d.target); | |
| const gap = 12; | |
| if (Math.abs(sourceNode.x - targetNode.x) < 50) { | |
| const sx = sourceNode.x; | |
| const sy = sourceNode.y + sourceNode.height / 2 + gap; | |
| const tx = targetNode.x; | |
| const ty = targetNode.y - targetNode.height / 2 - gap; | |
| return `M ${sx} ${sy} L ${tx} ${ty}`; | |
| } | |
| let sx, sy, tx, ty; | |
| if (Math.abs(sourceNode.y - targetNode.y) < 50) { | |
| const sourceIsLeft = sourceNode.x < targetNode.x; | |
| sx = sourceNode.x + (sourceIsLeft ? sourceNode.width / 2 + gap : -(sourceNode.width / 2 + gap)); | |
| sy = sourceNode.y; | |
| tx = targetNode.x + (sourceIsLeft ? -(targetNode.width / 2 + gap) : targetNode.width / 2 + gap); | |
| ty = targetNode.y; | |
| } else { | |
| sx = sourceNode.x; | |
| sy = sourceNode.y + (sourceNode.y < targetNode.y ? sourceNode.height / 2 + gap : -(sourceNode.height / 2 + gap)); | |
| tx = targetNode.x; | |
| ty = targetNode.y + (targetNode.y > sourceNode.y ? -(targetNode.height / 2 + gap) : targetNode.height / 2 + gap); | |
| } | |
| const midX = (sx + tx) / 2; | |
| const midY = (sy + ty) / 2; | |
| return `M ${sx} ${sy} C ${sx} ${midY}, ${tx} ${midY}, ${tx} ${ty}`; | |
| }) | |
| .attr('stroke', colors.link); | |
| // Draw label backgrounds and text (only for non-empty labels) | |
| linkMerge.filter(d => d.label && d.label.trim()) | |
| .each(function (d) { | |
| const sourceNode = nodes.find(n => n.id === d.source); | |
| const targetNode = nodes.find(n => n.id === d.target); | |
| const x = (sourceNode.x + targetNode.x) / 2; | |
| const y = (sourceNode.y + targetNode.y) / 2; | |
| const labelEl = d3.select(this); | |
| const textEl = labelEl.select('.link-label'); | |
| tempText.style('font-size', '14px').style('font-weight', '700').text(d.label); | |
| const textWidth = tempText.node().getComputedTextLength(); | |
| const textHeight = 20; | |
| const padding = 10; | |
| labelEl.select('.link-label-bg') | |
| .attr('x', x - textWidth / 2 - padding) | |
| .attr('y', y - textHeight / 2 - padding / 2) | |
| .attr('width', textWidth + padding * 2) | |
| .attr('height', textHeight + padding) | |
| .style('opacity', 1); | |
| textEl | |
| .attr('x', x) | |
| .attr('y', y) | |
| .text(d.label); | |
| }); | |
| linkMerge.filter(d => !d.label || !d.label.trim()) | |
| .select('.link-label') | |
| .attr('x', d => { | |
| const sourceNode = nodes.find(n => n.id === d.source); | |
| const targetNode = nodes.find(n => n.id === d.target); | |
| return (sourceNode.x + targetNode.x) / 2; | |
| }) | |
| .attr('y', d => { | |
| const sourceNode = nodes.find(n => n.id === d.source); | |
| const targetNode = nodes.find(n => n.id === d.target); | |
| return (sourceNode.y + targetNode.y) / 2; | |
| }) | |
| .text(''); | |
| tempText.remove(); | |
| // Draw nodes | |
| const nodeGroup = gRoot.selectAll('.node-group').data(nodes); | |
| const nodeEnter = nodeGroup.enter().append('g').attr('class', 'node-group'); | |
| nodeEnter.append('rect').attr('class', d => `node-rect node-${d.type}`); | |
| nodeEnter.append('text').attr('class', 'node-text'); | |
| const nodeMerge = nodeEnter.merge(nodeGroup); | |
| nodeMerge.select('.node-rect') | |
| .attr('x', d => d.x - d.width / 2) | |
| .attr('y', d => d.y - d.height / 2) | |
| .attr('width', d => d.width) | |
| .attr('height', d => d.height) | |
| .attr('fill', d => { | |
| switch (d.type) { | |
| case 'question': return 'currentColor'; | |
| case 'decision': return colors.decision; | |
| case 'success': return 'currentColor'; | |
| case 'category': return 'currentColor'; | |
| default: return colors.decision; | |
| } | |
| }); | |
| nodeMerge.select('.node-text') | |
| .attr('x', d => d.x) | |
| .each(function (d) { | |
| const lines = d.wrappedLines || d.label.split('\n'); | |
| const textEl = d3.select(this); | |
| textEl.selectAll('tspan').remove(); | |
| const fontSize = d.type === 'category' ? '19px' : '18px'; | |
| const lineHeight = d.type === 'category' ? 28 : 26; | |
| textEl.attr('y', d.y); | |
| const numLines = lines.length; | |
| const totalTextHeight = (numLines - 1) * lineHeight; | |
| lines.forEach((line, i) => { | |
| const offsetFromCenter = (i - (numLines - 1) / 2) * lineHeight; | |
| textEl.append('tspan') | |
| .attr('x', d.x) | |
| .attr('dy', i === 0 ? `${offsetFromCenter}px` : `${lineHeight}px`) | |
| .attr('font-size', fontSize) | |
| .attr('text-anchor', 'middle') | |
| .attr('dominant-baseline', 'central') | |
| .text(line); | |
| }); | |
| }); | |
| // Update arrowhead color | |
| marker.select('path').attr('fill', colors.link); | |
| } | |
| // Initial render + resize handling | |
| render(); | |
| const rerender = () => render(); | |
| if (window.ResizeObserver) { | |
| const ro = new ResizeObserver(() => rerender()); | |
| ro.observe(container); | |
| } else { | |
| window.addEventListener('resize', rerender); | |
| } | |
| // Listen for theme changes | |
| const observer = new MutationObserver(() => { | |
| render(); | |
| }); | |
| if (document.documentElement) { | |
| observer.observe(document.documentElement, { | |
| attributes: true, | |
| attributeFilter: ['data-theme', 'class'] | |
| }); | |
| } | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); | |
| } else { | |
| ensureD3(bootstrap); | |
| } | |
| })(); | |
| </script> |