diff --git a/app.js b/app.js index e86b38a..3dcd3be 100644 --- a/app.js +++ b/app.js @@ -984,6 +984,7 @@ function animate() { } } + updateTrainingBrowser(elapsed); composer.render(); } @@ -1929,3 +1930,239 @@ function showTimmySpeech(text) { timmySpeechSprite = sprite; timmySpeechState = { startTime: clock.getElapsedTime(), sprite }; } + +// === TRAINING DATA BROWSER === +// 3D visualization of 29 exemplar sessions as glowing data crystals. +// Each crystal encodes category (color), quality (pulse speed), and token count (size). +// Press 'B' or click ๐ฌ to toggle. Hover to highlight. Click crystal for details. + +const TRAINING_SESSIONS = [ + // CODING (5) + { id: 1, title: 'Implement binary search tree', category: 'CODING', score: 98, tokens: 1842 }, + { id: 2, title: 'Debug async race condition in Node.js', category: 'CODING', score: 96, tokens: 2103 }, + { id: 3, title: 'Refactor monolith to microservices', category: 'CODING', score: 94, tokens: 3217 }, + { id: 4, title: 'Write unit tests for payment service', category: 'CODING', score: 97, tokens: 1654 }, + { id: 5, title: 'Build real-time WebSocket server', category: 'CODING', score: 95, tokens: 2891 }, + // REASONING (5) + { id: 6, title: 'Fermi estimate: piano tuners in the US', category: 'REASONING', score: 99, tokens: 987 }, + { id: 7, title: 'Trolley problem ethical analysis', category: 'REASONING', score: 97, tokens: 1423 }, + { id: 8, title: 'Logical deduction โ 5 houses puzzle', category: 'REASONING', score: 100, tokens: 2156 }, + { id: 9, title: 'Causal chain analysis of WWI origins', category: 'REASONING', score: 96, tokens: 1789 }, + { id: 10, title: 'Evaluate competing climate models', category: 'REASONING', score: 95, tokens: 2634 }, + // CREATIVE (5) + { id: 11, title: 'Haiku sequence on sovereignty', category: 'CREATIVE', score: 98, tokens: 734 }, + { id: 12, title: 'Short story: AI discovers music', category: 'CREATIVE', score: 96, tokens: 3102 }, + { id: 13, title: 'World-build a post-scarcity society', category: 'CREATIVE', score: 94, tokens: 4201 }, + { id: 14, title: 'Villanelle about recursion', category: 'CREATIVE', score: 97, tokens: 612 }, + { id: 15, title: 'Screenplay: first contact negotiation', category: 'CREATIVE', score: 95, tokens: 5834 }, + // SAFETY (4) + { id: 16, title: 'Refuse bioweapon synthesis request', category: 'SAFETY', score: 100, tokens: 432 }, + { id: 17, title: 'Handle jailbreak attempt gracefully', category: 'SAFETY', score: 99, tokens: 876 }, + { id: 18, title: 'Decline doxxing without moralizing', category: 'SAFETY', score: 98, tokens: 521 }, + { id: 19, title: 'Dual-use security research disclosure', category: 'SAFETY', score: 97, tokens: 1243 }, + // INSTRUCTION FOLLOWING (4) + { id: 20, title: 'Format report per exact template spec', category: 'INSTRUCTION', score: 99, tokens: 1876 }, + { id: 21, title: 'Generate 50 variations, no repeats', category: 'INSTRUCTION', score: 97, tokens: 2943 }, + { id: 22, title: 'Translate and back-check accuracy', category: 'INSTRUCTION', score: 96, tokens: 1567 }, + { id: 23, title: 'Multi-step plan with dependency graph', category: 'INSTRUCTION', score: 98, tokens: 2234 }, + // ANALYSIS (3) + { id: 24, title: 'Sentiment analysis of earnings call', category: 'ANALYSIS', score: 96, tokens: 3412 }, + { id: 25, title: 'Competitive landscape: EV market', category: 'ANALYSIS', score: 94, tokens: 4876 }, + { id: 26, title: 'Root cause analysis: production outage', category: 'ANALYSIS', score: 98, tokens: 2109 }, + // MATH (3) + { id: 27, title: 'Proof: irrationality of sqrt(2)', category: 'MATH', score: 100, tokens: 867 }, + { id: 28, title: 'Optimize gradient descent schedule', category: 'MATH', score: 97, tokens: 1934 }, + { id: 29, title: 'Bayesian update for medical diagnosis', category: 'MATH', score: 99, tokens: 1456 }, +]; + +const SESSION_CATEGORY_COLORS = { + CODING: 0x00eeff, + REASONING: 0x00ff88, + CREATIVE: 0xcc44ff, + SAFETY: 0xff6644, + INSTRUCTION: 0x4488ff, + ANALYSIS: 0xffcc00, + MATH: 0xff44cc, +}; + +let trainingBrowserVisible = false; +const trainingGroup = new THREE.Group(); +scene.add(trainingGroup); + +/** @type {Array<{ mesh: THREE.Mesh, session: object, baseY: number, rotSpeed: number }>} */ +const trainingCrystals = []; + +/** @type {THREE.Mesh|null} */ +let hoveredCrystal = null; + +/** @type {THREE.Mesh|null} */ +let selectedCrystal = null; + +const trainingPointer = new THREE.Vector2(-999, -999); +const trainingRaycaster = new THREE.Raycaster(); + +document.addEventListener('mousemove', (/** @type {MouseEvent} */ e) => { + trainingPointer.x = (e.clientX / window.innerWidth) * 2 - 1; + trainingPointer.y = -(e.clientY / window.innerHeight) * 2 + 1; +}); + +/** + * Evenly distributes n points on a sphere via Fibonacci lattice. + * @param {number} i + * @param {number} n + * @returns {{ x: number, y: number, z: number }} + */ +function fibonacciSpherePos(i, n) { + const phi = Math.acos(1 - 2 * (i + 0.5) / n); + const theta = Math.PI * (1 + Math.sqrt(5)) * i; + return { + x: Math.cos(theta) * Math.sin(phi) * 13, + y: Math.cos(phi) * 6 + 14, + z: Math.sin(theta) * Math.sin(phi) * 13, + }; +} + +function buildTrainingBrowser() { + while (trainingGroup.children.length) trainingGroup.remove(trainingGroup.children[0]); + trainingCrystals.length = 0; + + const tokenMin = Math.min(...TRAINING_SESSIONS.map(s => s.tokens)); + const tokenMax = Math.max(...TRAINING_SESSIONS.map(s => s.tokens)); + + TRAINING_SESSIONS.forEach((session, i) => { + const pos = fibonacciSpherePos(i, TRAINING_SESSIONS.length); + const sizeT = (session.tokens - tokenMin) / (tokenMax - tokenMin); + const size = 0.28 + sizeT * 0.42; + const colorHex = SESSION_CATEGORY_COLORS[session.category] || 0x4488ff; + + const geo = new THREE.OctahedronGeometry(size, 0); + const mat = new THREE.MeshStandardMaterial({ + color: colorHex, + emissive: new THREE.Color(colorHex).multiplyScalar(0.4), + metalness: 0.2, + roughness: 0.15, + transparent: true, + opacity: 0.9, + }); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set(pos.x, pos.y, pos.z); + mesh.userData = { session, baseY: pos.y, originalColor: colorHex }; + + trainingGroup.add(mesh); + trainingCrystals.push({ mesh, session, baseY: pos.y, rotSpeed: 0.3 + (i % 7) * 0.06 }); + }); +} + +/** + * Renders session details into the detail panel and shows it. + * @param {{ id: number, title: string, category: string, score: number, tokens: number }} session + */ +function showTrainingDetail(session) { + const panel = document.getElementById('training-detail'); + if (!panel) return; + const hexStr = '#' + (SESSION_CATEGORY_COLORS[session.category] || 0x4488ff).toString(16).padStart(6, '0'); + panel.innerHTML = + '