|
|
<div class="sequence-alignment-visualization" |
|
|
style="width:100%;margin:10px 0;aspect-ratio:3/1;min-height:260px;position:relative;overflow:hidden;background:var(--surface-bg);border-radius:12px;border:1px solid var(--border-color);box-shadow:0 2px 8px rgba(0, 0, 0, 0.08);display:flex;"> |
|
|
<div class="section-container" style="flex:1;position:relative;border-right:1px dashed var(--border-color);padding:20px;"> |
|
|
<div style="position:absolute;top:10px;left:20px;font-weight:600;font-size:12px;color:var(--text-color);"> |
|
|
1. Determine the token merges |
|
|
</div> |
|
|
<canvas id="canvas-section1" style="width:100%;height:100%;display:block;"></canvas> |
|
|
</div> |
|
|
<div class="section-container" style="flex:1;position:relative;padding:20px;"> |
|
|
<div style="position:absolute;top:10px;left:20px;font-weight:600;font-size:12px;color:var(--text-color);"> |
|
|
2. Add logprob tensors in the merged positions |
|
|
</div> |
|
|
<canvas id="canvas-section2" style="width:100%;height:100%;display:block;"></canvas> |
|
|
</div> |
|
|
</div> |
|
|
<script> |
|
|
(() => { |
|
|
const getColors = () => { |
|
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; |
|
|
return { |
|
|
originalToken: isDark ? 'rgba(134, 239, 172, 0.3)' : 'rgba(187, 247, 208, 0.6)', |
|
|
subToken: isDark ? 'rgba(251, 191, 36, 0.6)' : 'rgba(253, 224, 71, 0.7)', |
|
|
mergedToken: isDark ? 'rgba(147, 197, 253, 0.4)' : 'rgba(191, 219, 254, 0.6)', |
|
|
text: isDark ? 'rgba(255, 255, 255, 0.9)' : 'rgba(0, 0, 0, 0.85)', |
|
|
line: isDark ? 'rgba(255, 255, 255, 0.25)' : 'rgba(0, 0, 0, 0.3)', |
|
|
plus: isDark ? 'rgba(255, 255, 255, 0.6)' : 'rgba(0, 0, 0, 0.6)', |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
if (!CanvasRenderingContext2D.prototype.roundRect) { |
|
|
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { |
|
|
if (w < 2 * r) r = w / 2; |
|
|
if (h < 2 * r) r = h / 2; |
|
|
this.moveTo(x + r, y); |
|
|
this.lineTo(x + w - r, y); |
|
|
this.quadraticCurveTo(x + w, y, x + w, y + r); |
|
|
this.lineTo(x + w, y + h - r); |
|
|
this.quadraticCurveTo(x + w, y + h, x + w - r, y + h); |
|
|
this.lineTo(x + r, y + h); |
|
|
this.quadraticCurveTo(x, y + h, x, y + h - r); |
|
|
this.lineTo(x, y + r); |
|
|
this.quadraticCurveTo(x, y, x + r, y); |
|
|
}; |
|
|
} |
|
|
|
|
|
const drawSection1 = (canvas, colors) => { |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const width = canvas.width; |
|
|
const height = canvas.height; |
|
|
|
|
|
ctx.clearRect(0, 0, width, height); |
|
|
|
|
|
|
|
|
const totalContentWidth = 180 + 120 + 50 + 100 + 200 + (15 * 4); |
|
|
const scale = Math.min(1, (width - 40) / totalContentWidth); |
|
|
|
|
|
const padding = 20 * scale; |
|
|
const tokenHeight = 32 * scale; |
|
|
const spacing = 15 * scale; |
|
|
const subTokenSize = 18 * scale; |
|
|
|
|
|
const originalWords = [ |
|
|
{ text: '<think>', subTokens: [0, 1, 2], width: 180 * scale }, |
|
|
{ text: 'Hugging Face', subTokens: [3], width: 120 * scale }, |
|
|
{ text: 'is', subTokens: [4], width: 50 * scale }, |
|
|
{ text: 'awesome!', subTokens: [5], width: 100 * scale }, |
|
|
{ text: '</think>', subTokens: [6, 7, 8, 9], width: 200 * scale } |
|
|
]; |
|
|
|
|
|
let currentX = padding; |
|
|
const originalY = height * 0.2; |
|
|
const subTokenY = height * 0.45; |
|
|
const mergedY = height * 0.7; |
|
|
|
|
|
|
|
|
originalWords.forEach((word) => { |
|
|
|
|
|
ctx.fillStyle = colors.originalToken; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(currentX, originalY - tokenHeight / 2, word.width, tokenHeight, 8); |
|
|
ctx.fill(); |
|
|
|
|
|
ctx.fillStyle = colors.text; |
|
|
ctx.font = `${10 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; |
|
|
ctx.textAlign = 'center'; |
|
|
ctx.textBaseline = 'middle'; |
|
|
ctx.fillText(word.text, currentX + word.width / 2, originalY); |
|
|
|
|
|
|
|
|
const subTokenSpacing = word.width / (word.subTokens.length + 1); |
|
|
word.subTokens.forEach((stId, stIdx) => { |
|
|
const stX = currentX + subTokenSpacing * (stIdx + 1) - subTokenSize / 2; |
|
|
ctx.fillStyle = colors.subToken; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(stX, subTokenY - subTokenSize / 2, subTokenSize, subTokenSize, 5); |
|
|
ctx.fill(); |
|
|
|
|
|
ctx.fillStyle = colors.text; |
|
|
ctx.font = `${9 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; |
|
|
ctx.fillText(stId.toString(), stX + subTokenSize / 2, subTokenY); |
|
|
}); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = colors.line; |
|
|
ctx.lineWidth = 1; |
|
|
ctx.setLineDash([3, 3]); |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(currentX + word.width / 2, originalY + tokenHeight / 2); |
|
|
ctx.lineTo(currentX + word.width / 2, mergedY - tokenHeight / 2); |
|
|
ctx.stroke(); |
|
|
ctx.setLineDash([]); |
|
|
|
|
|
currentX += word.width + spacing; |
|
|
}); |
|
|
|
|
|
|
|
|
const mergedTokens = [ |
|
|
{ id: 0, fromSubTokens: [0, 1, 2], width: 60 * scale }, |
|
|
{ id: 1, fromSubTokens: [3], width: 60 * scale }, |
|
|
{ id: 2, fromSubTokens: [4], width: 60 * scale }, |
|
|
{ id: 3, fromSubTokens: [5], width: 60 * scale }, |
|
|
{ id: 4, fromSubTokens: [6, 7, 8, 9], width: 60 * scale } |
|
|
]; |
|
|
|
|
|
currentX = padding + (originalWords.reduce((sum, w) => sum + w.width + spacing, -spacing) - mergedTokens.reduce((sum, t) => sum + t.width + spacing, -spacing)) / 2; |
|
|
mergedTokens.forEach((token) => { |
|
|
ctx.fillStyle = colors.mergedToken; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(currentX, mergedY - tokenHeight / 2, token.width, tokenHeight, 8); |
|
|
ctx.fill(); |
|
|
|
|
|
ctx.fillStyle = colors.text; |
|
|
ctx.font = `${10 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; |
|
|
ctx.textAlign = 'center'; |
|
|
ctx.textBaseline = 'middle'; |
|
|
ctx.fillText(token.id.toString(), currentX + token.width / 2, mergedY); |
|
|
|
|
|
currentX += token.width + spacing; |
|
|
}); |
|
|
}; |
|
|
|
|
|
const drawSection2 = (canvas, colors) => { |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const width = canvas.width; |
|
|
const height = canvas.height; |
|
|
|
|
|
ctx.clearRect(0, 0, width, height); |
|
|
|
|
|
|
|
|
const totalContentWidth = 180 + 120 + 50 + 100 + 200 + (20 * 4); |
|
|
const scale = Math.min(1, (width - 40) / totalContentWidth); |
|
|
|
|
|
const padding = 20 * scale; |
|
|
const logprobStackHeight = 60 * scale; |
|
|
const logprobRectSize = 12 * scale; |
|
|
const logprobSpacing = 3 * scale; |
|
|
const rectsPerStack = 4; |
|
|
const stackWidth = (logprobRectSize + logprobSpacing) * rectsPerStack - logprobSpacing; |
|
|
const spacing = 20 * scale; |
|
|
|
|
|
const words = [ |
|
|
{ text: '<think>', stacks: 3, width: 180 * scale }, |
|
|
{ text: 'Hugging Face', stacks: 1, width: 120 * scale }, |
|
|
{ text: 'is', stacks: 1, width: 50 * scale }, |
|
|
{ text: 'awesome!', stacks: 1, width: 100 * scale }, |
|
|
{ text: '</think>', stacks: 4, width: 200 * scale } |
|
|
]; |
|
|
|
|
|
let currentX = padding; |
|
|
const inputY = height * 0.2; |
|
|
const outputY = height * 0.7; |
|
|
|
|
|
|
|
|
words.forEach((word) => { |
|
|
const wordCenterX = currentX + word.width / 2; |
|
|
|
|
|
|
|
|
for (let s = 0; s < word.stacks; s++) { |
|
|
const stackX = wordCenterX - (word.stacks - 1) * (stackWidth + 8) / 2 + s * (stackWidth + 8); |
|
|
|
|
|
|
|
|
for (let i = 0; i < rectsPerStack; i++) { |
|
|
const rectY = inputY - logprobStackHeight / 2 + i * (logprobRectSize + logprobSpacing); |
|
|
ctx.fillStyle = colors.subToken; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(stackX, rectY, logprobRectSize, logprobRectSize, 3); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
|
|
|
if (s < word.stacks - 1) { |
|
|
ctx.fillStyle = colors.plus; |
|
|
ctx.font = `bold ${14 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; |
|
|
ctx.textAlign = 'center'; |
|
|
ctx.fillText('+', stackX + stackWidth / 2 + (4 * scale), inputY); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const labelY = inputY + logprobStackHeight / 2 + (12 * scale); |
|
|
ctx.fillStyle = colors.originalToken; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(currentX, labelY - (8 * scale), word.width, 16 * scale, 4 * scale); |
|
|
ctx.fill(); |
|
|
|
|
|
ctx.fillStyle = colors.text; |
|
|
ctx.font = `${9 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; |
|
|
ctx.textAlign = 'center'; |
|
|
ctx.textBaseline = 'middle'; |
|
|
ctx.fillText(word.text, currentX + word.width / 2, labelY); |
|
|
|
|
|
|
|
|
ctx.strokeStyle = colors.line; |
|
|
ctx.lineWidth = 1; |
|
|
ctx.setLineDash([3, 3]); |
|
|
ctx.beginPath(); |
|
|
ctx.moveTo(wordCenterX, labelY + (8 * scale)); |
|
|
ctx.lineTo(wordCenterX, outputY - logprobStackHeight / 2); |
|
|
ctx.stroke(); |
|
|
ctx.setLineDash([]); |
|
|
|
|
|
currentX += word.width + spacing; |
|
|
}); |
|
|
|
|
|
|
|
|
currentX = padding; |
|
|
words.forEach((word) => { |
|
|
const wordCenterX = currentX + word.width / 2; |
|
|
|
|
|
|
|
|
const outputRectsPerStack = 5; |
|
|
const outputStackWidth = (logprobRectSize + logprobSpacing) * outputRectsPerStack - logprobSpacing; |
|
|
const adjustedStackX = wordCenterX - outputStackWidth / 2; |
|
|
|
|
|
for (let i = 0; i < outputRectsPerStack; i++) { |
|
|
const rectY = outputY - logprobStackHeight / 2 + i * (logprobRectSize + logprobSpacing); |
|
|
const rectX = adjustedStackX + i * (logprobRectSize + logprobSpacing); |
|
|
ctx.fillStyle = colors.mergedToken; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(rectX, rectY, logprobRectSize, logprobRectSize, 3); |
|
|
ctx.fill(); |
|
|
} |
|
|
|
|
|
|
|
|
const labelY = outputY + logprobStackHeight / 2 + (12 * scale); |
|
|
ctx.fillStyle = colors.mergedToken; |
|
|
ctx.beginPath(); |
|
|
ctx.roundRect(currentX, labelY - (8 * scale), word.width, 16 * scale, 4 * scale); |
|
|
ctx.fill(); |
|
|
|
|
|
ctx.fillStyle = colors.text; |
|
|
ctx.font = `${9 * scale}px -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`; |
|
|
ctx.textAlign = 'center'; |
|
|
ctx.textBaseline = 'middle'; |
|
|
ctx.fillText(word.text, currentX + word.width / 2, labelY); |
|
|
|
|
|
currentX += word.width + spacing; |
|
|
}); |
|
|
}; |
|
|
|
|
|
const setupCanvas = (canvasId, drawFn) => { |
|
|
const container = document.querySelector('.sequence-alignment-visualization'); |
|
|
if (!container) return; |
|
|
|
|
|
const canvas = document.getElementById(canvasId); |
|
|
if (!canvas) return; |
|
|
|
|
|
const resize = () => { |
|
|
const sectionContainer = canvas.closest('.section-container'); |
|
|
if (!sectionContainer) return; |
|
|
|
|
|
const containerRect = container.getBoundingClientRect(); |
|
|
const sectionRect = sectionContainer.getBoundingClientRect(); |
|
|
|
|
|
|
|
|
const width = Math.max(100, sectionRect.width - 40); |
|
|
const height = Math.max(150, containerRect.height - 50); |
|
|
|
|
|
canvas.width = width; |
|
|
canvas.height = height; |
|
|
|
|
|
const colors = getColors(); |
|
|
drawFn(canvas, colors); |
|
|
}; |
|
|
|
|
|
|
|
|
const observer = new MutationObserver(() => { |
|
|
resize(); |
|
|
}); |
|
|
observer.observe(document.documentElement, { |
|
|
attributes: true, |
|
|
attributeFilter: ['data-theme'] |
|
|
}); |
|
|
|
|
|
if (window.ResizeObserver) { |
|
|
const ro = new ResizeObserver(resize); |
|
|
ro.observe(container); |
|
|
} else { |
|
|
window.addEventListener('resize', resize); |
|
|
} |
|
|
|
|
|
resize(); |
|
|
}; |
|
|
|
|
|
const bootstrap = () => { |
|
|
setupCanvas('canvas-section1', drawSection1); |
|
|
setupCanvas('canvas-section2', drawSection2); |
|
|
}; |
|
|
|
|
|
if (document.readyState === 'loading') { |
|
|
document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); |
|
|
} else { |
|
|
bootstrap(); |
|
|
} |
|
|
})(); |
|
|
</script> |
|
|
|