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 = + '
' + + '#' + String(session.id).padStart(2, '0') + '' + + '' + session.category + '' + + '
' + + '
' + session.title + '
' + + '
' + + 'SCORE ' + session.score + '' + + 'TOKENS ' + session.tokens.toLocaleString() + '' + + '
' + + '
'; + panel.classList.add('visible'); +} + +function hideTrainingDetail() { + const panel = document.getElementById('training-detail'); + if (panel) panel.classList.remove('visible'); +} + +function toggleTrainingBrowser() { + trainingBrowserVisible = !trainingBrowserVisible; + trainingGroup.visible = trainingBrowserVisible; + const hud = document.getElementById('training-browser-hud'); + if (hud) hud.classList.toggle('visible', trainingBrowserVisible); + if (!trainingBrowserVisible) { + hideTrainingDetail(); + if (hoveredCrystal) { hoveredCrystal.scale.setScalar(1); hoveredCrystal = null; } + if (selectedCrystal) { selectedCrystal = null; } + document.body.style.cursor = ''; + } +} + +document.addEventListener('click', (/** @type {MouseEvent} */ e) => { + if (!trainingBrowserVisible) return; + // Ignore clicks on HUD buttons + if (e.target instanceof HTMLElement && e.target.closest('button, #training-detail')) return; + trainingRaycaster.setFromCamera(trainingPointer, camera); + const hits = trainingRaycaster.intersectObjects(trainingGroup.children); + if (hits.length > 0) { + if (selectedCrystal && selectedCrystal !== hits[0].object) { + selectedCrystal.scale.setScalar(hoveredCrystal === selectedCrystal ? 1.3 : 1); + } + selectedCrystal = /** @type {THREE.Mesh} */ (hits[0].object); + showTrainingDetail(selectedCrystal.userData.session); + } else { + if (selectedCrystal) { selectedCrystal.scale.setScalar(1); selectedCrystal = null; } + hideTrainingDetail(); + } +}); + +document.addEventListener('keydown', (/** @type {KeyboardEvent} */ e) => { + if ((e.key === 'b' || e.key === 'B') && !e.metaKey && !e.ctrlKey && !e.altKey) { + toggleTrainingBrowser(); + } +}); + +const trainingBrowserBtn = document.getElementById('training-browser-btn'); +if (trainingBrowserBtn) { + trainingBrowserBtn.addEventListener('click', toggleTrainingBrowser); +} + +buildTrainingBrowser(); +trainingGroup.visible = false; + +// === TRAINING BROWSER ANIMATION (injected into the frame loop via a separate updater) === +// Called from within animate() each frame. +function updateTrainingBrowser(elapsed) { + if (!trainingBrowserVisible) return; + + trainingRaycaster.setFromCamera(trainingPointer, camera); + const hits = trainingRaycaster.intersectObjects(trainingGroup.children); + const nowHovered = hits.length > 0 ? /** @type {THREE.Mesh} */ (hits[0].object) : null; + + if (hoveredCrystal !== nowHovered) { + if (hoveredCrystal && hoveredCrystal !== selectedCrystal) { + hoveredCrystal.scale.setScalar(1); + } + if (nowHovered && nowHovered !== selectedCrystal) { + nowHovered.scale.setScalar(1.3); + document.body.style.cursor = 'pointer'; + } else if (!nowHovered) { + document.body.style.cursor = ''; + } + hoveredCrystal = nowHovered; + } + + for (const { mesh, baseY, rotSpeed } of trainingCrystals) { + mesh.rotation.x += rotSpeed * 0.01; + mesh.rotation.y += rotSpeed * 0.008; + mesh.position.y = baseY + Math.sin(elapsed * 0.6 + mesh.userData.session.id) * 0.3; + + const isSelected = mesh === selectedCrystal; + const isHovered = mesh === hoveredCrystal; + const pulse = 0.3 + Math.sin(elapsed * 1.5 + mesh.userData.session.id * 0.8) * 0.15; + const intensity = isSelected ? 0.9 : isHovered ? 0.75 : pulse; + + const c = new THREE.Color(mesh.userData.originalColor); + /** @type {THREE.MeshStandardMaterial} */ (mesh.material).emissive.copy(c).multiplyScalar(intensity); + + if (isSelected) { + const pulsedScale = 1.5 + Math.sin(elapsed * 2.5) * 0.08; + mesh.scale.setScalar(pulsedScale); + } + } +} diff --git a/index.html b/index.html index 73dd69a..1c786bb 100644 --- a/index.html +++ b/index.html @@ -36,6 +36,9 @@ + @@ -61,5 +64,11 @@
+
+ TRAINING BROWSER + 29 SESSIONS + [B] close  ยท  click crystal for details +
+
diff --git a/style.css b/style.css index 3dc0e04..7ebbe9a 100644 --- a/style.css +++ b/style.css @@ -243,3 +243,115 @@ body.photo-mode #overview-indicator { 50% { opacity: 0.15; } 100% { opacity: 0.05; } } + +/* === TRAINING DATA BROWSER === */ +#training-browser-hud { + display: none; + position: fixed; + top: 8px; + left: 50%; + transform: translateX(-50%); + align-items: center; + gap: 12px; + color: var(--color-primary); + font-family: var(--font-body); + font-size: 11px; + letter-spacing: 0.2em; + text-transform: uppercase; + pointer-events: none; + z-index: 20; + border: 1px solid var(--color-primary); + padding: 4px 14px; + background: rgba(0, 0, 8, 0.6); + white-space: nowrap; + animation: overview-pulse 2s ease-in-out infinite; +} + +#training-browser-hud.visible { + display: flex; +} + +.training-count { + color: #00eeff; + font-size: 10px; +} + +.training-hint { + color: var(--color-text-muted); + font-size: 10px; +} + +#training-detail { + display: none; + position: fixed; + bottom: 28px; + left: 50%; + transform: translateX(-50%); + min-width: 340px; + background: rgba(0, 4, 18, 0.92); + border: 1px solid var(--color-primary); + padding: 14px 18px; + z-index: 25; + pointer-events: none; + font-family: var(--font-body); +} + +#training-detail.visible { + display: block; + animation: overview-pulse 2s ease-in-out infinite; +} + +.td-header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 6px; +} + +.td-id { + font-size: 11px; + color: var(--color-text-muted); + letter-spacing: 0.1em; +} + +.td-category { + font-size: 10px; + letter-spacing: 0.2em; + text-transform: uppercase; +} + +.td-title { + font-size: 14px; + color: var(--color-text); + margin-bottom: 10px; + line-height: 1.4; +} + +.td-stats { + display: flex; + gap: 20px; + margin-bottom: 8px; + font-size: 10px; + color: var(--color-text-muted); + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.td-stats strong { + color: var(--color-primary); + font-size: 13px; +} + +.td-bar { + height: 3px; + background: rgba(68, 136, 255, 0.15); + border-radius: 2px; + overflow: hidden; +} + +.td-bar-fill { + height: 100%; + border-radius: 2px; + opacity: 0.85; + transition: width 0.3s ease; +}