| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Post-Training Adventure</title> |
| <style> |
| .post-training-wrapper { |
| font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); |
| color: var(--text-color, #333333); |
| margin: 20px 0; |
| } |
| |
| .post-training-container { |
| position: relative; |
| width: 100%; |
| height: 250px; |
| overflow: hidden; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| } |
| |
| .node-text { |
| font-size: 12px; |
| font-weight: 500; |
| text-anchor: middle; |
| dominant-baseline: central; |
| pointer-events: none; |
| } |
| |
| .link { |
| stroke-width: 2; |
| fill: none; |
| } |
| |
| .link-direct { |
| stroke-width: 2; |
| stroke: var(--primary-color, #007bff); |
| } |
| |
| .link-via-sft { |
| stroke-width: 1.5; |
| stroke: var(--muted-color, #999999); |
| stroke-dasharray: 5, 5; |
| } |
| |
| .arrowhead { |
| fill: var(--primary-color, #007bff); |
| } |
| |
| .arrowhead-via-sft { |
| fill: var(--muted-color, #999999); |
| } |
| |
| .node-base-model { |
| fill: var(--surface-bg); |
| stroke: var(--muted-color); |
| } |
| |
| .node-sft { |
| fill: var(--surface-bg); |
| stroke: var(--muted-color); |
| } |
| |
| .node-other { |
| fill: var(--surface-bg); |
| stroke: var(--muted-color); |
| } |
| </style> |
| </head> |
|
|
| <body> |
| <div class="post-training-wrapper"> |
| <div class="post-training-container"></div> |
| </div> |
|
|
| <script> |
| |
| const CONFIG = { |
| |
| nodes: { |
| 'base': { |
| id: 'base', |
| label: 'Base model', |
| x: 400, |
| y: 80, |
| type: 'base-model', |
| width: 120, |
| height: 50 |
| }, |
| 'sft': { |
| id: 'sft', |
| label: 'SFT', |
| x: 400, |
| y: 225, |
| type: 'sft', |
| width: 80, |
| height: 40 |
| }, |
| 'orpo': { |
| id: 'orpo', |
| label: 'ORPO', |
| x: 175, |
| y: 360, |
| type: 'other', |
| width: 140, |
| height: 40 |
| }, |
| 'dpo': { |
| id: 'dpo', |
| label: 'DPO and friends', |
| x: 325, |
| y: 360, |
| type: 'other', |
| width: 140, |
| height: 40 |
| }, |
| 'rl': { |
| id: 'rl', |
| label: 'RL (here be dragons)', |
| x: 475, |
| y: 360, |
| type: 'other', |
| width: 140, |
| height: 40 |
| }, |
| 'kto': { |
| id: 'kto', |
| label: 'KTO', |
| x: 625, |
| y: 360, |
| type: 'other', |
| width: 140, |
| height: 40 |
| } |
| }, |
| |
| |
| links: [ |
| { from: 'base', to: 'sft', type: 'direct' }, |
| { from: 'base', to: 'orpo', type: 'direct' }, |
| { from: 'base', to: 'dpo', type: 'direct' }, |
| { from: 'base', to: 'rl', type: 'direct' }, |
| { from: 'base', to: 'kto', type: 'direct' }, |
| { from: 'sft', to: 'orpo', type: 'via-sft' }, |
| { from: 'sft', to: 'dpo', type: 'via-sft' }, |
| { from: 'sft', to: 'rl', type: 'via-sft' }, |
| { from: 'sft', to: 'kto', type: 'via-sft' } |
| ], |
| |
| |
| colors: { |
| linkColor: 'var(--primary-color, #007bff)', |
| nodeFill: 'var(--page-bg, #ffffff)', |
| nodeStroke: 'var(--muted-color, #666666)', |
| nodeText: 'var(--text-color, #333333)' |
| } |
| }; |
| |
| |
| class PostTrainingRenderer { |
| constructor(container) { |
| this.container = container; |
| this.svg = null; |
| this.colors = this.getColors(); |
| |
| this.init(); |
| } |
| |
| getColors() { |
| |
| return ['var(--primary-color, #007bff)', 'var(--muted-color, #6c757d)']; |
| } |
| |
| init() { |
| |
| this.svg = d3.select(this.container) |
| .append('svg') |
| .attr('width', '100%') |
| .attr('height', '100%') |
| .attr('viewBox', '50 50 700 350'); |
| |
| this.render(); |
| } |
| |
| render() { |
| |
| this.svg.selectAll('*').remove(); |
| |
| |
| this.createArrowheadMarkers(); |
| |
| |
| this.svg.selectAll('.link') |
| .data(CONFIG.links) |
| .enter() |
| .append('path') |
| .attr('class', d => `link link-${d.type}`) |
| .attr('d', d => this.getLinkPath(d)) |
| .attr('fill', 'none') |
| .attr('stroke', d => d.type === 'direct' ? this.colors[0] : this.colors[1]) |
| .attr('marker-end', d => `url(#arrowhead-${d.type})`); |
| |
| |
| const nodeGroups = this.svg.selectAll('.node-group') |
| .data(Object.values(CONFIG.nodes)) |
| .enter() |
| .append('g') |
| .attr('class', 'node-group') |
| .attr('transform', d => `translate(${d.x - d.width / 2}, ${d.y - d.height / 2})`); |
| |
| |
| nodeGroups.append('rect') |
| .attr('class', d => `node node-${d.type}`) |
| .attr('width', d => d.width) |
| .attr('height', d => d.height) |
| .attr('rx', 8) |
| .attr('ry', 8) |
| .attr('stroke-width', 2) |
| .attr('fill', d => { |
| if (d.type === 'base-model') return 'var(--surface-bg)'; |
| if (d.type === 'sft') return `color-mix(in srgb, ${this.colors[0]} 25%, var(--surface-bg))`; |
| return `color-mix(in srgb, ${this.colors[1]} 25%, var(--surface-bg))`; |
| }) |
| .attr('stroke', d => { |
| if (d.type === 'base-model') return 'var(--muted-color)'; |
| if (d.type === 'sft') return `color-mix(in srgb, ${this.colors[0]} 60%, var(--surface-bg))`; |
| return `color-mix(in srgb, ${this.colors[1]} 60%, var(--surface-bg))`; |
| }); |
| |
| |
| nodeGroups.append('text') |
| .attr('class', 'node-text') |
| .attr('x', d => d.width / 2) |
| .attr('y', d => d.height / 2) |
| .text(d => d.label) |
| .style('font-size', '12px') |
| .style('font-weight', '500') |
| .style('text-anchor', 'middle') |
| .style('dominant-baseline', 'central') |
| .style('pointer-events', 'none'); |
| } |
| |
| createArrowheadMarkers() { |
| |
| const defs = this.svg.append('defs'); |
| |
| |
| defs.append('marker') |
| .attr('id', 'arrowhead-direct') |
| .attr('viewBox', '0 -5 10 10') |
| .attr('refX', 8) |
| .attr('refY', 0) |
| .attr('markerWidth', 6) |
| .attr('markerHeight', 6) |
| .attr('orient', 'auto') |
| .append('path') |
| .attr('d', 'M0,-5L10,0L0,5') |
| .attr('fill', this.colors[0]); |
| |
| |
| defs.append('marker') |
| .attr('id', 'arrowhead-via-sft') |
| .attr('viewBox', '0 -5 10 10') |
| .attr('refX', 8) |
| .attr('refY', 0) |
| .attr('markerWidth', 6) |
| .attr('markerHeight', 6) |
| .attr('orient', 'auto') |
| .append('path') |
| .attr('d', 'M0,-5L10,0L0,5') |
| .attr('fill', this.colors[1]); |
| } |
| |
| getLinkPath(link) { |
| const source = CONFIG.nodes[link.from]; |
| const target = CONFIG.nodes[link.to]; |
| |
| if (!source || !target) return ''; |
| |
| |
| const nodeDistance = 15; |
| |
| let sourceX, sourceY, targetX, targetY; |
| |
| |
| if (link.from === 'base' && link.to === 'sft') { |
| |
| sourceX = source.x; |
| sourceY = source.y + source.height / 2 + nodeDistance; |
| |
| |
| targetX = target.x; |
| targetY = target.y - target.height / 2 - nodeDistance; |
| } else { |
| |
| sourceX = source.x; |
| sourceY = source.y + source.height / 2 + nodeDistance; |
| |
| targetX = target.x; |
| targetY = target.y - target.height / 2 - nodeDistance; |
| |
| |
| const targetConnections = CONFIG.links.filter(l => l.to === link.to); |
| if (targetConnections.length > 1) { |
| |
| if (link.to === 'rl' || link.to === 'kto') { |
| |
| const sortedConnections = targetConnections.sort((a, b) => { |
| if (a.from === 'sft' && b.from === 'base') return -1; |
| if (a.from === 'base' && b.from === 'sft') return 1; |
| return 0; |
| }); |
| |
| const linkIndex = sortedConnections.findIndex(l => l.from === link.from && l.to === link.to); |
| const totalLinks = targetConnections.length; |
| const spacing = 20; |
| const offset = (linkIndex - (totalLinks - 1) / 2) * spacing; |
| |
| targetX += offset; |
| sourceX += offset; |
| } else { |
| |
| const linkIndex = targetConnections.findIndex(l => l.from === link.from && l.to === link.to); |
| const totalLinks = targetConnections.length; |
| const spacing = 20; |
| const offset = (linkIndex - (totalLinks - 1) / 2) * spacing; |
| |
| targetX += offset; |
| sourceX += offset; |
| } |
| } |
| |
| |
| const sourceConnections = CONFIG.links.filter(l => l.from === link.from); |
| if (sourceConnections.length > 1) { |
| |
| const sortedConnections = sourceConnections.sort((a, b) => { |
| const nodeA = CONFIG.nodes[a.to]; |
| const nodeB = CONFIG.nodes[b.to]; |
| return nodeA.x - nodeB.x; |
| }); |
| |
| const linkIndex = sortedConnections.findIndex(l => l.from === link.from && l.to === link.to); |
| const totalLinks = sourceConnections.length; |
| const spacing = 8; |
| const offset = (linkIndex - (totalLinks - 1) / 2) * spacing; |
| |
| sourceX += offset; |
| targetX += offset; |
| } |
| } |
| |
| |
| const dx = targetX - sourceX; |
| const dy = targetY - sourceY; |
| const distance = Math.sqrt(dx * dx + dy * dy); |
| |
| |
| const controlOffset = Math.min(distance * 0.3, 50); |
| const controlX = sourceX + dx / 2; |
| const controlY = sourceY + dy / 2 - controlOffset; |
| |
| return `M ${sourceX} ${sourceY} Q ${controlX} ${controlY} ${targetX} ${targetY}`; |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const container = document.querySelector('.post-training-container'); |
| new PostTrainingRenderer(container); |
| }); |
| </script> |
|
|
| |
| <script src="https://d3js.org/d3.v7.min.js"></script> |
| </body> |
|
|
| </html> |