diff --git a/app.js b/app.js index e86b38a..edda7a1 100644 --- a/app.js +++ b/app.js @@ -942,6 +942,9 @@ function animate() { } } + // Animate eval bar chart — gentle float + evalChartGroup.position.y = 1.5 + Math.sin(elapsed * 0.55 + 1.2) * 0.18; + // Animate rune ring — orbit and vertical float for (const rune of runeSprites) { const angle = rune.baseAngle + elapsed * RUNE_ORBIT_SPEED; @@ -1834,6 +1837,265 @@ async function refreshAgentBoard() { refreshAgentBoard(); setInterval(refreshAgentBoard, 30000); +// === EVAL BAR CHART === +// 3D bar chart floating beside the platform — baseline vs fine-tuned model scores. +// Data loaded from eval-results.json; falls back to EVAL_STUB when unavailable. + +const EVAL_BAR_W = 0.38; +const EVAL_BAR_DEPTH = 0.38; +const EVAL_BAR_GAP = 0.10; // gap between baseline / fine-tuned bar within a group +const EVAL_GROUP_GAP = 0.62; // gap between metric groups +const EVAL_MAX_H = 3.8; // bar height when score = 1.0 +const EVAL_COLOR_BASE = 0x4488ff; // blue — baseline +const EVAL_COLOR_FINE = 0x00ff88; // green — fine-tuned + +const EVAL_STUB = { + title: 'Eval Harness', + metrics: [ + { name: 'Accuracy', baseline: 0.72, finetuned: 0.89 }, + { name: 'F1', baseline: 0.68, finetuned: 0.85 }, + { name: 'BLEU', baseline: 0.41, finetuned: 0.63 }, + { name: 'Recall', baseline: 0.65, finetuned: 0.83 }, + { name: 'Precision', baseline: 0.71, finetuned: 0.87 }, + ], +}; + +const evalChartGroup = new THREE.Group(); +evalChartGroup.position.set(13, 1.5, -4); +evalChartGroup.rotation.y = -Math.PI / 2.2; +scene.add(evalChartGroup); + +/** + * Canvas texture for a metric label (name + both scores) below the bar pair. + * @param {string} name + * @param {number} baseline + * @param {number} finetuned + * @returns {THREE.CanvasTexture} + */ +function createEvalMetricLabel(name, baseline, finetuned) { + const W = 256, H = 80; + const canvas = document.createElement('canvas'); + canvas.width = W; canvas.height = H; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, W, H); + + ctx.font = 'bold 17px "Courier New", monospace'; + ctx.fillStyle = '#ccd6f6'; + ctx.textAlign = 'center'; + ctx.fillText(name.toUpperCase(), W / 2, 22); + + ctx.font = '13px "Courier New", monospace'; + ctx.fillStyle = '#4488ff'; + ctx.fillText(`B: ${(baseline * 100).toFixed(0)}%`, W / 2 - 36, 52); + ctx.fillStyle = '#00ff88'; + ctx.fillText(`F: ${(finetuned * 100).toFixed(0)}%`, W / 2 + 36, 52); + + return new THREE.CanvasTexture(canvas); +} + +/** + * Canvas texture for the chart title banner. + * @param {string} title + * @returns {THREE.CanvasTexture} + */ +function createEvalChartTitle(title) { + const W = 512, H = 96; + const canvas = document.createElement('canvas'); + canvas.width = W; canvas.height = H; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, W, H); + + ctx.shadowColor = '#4488ff'; + ctx.shadowBlur = 18; + ctx.font = 'bold 26px "Courier New", monospace'; + ctx.fillStyle = '#88bbff'; + ctx.textAlign = 'center'; + ctx.fillText(title.toUpperCase(), W / 2, 38); + + ctx.shadowBlur = 0; + ctx.font = '13px "Courier New", monospace'; + ctx.fillStyle = '#334466'; + ctx.fillText('BASELINE vs FINE-TUNED', W / 2, 66); + + return new THREE.CanvasTexture(canvas); +} + +/** + * Canvas texture for the baseline / fine-tuned colour legend. + * @returns {THREE.CanvasTexture} + */ +function createEvalLegend() { + const W = 320, H = 48; + const canvas = document.createElement('canvas'); + canvas.width = W; canvas.height = H; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, W, H); + + ctx.fillStyle = '#4488ff'; + ctx.fillRect(40, 15, 20, 13); + ctx.font = '14px "Courier New", monospace'; + ctx.fillStyle = '#4488ff'; + ctx.textAlign = 'left'; + ctx.fillText('BASELINE', 68, 26); + + ctx.fillStyle = '#00ff88'; + ctx.fillRect(182, 15, 20, 13); + ctx.fillStyle = '#00ff88'; + ctx.fillText('FINE-TUNED', 210, 26); + + return new THREE.CanvasTexture(canvas); +} + +/** + * Builds (or rebuilds) the 3D bar chart from eval data. + * @param {{ title: string, metrics: Array<{ name: string, baseline: number, finetuned: number }> }} data + */ +function buildEvalBarChart(data) { + while (evalChartGroup.children.length) { + const child = evalChartGroup.children[0]; + if (child.geometry) child.geometry.dispose(); + if (child.material) { + if (child.material.map) child.material.map.dispose(); + child.material.dispose(); + } + evalChartGroup.remove(child); + } + + const metrics = data.metrics; + const n = metrics.length; + const groupW = EVAL_BAR_W * 2 + EVAL_BAR_GAP; + const totalW = n * groupW + Math.max(0, n - 1) * EVAL_GROUP_GAP; + const startX = -totalW / 2 + groupW / 2; + + // Base platform + const baseGeo = new THREE.BoxGeometry(totalW + 1.2, 0.07, EVAL_BAR_DEPTH + 1.0); + const baseMat = new THREE.MeshStandardMaterial({ + color: 0x091624, + metalness: 0.85, + roughness: 0.15, + emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.04), + }); + const base = new THREE.Mesh(baseGeo, baseMat); + base.position.y = 0.035; + evalChartGroup.add(base); + + const baseEdgeGeo = new THREE.EdgesGeometry(baseGeo); + const baseEdgeMat = new THREE.LineBasicMaterial({ color: NEXUS.colors.accent, transparent: true, opacity: 0.45 }); + const baseEdge = new THREE.LineSegments(baseEdgeGeo, baseEdgeMat); + baseEdge.position.y = 0.035; + evalChartGroup.add(baseEdge); + + // Y-axis gridlines at 25 / 50 / 75 / 100 % + for (const level of [0.25, 0.5, 0.75, 1.0]) { + const y = level * EVAL_MAX_H; + const gridGeo = new THREE.BufferGeometry().setFromPoints([ + new THREE.Vector3(-totalW / 2 - 0.2, y, 0), + new THREE.Vector3(totalW / 2 + 0.2, y, 0), + ]); + evalChartGroup.add(new THREE.Line(gridGeo, + new THREE.LineBasicMaterial({ color: 0x1a3a5a, transparent: true, opacity: 0.5 }))); + + const glCanvas = document.createElement('canvas'); + glCanvas.width = 80; glCanvas.height = 32; + const glCtx = glCanvas.getContext('2d'); + glCtx.font = '14px "Courier New", monospace'; + glCtx.fillStyle = '#334466'; + glCtx.textAlign = 'right'; + glCtx.fillText(`${Math.round(level * 100)}%`, 76, 22); + const glMat = new THREE.SpriteMaterial({ map: new THREE.CanvasTexture(glCanvas), transparent: true, depthWrite: false }); + const glSprite = new THREE.Sprite(glMat); + glSprite.scale.set(1.4, 0.55, 1); + glSprite.position.set(-totalW / 2 - 1.0, y, 0); + evalChartGroup.add(glSprite); + } + + // Bars + metrics.forEach((metric, i) => { + const cx = startX + i * (groupW + EVAL_GROUP_GAP); + + // Baseline bar + const bH = Math.max(0.04, metric.baseline * EVAL_MAX_H); + const bGeo = new THREE.BoxGeometry(EVAL_BAR_W, bH, EVAL_BAR_DEPTH); + const bMat = new THREE.MeshStandardMaterial({ + color: EVAL_COLOR_BASE, + emissive: new THREE.Color(EVAL_COLOR_BASE).multiplyScalar(0.25), + metalness: 0.1, roughness: 0.35, transparent: true, opacity: 0.88, + }); + const bMesh = new THREE.Mesh(bGeo, bMat); + bMesh.position.set(cx - (EVAL_BAR_W + EVAL_BAR_GAP) / 2, bH / 2 + 0.07, 0); + evalChartGroup.add(bMesh); + evalChartGroup.add(Object.assign( + new THREE.LineSegments(new THREE.EdgesGeometry(bGeo), + new THREE.LineBasicMaterial({ color: EVAL_COLOR_BASE, transparent: true, opacity: 0.6 })), + { position: bMesh.position.clone() } + )); + + // Fine-tuned bar + const fH = Math.max(0.04, metric.finetuned * EVAL_MAX_H); + const fGeo = new THREE.BoxGeometry(EVAL_BAR_W, fH, EVAL_BAR_DEPTH); + const fMat = new THREE.MeshStandardMaterial({ + color: EVAL_COLOR_FINE, + emissive: new THREE.Color(EVAL_COLOR_FINE).multiplyScalar(0.25), + metalness: 0.1, roughness: 0.35, transparent: true, opacity: 0.88, + }); + const fMesh = new THREE.Mesh(fGeo, fMat); + fMesh.position.set(cx + (EVAL_BAR_W + EVAL_BAR_GAP) / 2, fH / 2 + 0.07, 0); + evalChartGroup.add(fMesh); + evalChartGroup.add(Object.assign( + new THREE.LineSegments(new THREE.EdgesGeometry(fGeo), + new THREE.LineBasicMaterial({ color: EVAL_COLOR_FINE, transparent: true, opacity: 0.6 })), + { position: fMesh.position.clone() } + )); + + // Metric label below bar pair + const labelMat = new THREE.SpriteMaterial({ + map: createEvalMetricLabel(metric.name, metric.baseline, metric.finetuned), + transparent: true, depthWrite: false, + }); + const labelSprite = new THREE.Sprite(labelMat); + labelSprite.scale.set(2.4, 0.75, 1); + labelSprite.position.set(cx, -0.65, 0); + evalChartGroup.add(labelSprite); + }); + + // Title + const titleSprite = new THREE.Sprite(new THREE.SpriteMaterial({ + map: createEvalChartTitle(data.title || 'EVAL HARNESS'), + transparent: true, depthWrite: false, + })); + titleSprite.scale.set(7.2, 1.1, 1); + titleSprite.position.set(0, EVAL_MAX_H + 1.25, 0); + evalChartGroup.add(titleSprite); + + // Legend + const legendSprite = new THREE.Sprite(new THREE.SpriteMaterial({ + map: createEvalLegend(), + transparent: true, depthWrite: false, + })); + legendSprite.scale.set(5.0, 0.65, 1); + legendSprite.position.set(0, EVAL_MAX_H + 0.42, 0); + evalChartGroup.add(legendSprite); + + // Ambient glow + const evalLight = new THREE.PointLight(0x2255aa, 0.55, 12); + evalLight.position.set(0, EVAL_MAX_H / 2, 1.5); + evalChartGroup.add(evalLight); +} + +async function loadEvalResults() { + let data; + try { + const res = await fetch('./eval-results.json'); + if (!res.ok) throw new Error('not found'); + data = await res.json(); + } catch { + data = EVAL_STUB; + } + buildEvalBarChart(data); +} + +loadEvalResults(); + // === TIMMY SPEECH BUBBLE === // When Timmy sends a chat message, a glowing floating text sprite appears near // his avatar position above the platform. Fades in quickly, holds for 5 s total, diff --git a/eval-results.json b/eval-results.json new file mode 100644 index 0000000..e298a35 --- /dev/null +++ b/eval-results.json @@ -0,0 +1,10 @@ +{ + "title": "Eval Harness", + "metrics": [ + { "name": "Accuracy", "baseline": 0.72, "finetuned": 0.89 }, + { "name": "F1", "baseline": 0.68, "finetuned": 0.85 }, + { "name": "BLEU", "baseline": 0.41, "finetuned": 0.63 }, + { "name": "Recall", "baseline": 0.65, "finetuned": 0.83 }, + { "name": "Precision", "baseline": 0.71, "finetuned": 0.87 } + ] +}