Files
JARVIS/frontend/prototypes/holographic-brain-chat.html

867 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>J.A.R.V.I.S. - Neural Command Center</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
:root {
--jarvis-blue: #00f3ff;
--jarvis-blue-dim: rgba(0, 243, 255, 0.1);
--bg-color: #020408;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: var(--bg-color);
color: var(--jarvis-blue);
font-family: 'Orbitron', 'Share Tech Mono', monospace;
overflow: hidden;
margin: 0;
height: 100vh;
width: 100vw;
}
.scanlines {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 100;
background: linear-gradient(to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0) 50%,
rgba(0, 0, 0, 0.08) 50%,
rgba(0, 0, 0, 0.08));
background-size: 100% 3px;
}
.vignette {
position: fixed;
inset: 0;
background: radial-gradient(circle, transparent 40%, rgba(0, 0, 0, 0.8) 100%);
pointer-events: none;
z-index: 90;
}
.text-glow {
text-shadow: 0 0 8px var(--jarvis-blue), 0 0 15px rgba(0, 243, 255, 0.5);
}
.tech-panel {
background: rgba(0, 12, 28, 0.75);
border: 1px solid rgba(0, 243, 255, 0.2);
position: relative;
backdrop-filter: blur(8px);
transition: all 0.3s ease;
}
.tech-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 10px;
height: 10px;
border-top: 1px solid var(--jarvis-blue);
border-left: 1px solid var(--jarvis-blue);
}
.tech-panel::after {
content: '';
position: absolute;
bottom: 0;
right: 0;
width: 10px;
height: 10px;
border-bottom: 1px solid var(--jarvis-blue);
border-right: 1px solid var(--jarvis-blue);
}
@keyframes pulse-status {
0%, 100% { opacity: 0.7; text-shadow: 0 0 5px var(--jarvis-blue); }
50% { opacity: 1; text-shadow: 0 0 15px var(--jarvis-blue), 0 0 30px rgba(0, 243, 255, 0.3); }
}
.status-pulse {
animation: pulse-status 2s infinite ease-in-out;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes typing-bounce {
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
30% { transform: translateY(-4px); opacity: 1; }
}
.typing-dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--jarvis-blue);
margin: 0 3px;
animation: typing-bounce 1.2s ease-in-out infinite;
}
.typing-dot:nth-child(2) { animation-delay: 0.2s; }
.typing-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes msg-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.msg-animate {
animation: msg-in 0.3s ease both;
}
#brain-canvas {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
opacity: 0.7;
}
.chat-panel {
position: relative;
z-index: 10;
background: rgba(0, 8, 20, 0.85);
}
.msg-bubble {
background: rgba(0, 30, 50, 0.6);
border: 1px solid rgba(0, 243, 255, 0.15);
padding: 10px 14px;
border-radius: 4px 12px 12px 12px;
max-width: 85%;
line-height: 1.6;
font-size: 13px;
}
.msg-bubble.user {
background: rgba(0, 243, 255, 0.08);
border-color: rgba(0, 243, 255, 0.25);
border-radius: 12px 4px 12px 12px;
}
.input-holo {
background: rgba(0, 15, 30, 0.7);
border: 1px solid rgba(0, 243, 255, 0.3);
padding: 12px 16px;
border-radius: 8px;
width: 100%;
color: #e0f7ff;
font-family: 'Share Tech Mono', monospace;
font-size: 13px;
outline: none;
transition: all 0.3s;
}
.input-holo:focus {
border-color: rgba(0, 243, 255, 0.6);
box-shadow: 0 0 20px rgba(0, 243, 255, 0.15);
}
.input-holo::placeholder {
color: rgba(0, 243, 255, 0.4);
}
.send-btn {
background: rgba(0, 243, 255, 0.1);
border: 1px solid rgba(0, 243, 255, 0.3);
color: var(--jarvis-blue);
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
font-family: 'Orbitron', monospace;
font-size: 11px;
letter-spacing: 0.1em;
transition: all 0.3s;
}
.send-btn:hover {
background: rgba(0, 243, 255, 0.2);
box-shadow: 0 0 20px rgba(0, 243, 255, 0.3);
}
.send-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
box-shadow: 0 0 8px currentColor;
}
.node-item {
padding: 6px 10px;
border-bottom: 1px solid rgba(0, 243, 255, 0.08);
cursor: pointer;
transition: all 0.2s;
font-size: 10px;
}
.node-item:hover {
background: rgba(0, 243, 255, 0.05);
}
.node-item.selected {
background: rgba(0, 243, 255, 0.1);
border-left: 2px solid var(--jarvis-blue);
}
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.3); }
::-webkit-scrollbar-thumb { background: rgba(0, 243, 255, 0.4); border-radius: 2px; }
.metric-bar {
height: 3px;
background: rgba(0, 243, 255, 0.2);
border-radius: 2px;
overflow: hidden;
}
.metric-fill {
height: 100%;
background: var(--jarvis-blue);
box-shadow: 0 0 8px var(--jarvis-blue);
transition: width 0.5s ease-out;
}
</style>
</head>
<body>
<div class="scanlines"></div>
<div class="vignette"></div>
<div class="relative z-30 h-full w-full flex flex-col">
<!-- HEADER -->
<header class="flex justify-between items-center px-6 py-3 border-b border-cyan-500/20 bg-black/40 backdrop-blur-md">
<div class="flex items-center gap-4">
<div class="flex flex-col items-center">
<div class="w-2 h-2 bg-cyan-400 rounded-full shadow-[0_0_10px_#00f3ff]"></div>
<div class="w-px h-6 bg-cyan-500/50 my-1"></div>
</div>
<div>
<h1 class="text-2xl font-bold tracking-[0.2em] text-glow">J.A.R.V.I.S.</h1>
<div class="flex items-center gap-2 mt-1">
<div class="h-1 w-16 bg-cyan-500/30 overflow-hidden rounded">
<div class="h-full bg-cyan-400 w-1/3 animate-pulse"></div>
</div>
<span class="text-[9px] tracking-[0.3em] opacity-60">NEURAL COMMAND v2.0</span>
</div>
</div>
</div>
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<span class="text-[10px] tracking-widest opacity-60">BRAIN MATRIX</span>
<span id="brain-status" class="text-xs font-bold px-3 py-1 rounded status-pulse bg-cyan-900/40 border border-cyan-500/30">ONLINE</span>
</div>
<div class="text-right">
<div class="text-[10px] opacity-50">SYS_TIME</div>
<div id="time-display" class="font-mono text-lg text-cyan-200 tracking-wider">00:00:00</div>
</div>
</div>
</header>
<!-- MAIN CONTENT -->
<div class="flex-grow flex relative">
<!-- BRAIN CANVAS BACKGROUND -->
<div id="brain-canvas" class="absolute inset-0 pointer-events-none"></div>
<!-- LEFT PANEL: Brain Metrics -->
<aside class="w-64 p-4 flex flex-col gap-4 relative z-20 bg-gradient-to-b from-black/60 to-transparent">
<div class="tech-panel p-4">
<h3 class="text-xs font-bold tracking-widest text-cyan-300 mb-3 border-b border-cyan-500/20 pb-2">NODE REGISTRY</h3>
<div id="node-list" class="max-h-40 overflow-y-auto space-y-1">
<!-- nodes injected -->
</div>
</div>
<div class="tech-panel p-4">
<h3 class="text-xs font-bold tracking-widest text-cyan-300 mb-3 border-b border-cyan-500/20 pb-2">BRAIN METRICS</h3>
<div class="space-y-3 text-[10px]">
<div>
<div class="flex justify-between mb-1"><span class="opacity-60">NODES</span><span id="m-nodes" class="text-cyan-300">14</span></div>
<div class="metric-bar"><div id="bar-nodes" class="metric-fill" style="width: 70%"></div></div>
</div>
<div>
<div class="flex justify-between mb-1"><span class="opacity-60">LINKS</span><span id="m-links" class="text-cyan-300">20</span></div>
<div class="metric-bar"><div id="bar-links" class="metric-fill" style="width: 66%"></div></div>
</div>
<div>
<div class="flex justify-between mb-1"><span class="opacity-60">CLUSTERS</span><span id="m-clusters" class="text-cyan-300">3</span></div>
<div class="metric-bar"><div id="bar-clusters" class="metric-fill" style="width: 60%"></div></div>
</div>
</div>
</div>
<div class="tech-panel p-4 flex-grow">
<h3 class="text-xs font-bold tracking-widest text-cyan-300 mb-3 border-b border-cyan-500/20 pb-2">SIGNAL LEGEND</h3>
<div class="space-y-2 text-[11px]">
<div class="flex items-center gap-2">
<div class="legend-dot" style="background: #00f3ff; color: #00f3ff;"></div>
<span class="text-cyan-300">KNOWLEDGE</span>
</div>
<div class="flex items-center gap-2">
<div class="legend-dot" style="background: #ff6b9d; color: #ff6b9d;"></div>
<span class="text-cyan-300">CHAT</span>
</div>
<div class="flex items-center gap-2">
<div class="legend-dot" style="background: #a855f7; color: #a855f7;"></div>
<span class="text-cyan-300">FORUM</span>
</div>
<div class="flex items-center gap-2">
<div class="legend-dot" style="background: #fbbf24; color: #fbbf24;"></div>
<span class="text-cyan-300">SCHEDULE</span>
</div>
</div>
</div>
</aside>
<!-- CENTER: Brain Activity (clickable area) -->
<div class="flex-grow relative z-10 flex flex-col justify-end pointer-events-none">
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto" id="brain-click-zone">
<!-- Brain visualization is rendered here by Three.js -->
</div>
<!-- Terminal Log at bottom -->
<div class="tech-panel m-4 p-3 pointer-events-auto max-h-40 overflow-y-auto">
<div class="text-[10px] text-cyan-500/70 tracking-widest mb-2">// SYSTEM LOG</div>
<div id="terminal-log" class="space-y-1 text-[10px] font-mono">
<div class="text-cyan-400/60">>> Neural matrix initialized.</div>
<div class="text-cyan-400/60">>> 14 nodes, 20 connections active.</div>
</div>
</div>
</div>
<!-- RIGHT PANEL: Chat -->
<aside class="w-96 p-4 flex flex-col gap-4 relative z-20 bg-gradient-to-l from-black/60 to-transparent">
<div class="tech-panel p-4 flex-grow flex flex-col overflow-hidden">
<div class="flex justify-between items-center mb-3 border-b border-cyan-500/20 pb-2">
<h3 class="text-xs font-bold tracking-widest text-cyan-300">NEURAL CHAT</h3>
<span class="text-[9px] bg-cyan-500/20 px-2 py-1 rounded status-pulse">LIVE</span>
</div>
<div id="chat-messages" class="flex-grow overflow-y-auto space-y-3 mb-4 pr-1">
<!-- Welcome -->
<div class="text-center py-8">
<div class="text-3xl font-bold tracking-[0.3em] text-glow mb-2">JARVIS</div>
<div class="text-[10px] tracking-[0.2em] opacity-50">STRATEGIC THINKING PARTNER</div>
<div class="text-xs opacity-40 mt-4">有什么我可以帮您分析的?</div>
</div>
</div>
<!-- Chat Input -->
<div class="flex gap-2">
<input type="text" id="chat-input" class="input-holo flex-grow" placeholder="输入指令..." onkeydown="if(event.key==='Enter')sendMsg()">
<button id="send-btn" class="send-btn" onclick="sendMsg()">SEND</button>
</div>
</div>
<!-- Selected Node Detail -->
<div id="node-detail" class="tech-panel p-4 hidden">
<div class="flex justify-between items-center mb-2 border-b border-cyan-500/20 pb-2">
<h3 class="text-xs font-bold tracking-widest text-cyan-300">SELECTED NODE</h3>
<button onclick="clearSelection()" class="text-cyan-500/60 hover:text-cyan-300 text-lg">×</button>
</div>
<div id="detail-content" class="text-[11px] space-y-2">
<!-- detail injected -->
</div>
</div>
</aside>
</div>
</div>
<script>
(function() {
const NODES = [
{ id: 'core-1', name: 'Orchestrator Prime', type: 'knowledge', importance: 0.98, description: '中枢编排核心' },
{ id: 'core-2', name: 'Memory Vault', type: 'knowledge', importance: 0.91, description: '知识存储层' },
{ id: 'chat-1', name: 'Session Cluster', type: 'chat', importance: 0.89, description: '实时会话汇聚' },
{ id: 'chat-2', name: 'Operator Feed', type: 'chat', importance: 0.76, description: '操控台输入流' },
{ id: 'forum-1', name: 'Research Swarm', type: 'forum', importance: 0.81, description: '外部案例趋势池' },
{ id: 'forum-2', name: 'Threat Monitor', type: 'forum', importance: 0.72, description: '异常风险观察哨' },
{ id: 'sched-1', name: 'Sprint Grid', type: 'schedule', importance: 0.87, description: '执行排期矩阵' },
{ id: 'sched-2', name: 'Review Window', type: 'schedule', importance: 0.74, description: '评审验收窗口' },
{ id: 'bridge-1', name: 'Signal Bridge', type: 'knowledge', importance: 0.84, description: '跨域桥接节点' },
{ id: 'arch-1', name: 'Knowledge Archive', type: 'knowledge', importance: 0.66, description: '知识存档' },
{ id: 'thread-1', name: 'Conversation Thread', type: 'chat', importance: 0.62, description: '对话线程' },
{ id: 'signal-1', name: 'Forum Signal', type: 'forum', importance: 0.58, description: '外部论坛信号' },
{ id: 'lane-1', name: 'Schedule Lane', type: 'schedule', importance: 0.57, description: '排程支线' },
{ id: 'dormant-1', name: 'Dormant Trace', type: 'forum', importance: 0.45, description: '低优先级线索' },
];
const EDGES = [
{ id: 'e1', source: 'core-1', target: 'core-2', relation: '读取知识' },
{ id: 'e2', source: 'core-1', target: 'chat-1', relation: '接收会话' },
{ id: 'e3', source: 'core-1', target: 'sched-1', relation: '下发编排' },
{ id: 'e4', source: 'core-1', target: 'bridge-1', relation: '稳定桥接' },
{ id: 'e5', source: 'core-2', target: 'bridge-1', relation: '提供事实' },
{ id: 'e6', source: 'chat-1', target: 'chat-2', relation: '扩展输入' },
{ id: 'e7', source: 'chat-1', target: 'bridge-1', relation: '触发同步' },
{ id: 'e8', source: 'forum-1', target: 'bridge-1', relation: '补充趋势' },
{ id: 'e9', source: 'forum-2', target: 'bridge-1', relation: '反馈风险' },
{ id: 'e10', source: 'bridge-1', target: 'sched-1', relation: '投递任务' },
{ id: 'e11', source: 'sched-1', target: 'sched-2', relation: '进入评审' },
{ id: 'e12', source: 'forum-1', target: 'sched-2', relation: '支撑验证' },
{ id: 'e13', source: 'core-2', target: 'arch-1', relation: '归档事实' },
{ id: 'e14', source: 'bridge-1', target: 'arch-1', relation: '桥接引用' },
{ id: 'e15', source: 'chat-1', target: 'thread-1', relation: '拆分会话' },
{ id: 'e16', source: 'chat-2', target: 'thread-1', relation: '补充回执' },
{ id: 'e17', source: 'forum-1', target: 'signal-1', relation: '吸收案例' },
{ id: 'e18', source: 'sched-1', target: 'lane-1', relation: '拆分排期' },
{ id: 'e19', source: 'sched-2', target: 'lane-1', relation: '投递评审' },
{ id: 'e20', source: 'forum-2', target: 'dormant-1', relation: '弱风险' },
];
const TYPE_COLORS = { knowledge: 0x00f3ff, chat: 0xff6b9d, forum: 0xa855f7, schedule: 0xfbbf24 };
const TYPE_LABELS = { knowledge: '知识库', chat: '对话', forum: '论坛', schedule: '日程' };
let scene, camera, renderer;
let nodeObjects = new Map();
let particleSystem, coreMesh, innerMesh;
let hoveredNodeId = null;
let selectedNodeId = null;
let mouseX = 0, mouseY = 0;
let targetRotX = 0, targetRotY = 0;
let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;
const nodeMeshes = [];
function updateTime() {
const now = new Date();
const el = document.getElementById('time-display');
if (el) el.textContent = now.toLocaleTimeString('en-US', { hour12: false });
}
setInterval(updateTime, 1000);
updateTime();
function init() {
const container = document.getElementById('brain-canvas');
const w = window.innerWidth;
const h = window.innerHeight;
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x020408, 0.003);
camera = new THREE.PerspectiveCamera(55, w / h, 0.1, 2000);
camera.position.z = 70;
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(w, h);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.3;
container.style.width = w + 'px';
container.style.height = h + 'px';
container.appendChild(renderer.domElement);
const ambient = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambient);
const point = new THREE.PointLight(0x00f3ff, 2, 250);
point.position.set(0, 30, 50);
scene.add(point);
createBackground();
createBrainCore();
createNodes();
createEdges();
createOrbitalRings();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('wheel', onWheel);
window.addEventListener('resize', onResize);
updateNodeList();
animate();
}
function createBackground() {
const count = 1200;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * 500;
positions[i * 3 + 1] = (Math.random() - 0.5) * 500;
positions[i * 3 + 2] = (Math.random() - 0.5) * 500;
const c = new THREE.Color();
c.setHSL(0.55 + Math.random() * 0.1, 0.8, 0.5 + Math.random() * 0.3);
colors[i * 3] = c.r;
colors[i * 3 + 1] = c.g;
colors[i * 3 + 2] = c.b;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const mat = new THREE.PointsMaterial({
size: 0.4, vertexColors: true, transparent: true, opacity: 0.5,
blending: THREE.AdditiveBlending,
});
particleSystem = new THREE.Points(geo, mat);
scene.add(particleSystem);
}
function createBrainCore() {
coreMesh = new THREE.Mesh(
new THREE.IcosahedronGeometry(3, 2),
new THREE.MeshBasicMaterial({ color: 0x00f3ff, wireframe: true, transparent: true, opacity: 0.1 })
);
scene.add(coreMesh);
innerMesh = new THREE.Mesh(
new THREE.IcosahedronGeometry(1.8, 1),
new THREE.MeshBasicMaterial({ color: 0x00f3ff, transparent: true, opacity: 0.15 })
);
scene.add(innerMesh);
}
function createHexShape(r) {
const shape = new THREE.Shape();
for (let i = 0; i < 6; i++) {
const a = (i / 6) * Math.PI * 2 - Math.PI / 6;
const x = Math.cos(a) * r, y = Math.sin(a) * r;
if (i === 0) shape.moveTo(x, y); else shape.lineTo(x, y);
}
shape.closePath();
return shape;
}
function createNodes() {
const angleStep = (Math.PI * 2) / NODES.length;
const radius = 28;
NODES.forEach((node, i) => {
const angle = i * angleStep;
const layer = Math.floor(i / 7);
const r = radius + layer * 16;
const x = Math.cos(angle) * r;
const z = Math.sin(angle) * r;
const y = (Math.random() - 0.5) * 6 + layer * 3;
const color = TYPE_COLORS[node.type];
const imp = node.importance;
const height = 2.5 + imp * 10;
const r2 = 1.2 + imp * 1.2;
const group = new THREE.Group();
group.userData = { nodeId: node.id };
group.position.set(x, y, z);
const geo = new THREE.ExtrudeGeometry(createHexShape(r2), {
depth: height, bevelEnabled: true, bevelThickness: 0.15, bevelSize: 0.1, bevelSegments: 1,
});
geo.center();
const mat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.1, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geo, mat);
mesh.rotation.x = -Math.PI / 2;
group.add(mesh);
const edgesGeo = new THREE.EdgesGeometry(geo);
const edgesMat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.7 });
const edges = new THREE.LineSegments(edgesGeo, edgesMat);
edges.rotation.x = -Math.PI / 2;
group.add(edges);
const glowGeo = new THREE.SphereGeometry(r2 * 0.5, 10, 10);
const glowMat = new THREE.MeshBasicMaterial({
color, transparent: true, opacity: 0.25 + imp * 0.4, blending: THREE.AdditiveBlending,
});
const glow = new THREE.Mesh(glowGeo, glowMat);
group.add(glow);
nodeMeshes.push(glow);
const topGeo = new THREE.CircleGeometry(r2 * 0.35, 6);
const topMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.6, side: THREE.DoubleSide });
const top = new THREE.Mesh(topGeo, topMat);
top.rotation.x = -Math.PI / 2;
top.position.y = height / 2;
group.add(top);
group.rotation.y = Math.random() * Math.PI;
scene.add(group);
nodeObjects.set(node.id, group);
});
}
function createEdges() {
EDGES.forEach(edge => {
const src = nodeObjects.get(edge.source);
const tgt = nodeObjects.get(edge.target);
if (!src || !tgt) return;
const srcNode = NODES.find(n => n.id === edge.source);
const color = TYPE_COLORS[srcNode?.type || 'knowledge'];
const points = [src.position.clone(), tgt.position.clone()];
const geo = new THREE.BufferGeometry().setFromPoints(points);
const mat = new THREE.LineDashedMaterial({
color, transparent: true, opacity: 0.35,
dashSize: 1.2, gapSize: 0.8, blending: THREE.AdditiveBlending,
});
const line = new THREE.Line(geo, mat);
line.computeLineDistances();
scene.add(line);
});
}
function createOrbitalRings() {
const ring1Geo = new THREE.BufferGeometry();
const ring1Pos = new Float32Array(60 * 3);
for (let i = 0; i < 60; i++) {
const t = (i / 60) * Math.PI * 2;
ring1Pos[i * 3] = Math.cos(t) * 40;
ring1Pos[i * 3 + 1] = (Math.random() - 0.5) * 0.4;
ring1Pos[i * 3 + 2] = Math.sin(t) * 40;
}
ring1Geo.setAttribute('position', new THREE.BufferAttribute(ring1Pos, 3));
const ring1 = new THREE.Points(ring1Geo, new THREE.PointsMaterial({ color: 0x00f3ff, size: 0.25, transparent: true, opacity: 0.35 }));
ring1.userData = { sx: 0.0008, sy: 0.0015 };
scene.add(ring1);
const ring2Geo = new THREE.BufferGeometry();
const ring2Pos = new Float32Array(50 * 3);
for (let i = 0; i < 50; i++) {
const t = (i / 50) * Math.PI * 2;
ring2Pos[i * 3] = (Math.random() - 0.5) * 0.4;
ring2Pos[i * 3 + 1] = Math.cos(t) * 45;
ring2Pos[i * 3 + 2] = Math.sin(t) * 45;
}
ring2Geo.setAttribute('position', new THREE.BufferAttribute(ring2Pos, 3));
const ring2 = new THREE.Points(ring2Geo, new THREE.PointsMaterial({ color: 0xa855f7, size: 0.2, transparent: true, opacity: 0.25 }));
ring2.userData = { sx: 0.0015, sy: 0.0008 };
scene.add(ring2);
}
function onMouseMove(e) {
mouseX = e.clientX - windowHalfX;
mouseY = e.clientY - windowHalfY;
targetRotY = mouseX * 0.0004;
targetRotX = -mouseY * 0.0004;
}
function onWheel(e) {
camera.position.z += e.deltaY * 0.04;
camera.position.z = Math.max(35, Math.min(180, camera.position.z));
}
function onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
const container = document.getElementById('brain-canvas');
container.style.width = w + 'px';
container.style.height = h + 'px';
}
function animate() {
requestAnimationFrame(animate);
const t = Date.now() * 0.001;
if (coreMesh) {
coreMesh.rotation.y += 0.001;
coreMesh.rotation.x -= 0.002;
}
if (innerMesh) {
innerMesh.rotation.y -= 0.002;
innerMesh.rotation.z += 0.001;
}
scene.rotation.y += (targetRotY - scene.rotation.y) * 0.03;
scene.rotation.x += (targetRotX - scene.rotation.x) * 0.03;
if (particleSystem) {
particleSystem.rotation.y += 0.00008;
particleSystem.rotation.x += 0.00004;
}
nodeMeshes.forEach((m, i) => {
const p = Math.sin(t * 1.8 + i * 0.4) * 0.12;
m.scale.setScalar(1 + p);
});
scene.children.forEach(c => {
if (c instanceof THREE.Points && c.userData.sx) {
c.rotation.x += c.userData.sx;
c.rotation.y += c.userData.sy;
}
});
renderer.render(scene, camera);
}
function updateNodeList() {
const list = document.getElementById('node-list');
list.innerHTML = '';
NODES.forEach(node => {
const color = '#' + TYPE_COLORS[node.type].toString(16).padStart(6, '0');
const div = document.createElement('div');
div.className = 'node-item flex items-center gap-2';
div.innerHTML = `
<div class="w-2 h-2 rounded-full flex-shrink-0" style="background: ${color}; box-shadow: 0 0 6px ${color};"></div>
<span class="text-cyan-300 truncate flex-grow">${node.name}</span>
<span class="opacity-50">${Math.round(node.importance * 100)}%</span>
`;
div.onclick = () => selectNode(node.id);
list.appendChild(div);
});
}
function selectNode(nodeId) {
selectedNodeId = nodeId;
const node = NODES.find(n => n.id === nodeId);
if (!node) return;
document.querySelectorAll('.node-item').forEach(el => el.classList.remove('selected'));
const items = document.querySelectorAll('.node-item');
const idx = NODES.findIndex(n => n.id === nodeId);
if (items[idx]) items[idx].classList.add('selected');
const color = '#' + TYPE_COLORS[node.type].toString(16).padStart(6, '0');
const detail = document.getElementById('detail-content');
const labels = { knowledge: '知识库', chat: '对话', forum: '论坛', schedule: '日程' };
detail.innerHTML = `
<div class="text-center mb-2">
<div class="text-[9px] opacity-50 uppercase tracking-widest" style="color: ${color}">${labels[node.type]}</div>
<div class="text-sm font-bold text-cyan-100 tracking-wider">${node.name}</div>
</div>
<div class="flex justify-between"><span class="opacity-60">重要性</span><span class="text-cyan-300">${Math.round(node.importance * 100)}%</span></div>
<div class="opacity-60">描述</div>
<div class="text-cyan-300/80 pl-2 border-l border-cyan-500/30">${node.description}</div>
<div class="mt-2">
<div class="opacity-60 mb-1">关联链路</div>
${EDGES.filter(e => e.source === nodeId || e.target === nodeId).map(e => {
const otherId = e.source === nodeId ? e.target : e.source;
const other = NODES.find(n => n.id === otherId);
return `<div class="text-[9px] text-cyan-500/70">${e.relation}${other?.name || ''}</div>`;
}).join('')}
</div>
`;
document.getElementById('node-detail').classList.remove('hidden');
addLog('>> Node selected: ' + node.name);
}
window.clearSelection = function() {
selectedNodeId = null;
document.querySelectorAll('.node-item').forEach(el => el.classList.remove('selected'));
document.getElementById('node-detail').classList.add('hidden');
};
function addLog(msg) {
const log = document.getElementById('terminal-log');
if (!log) return;
const div = document.createElement('div');
div.className = 'text-cyan-400/60';
div.textContent = '>> ' + msg;
log.appendChild(div);
log.scrollTop = log.scrollHeight;
while (log.children.length > 20) log.removeChild(log.firstChild);
}
window.sendMsg = function() {
const input = document.getElementById('chat-input');
const val = input.value.trim();
if (!val) return;
const messages = document.getElementById('chat-messages');
// User msg
const userDiv = document.createElement('div');
userDiv.className = 'flex justify-end msg-animate';
userDiv.innerHTML = `<div class="msg-bubble user">${escapeHtml(val)}</div>`;
messages.appendChild(userDiv);
input.value = '';
// Simulate Jarvis response
setTimeout(() => {
const respDiv = document.createElement('div');
respDiv.className = 'flex justify-start msg-animate';
respDiv.innerHTML = `<div class="msg-bubble">正在分析您的请求...</div>`;
messages.appendChild(respDiv);
messages.scrollTop = messages.scrollHeight;
setTimeout(() => {
respDiv.querySelector('.msg-bubble').innerHTML = `我已经分析了这个问题。基于当前知识图谱的相关节点:<br><br>
<span class="text-cyan-400">• Signal Bridge</span> 跨域桥接节点正在协调<br>
<span class="text-cyan-400">• Sprint Grid</span> 执行排期矩阵已就绪<br><br>
需要我展开某个节点的详细信息吗?`;
addLog('Response generated for: ' + val.substring(0, 30) + '...');
}, 1200);
}, 500);
messages.scrollTop = messages.scrollHeight;
addLog('User input: ' + val.substring(0, 40));
};
function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// Raycasting for node hover
document.addEventListener('mousemove', (e) => {
const canvas = renderer.domElement;
const rect = canvas.getBoundingClientRect();
if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) return;
const x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera({ x, y }, camera);
const hits = raycaster.intersectObjects(scene.children, true);
let found = null;
for (const hit of hits) {
let obj = hit.object;
while (obj) {
if (obj.userData?.nodeId) { found = obj.userData.nodeId; break; }
obj = obj.parent;
}
if (found) break;
}
hoveredNodeId = found;
canvas.style.cursor = found ? 'pointer' : 'grab';
});
document.addEventListener('click', (e) => {
const canvas = renderer.domElement;
const rect = canvas.getBoundingClientRect();
if (e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom) {
if (hoveredNodeId) selectNode(hoveredNodeId);
}
});
init();
})();
</script>
</body>
</html>