diff --git a/assets/js/main.js b/assets/js/main.js
index 207d762..71f0214 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -62,8 +62,8 @@ const labDecks = [
{
id: 'atlas-console',
type: 'lab',
- external: true,
- url: 'https://atlas.earthonline.systems',
+ external: false,
+ url: 'public/lab/atlas-console/index.html',
translations: {
en: {
title: 'Atlas Console',
@@ -86,8 +86,8 @@ const labDecks = [
{
id: 'helium-simulator',
type: 'lab',
- external: true,
- url: 'https://helium.earthonline.systems',
+ external: false,
+ url: 'public/lab/helium-simulator/index.html',
translations: {
en: {
title: 'Helium Simulator',
@@ -110,8 +110,8 @@ const labDecks = [
{
id: 'council-studio',
type: 'lab',
- external: true,
- url: 'https://council.earthonline.systems',
+ external: false,
+ url: 'public/lab/council-studio/index.html',
translations: {
en: {
title: 'Council Studio',
diff --git a/public/lab/atlas-console/index.html b/public/lab/atlas-console/index.html
new file mode 100644
index 0000000..04f106d
--- /dev/null
+++ b/public/lab/atlas-console/index.html
@@ -0,0 +1,102 @@
+
+
+
+
+
+ Atlas Console · Experience Systems Atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Research track
+ Operational ritual
+ Asset library
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Constellation design
+
+ 维护星图拓扑和语义层,支持跨学科研究索引与路径推演。
+
+
+ Systems research
+ Simulation
+ Narratives
+
+
+ Active briefs
+ 6
+
+
+ Research ingest (24h)
+ 18
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/lab/atlas-console/main.js b/public/lab/atlas-console/main.js
new file mode 100644
index 0000000..0a05578
--- /dev/null
+++ b/public/lab/atlas-console/main.js
@@ -0,0 +1,279 @@
+const tracks = {
+ 'constellation-design': {
+ title: 'Constellation design',
+ description:
+ '维护星图拓扑、语义链接与推演路径,生成可复制的跨学科作战地图。',
+ tags: ['Systems research', 'Simulation', 'Narratives'],
+ briefs: 6,
+ ingest: 18,
+ cluster: 'Atlas Core'
+ },
+ 'experience-engineering': {
+ title: 'Experience engineering',
+ description:
+ '原型化空间界面、遥测面板与 AI 协作体,将实验快速升舱到生产环境。',
+ tags: ['Spatial UI', 'AI toolkit', 'Telemetry'],
+ briefs: 4,
+ ingest: 12,
+ cluster: 'Experience Stack'
+ },
+ 'alliance-operations': {
+ title: 'Alliance operations',
+ description:
+ '编排合作仪式、激励桥梁与自治节点对齐例会,为联盟提供节奏。',
+ tags: ['Partner programs', 'Incentive design', 'Operations'],
+ briefs: 5,
+ ingest: 9,
+ cluster: 'Alliance Orbit'
+ }
+};
+
+const constellationNodes = [
+ {
+ id: 'atlas-core',
+ label: 'Atlas Core',
+ type: 'track',
+ x: 0.2,
+ y: 0.45,
+ orbit: 0
+ },
+ { id: 'experience-stack', label: 'Experience Stack', type: 'track', x: 0.52, y: 0.25, orbit: 0 },
+ { id: 'alliance-orbit', label: 'Alliance Orbit', type: 'track', x: 0.76, y: 0.52, orbit: 0 },
+ {
+ id: 'ritual-summit',
+ label: 'Governance summit',
+ type: 'ritual',
+ x: 0.36,
+ y: 0.72,
+ orbit: 1
+ },
+ { id: 'launch-console', label: 'Launch console', type: 'asset', x: 0.6, y: 0.65, orbit: 1 },
+ { id: 'knowledge-graph', label: 'Knowledge graph', type: 'asset', x: 0.45, y: 0.38, orbit: 1 },
+ { id: 'pilot-ops', label: 'Pilot ops', type: 'ritual', x: 0.72, y: 0.32, orbit: 1 }
+];
+
+const links = [
+ ['atlas-core', 'experience-stack'],
+ ['atlas-core', 'knowledge-graph'],
+ ['experience-stack', 'launch-console'],
+ ['experience-stack', 'pilot-ops'],
+ ['alliance-orbit', 'pilot-ops'],
+ ['atlas-core', 'ritual-summit'],
+ ['ritual-summit', 'alliance-orbit']
+];
+
+const timelineEvents = [
+ {
+ id: 'atlas-sprint',
+ time: 'Mon 09:00',
+ title: 'Atlas weekly sprint',
+ description: '对齐研究吸收、星图修补与下一批原型发布的节奏。'
+ },
+ {
+ id: 'ritual-design',
+ time: 'Tue 15:30',
+ title: 'Ritual design studio',
+ description: '联合设计伙伴固化治理仪式与指标,生成执行脚本。'
+ },
+ {
+ id: 'launch-review',
+ time: 'Thu 11:00',
+ title: 'Launch review loop',
+ description: '审视体验遥测、风险与补强项,并更新资产库版本。'
+ },
+ {
+ id: 'council-sync',
+ time: 'Fri 17:00',
+ title: 'Council sync',
+ description: '记录联盟决策、共享 scorecard,并生成多语种回顾。'
+ }
+];
+
+const assetInventory = [
+ {
+ id: 'atlas-map',
+ title: 'Atlas navigation map',
+ summary: '全局语义地图,覆盖 182 个研究节点和 54 条推演路径。',
+ status: 'v3.1 · Fresh build'
+ },
+ {
+ id: 'experience-kit',
+ title: 'Experience prototyping kit',
+ summary: '空间界面组件库与 Telemetry SDK,用于快速拼装体验。',
+ status: 'v2.4 · Stable'
+ },
+ {
+ id: 'governance-playbook',
+ title: 'Governance playbook',
+ summary: '仪式模板、角色脚本与复盘提问清单。',
+ status: 'v1.9 · Live updates'
+ }
+];
+
+const trackList = document.getElementById('trackList');
+const trackTitle = document.getElementById('trackTitle');
+const trackDescription = document.getElementById('trackDescription');
+const trackTags = document.getElementById('trackTags');
+const trackBriefs = document.getElementById('trackBriefs');
+const trackIngest = document.getElementById('trackIngest');
+const activeCluster = document.getElementById('activeCluster');
+
+trackList?.addEventListener('click', (event) => {
+ const target = event.target.closest('button[data-track]');
+ if (!target) return;
+
+ const trackId = target.dataset.track;
+ const data = tracks[trackId];
+ if (!data) return;
+
+ trackList.querySelectorAll('button').forEach((button) => {
+ button.setAttribute('aria-pressed', button === target ? 'true' : 'false');
+ });
+
+ trackTitle.textContent = data.title;
+ trackDescription.textContent = data.description;
+ trackTags.innerHTML = data.tags.map((tag) => `${tag}`).join('');
+ trackBriefs.textContent = data.briefs;
+ trackIngest.textContent = data.ingest;
+ activeCluster.textContent = data.cluster;
+ drawConstellation(trackId);
+});
+
+const timelineContainer = document.getElementById('timeline');
+if (timelineContainer) {
+ timelineContainer.innerHTML = timelineEvents
+ .map(
+ (event) => `
+
+
+ ${event.title}
+ ${event.description}
+
+ `
+ )
+ .join('');
+}
+
+const assetList = document.getElementById('assetList');
+if (assetList) {
+ assetList.innerHTML = assetInventory
+ .map(
+ (asset) => `
+
+ ${asset.title}
+ ${asset.summary}
+ ${asset.status}
+
+ `
+ )
+ .join('');
+}
+
+const canvas = document.getElementById('constellationCanvas');
+const ctx = canvas?.getContext('2d');
+let activeTrack = 'constellation-design';
+
+function resizeCanvas() {
+ if (!canvas) return;
+ const rect = canvas.parentElement?.getBoundingClientRect();
+ if (!rect) return;
+ const ratio = window.devicePixelRatio || 1;
+ canvas.width = rect.width * ratio;
+ canvas.height = rect.height * ratio;
+ canvas.style.width = `${rect.width}px`;
+ canvas.style.height = `${rect.height}px`;
+ if (ctx) {
+ ctx.scale(ratio, ratio);
+ }
+ drawConstellation(activeTrack, true);
+}
+
+let scaled = false;
+
+function drawConstellation(trackId, force = false) {
+ if (!canvas || !ctx) return;
+ if (force) {
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ const ratio = window.devicePixelRatio || 1;
+ ctx.scale(ratio, ratio);
+ }
+
+ const rect = canvas.getBoundingClientRect();
+ ctx.clearRect(0, 0, rect.width, rect.height);
+
+ const nodeRadius = 10;
+ const centerX = rect.width / 2;
+ const centerY = rect.height / 2;
+ const radius = Math.min(rect.width, rect.height) * 0.42;
+
+ ctx.lineWidth = 1.4;
+ ctx.strokeStyle = 'rgba(145, 169, 255, 0.4)';
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, radius * 0.75, 0, Math.PI * 2);
+ ctx.stroke();
+
+ links.forEach(([fromId, toId]) => {
+ const from = constellationNodes.find((node) => node.id === fromId);
+ const to = constellationNodes.find((node) => node.id === toId);
+ if (!from || !to) return;
+ const fromPoint = positionForNode(from, rect, radius, centerX, centerY);
+ const toPoint = positionForNode(to, rect, radius, centerX, centerY);
+ ctx.strokeStyle = 'rgba(145, 169, 255, 0.28)';
+ ctx.lineWidth = 1.2;
+ ctx.beginPath();
+ ctx.moveTo(fromPoint.x, fromPoint.y);
+ ctx.lineTo(toPoint.x, toPoint.y);
+ ctx.stroke();
+ });
+
+ constellationNodes.forEach((node) => {
+ const { x, y } = positionForNode(node, rect, radius, centerX, centerY);
+ const isActive =
+ (trackId === 'constellation-design' && node.id === 'atlas-core') ||
+ (trackId === 'experience-engineering' && node.id === 'experience-stack') ||
+ (trackId === 'alliance-operations' && node.id === 'alliance-orbit');
+
+ ctx.beginPath();
+ if (node.type === 'track') {
+ ctx.fillStyle = isActive ? 'rgba(77, 106, 255, 0.9)' : 'rgba(145, 169, 255, 0.6)';
+ ctx.strokeStyle = 'rgba(145, 169, 255, 0.5)';
+ ctx.lineWidth = 2;
+ ctx.arc(x, y, nodeRadius + 4, 0, Math.PI * 2);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.arc(x, y, nodeRadius + 1, 0, Math.PI * 2);
+ ctx.fill();
+ } else if (node.type === 'ritual') {
+ ctx.fillStyle = 'rgba(76, 224, 179, 0.8)';
+ ctx.beginPath();
+ ctx.arc(x, y, nodeRadius, 0, Math.PI * 2);
+ ctx.fill();
+ } else {
+ ctx.fillStyle = 'rgba(255, 212, 111, 0.8)';
+ ctx.beginPath();
+ ctx.rect(x - nodeRadius, y - nodeRadius, nodeRadius * 2, nodeRadius * 2);
+ ctx.fill();
+ }
+
+ ctx.font = '13px Inter, system-ui';
+ ctx.fillStyle = 'rgba(242, 245, 255, 0.85)';
+ ctx.textAlign = 'center';
+ ctx.fillText(node.label, x, y + nodeRadius + 16);
+ });
+
+ activeTrack = trackId;
+}
+
+function positionForNode(node, rect, radius, cx, cy) {
+ const baseAngle = node.orbit === 0 ? 0 : Math.PI / 4;
+ const angle = baseAngle + node.x * Math.PI * 1.6;
+ const orbitRadius = node.orbit === 0 ? radius * 0.5 : radius * (0.5 + node.y * 0.4);
+ return {
+ x: cx + Math.cos(angle) * orbitRadius,
+ y: cy + Math.sin(angle) * orbitRadius
+ };
+}
+
+window.addEventListener('resize', () => resizeCanvas());
+resizeCanvas();
+drawConstellation(activeTrack);
diff --git a/public/lab/council-studio/index.html b/public/lab/council-studio/index.html
new file mode 100644
index 0000000..3473fe2
--- /dev/null
+++ b/public/lab/council-studio/index.html
@@ -0,0 +1,93 @@
+
+
+
+
+
+ Council Studio · Experience Systems Atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Export snapshot
+
+
+
+
+
+
+
+
+
diff --git a/public/lab/council-studio/main.js b/public/lab/council-studio/main.js
new file mode 100644
index 0000000..afbfe7b
--- /dev/null
+++ b/public/lab/council-studio/main.js
@@ -0,0 +1,197 @@
+const agendaForm = document.getElementById('agendaForm');
+const agendaTimeline = document.getElementById('agendaTimeline');
+const decisionLog = document.getElementById('decisionLog');
+const exportButton = document.getElementById('exportButton');
+const exportPreview = document.getElementById('exportPreview');
+const insightList = document.getElementById('insightList');
+
+const alignmentSlider = document.getElementById('alignmentSlider');
+const incentiveSlider = document.getElementById('incentiveSlider');
+const readinessSlider = document.getElementById('readinessSlider');
+const alignmentScore = document.getElementById('alignmentScore');
+const incentiveScore = document.getElementById('incentiveScore');
+const readinessScore = document.getElementById('readinessScore');
+
+const state = {
+ agenda: [],
+ metrics: {
+ alignment: parseInt(alignmentSlider?.value ?? '82', 10),
+ incentive: parseInt(incentiveSlider?.value ?? '76', 10),
+ readiness: parseInt(readinessSlider?.value ?? '64', 10)
+ },
+ decisions: [
+ {
+ title: 'Adopt Atlas 2025 rituals',
+ owner: 'Miu',
+ outcome: 'Approved with amendments to include partner intake.',
+ timestamp: timestampLabel()
+ },
+ {
+ title: 'Launch Helium beta cohort',
+ owner: 'Sora',
+ outcome: 'Scheduled with telemetry guardrails and council review checkpoints.',
+ timestamp: timestampLabel(-17)
+ }
+ ]
+};
+
+function timestampLabel(offsetMinutes = 0) {
+ const date = new Date(Date.now() + offsetMinutes * 60 * 1000);
+ return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
+}
+
+function renderAgenda() {
+ if (!agendaTimeline) return;
+ if (state.agenda.length === 0) {
+ agendaTimeline.innerHTML = `尚未添加仪式议程。使用左侧表单创建一条。
`;
+ return;
+ }
+
+ let cursor = 0;
+ agendaTimeline.innerHTML = state.agenda
+ .map((item, index) => {
+ const start = minutesToLabel(cursor);
+ cursor += item.duration;
+ const end = minutesToLabel(cursor);
+ return `
+
+
+ ${item.topic}
+ ${item.owner} · ${item.duration} 分钟
+
+ `;
+ })
+ .join('');
+}
+
+function minutesToLabel(totalMinutes) {
+ const baseHour = 10;
+ const hours = Math.floor((baseHour * 60 + totalMinutes) / 60);
+ const minutes = (baseHour * 60 + totalMinutes) % 60;
+ return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
+}
+
+function renderDecisions() {
+ if (!decisionLog) return;
+ decisionLog.innerHTML = state.decisions
+ .map(
+ (entry) => `
+
+ ${entry.title}
+ ${entry.timestamp} · ${entry.owner}
+ ${entry.outcome}
+
+ `
+ )
+ .join('');
+}
+
+function renderInsights() {
+ if (!insightList) return;
+ const { alignment, incentive, readiness } = state.metrics;
+ const insights = [];
+
+ if (alignment >= 85) {
+ insights.push('联盟对齐度高,适合引入新的跨节点实验。');
+ } else if (alignment <= 65) {
+ insights.push('建议安排额外的回顾仪式以补强对齐度。');
+ }
+
+ if (incentive >= 80) {
+ insights.push('激励循环健康,可扩展共建预算。');
+ } else if (incentive < 60) {
+ insights.push('重新审视激励分配,避免伙伴贡献被稀释。');
+ }
+
+ if (readiness >= 70) {
+ insights.push('执行团队已就绪,能够承担下一轮发布节奏。');
+ } else {
+ insights.push('在启动重大任务前,需补齐执行资源或培训。');
+ }
+
+ insightList.innerHTML = insights
+ .map((text) => `${text}
`)
+ .join('');
+}
+
+function attachFormHandlers() {
+ agendaForm?.addEventListener('submit', (event) => {
+ event.preventDefault();
+ const topic = document.getElementById('agendaTopic')?.value.trim();
+ const owner = document.getElementById('agendaOwner')?.value.trim();
+ const duration = parseInt(document.getElementById('agendaDuration')?.value ?? '0', 10);
+ if (!topic || !owner || !duration) return;
+
+ state.agenda.push({ topic, owner, duration });
+ state.decisions.unshift({
+ title: `Agenda added: ${topic}`,
+ owner,
+ outcome: `Allocated ${duration} 分钟给 ${owner}。`,
+ timestamp: timestampLabel()
+ });
+ if (state.decisions.length > 8) state.decisions.length = 8;
+
+ renderAgenda();
+ renderDecisions();
+ exportPreview.textContent = '';
+ agendaForm.reset();
+ document.getElementById('agendaDuration').value = '25';
+ });
+}
+
+function attachMetricHandlers() {
+ const bindings = [
+ { slider: alignmentSlider, label: alignmentScore, key: 'alignment' },
+ { slider: incentiveSlider, label: incentiveScore, key: 'incentive' },
+ { slider: readinessSlider, label: readinessScore, key: 'readiness' }
+ ];
+
+ bindings.forEach(({ slider, label, key }) => {
+ slider?.addEventListener('input', (event) => {
+ const value = parseInt(event.target.value, 10);
+ state.metrics[key] = value;
+ if (label) {
+ label.textContent = `${value}%`;
+ }
+ state.decisions.unshift({
+ title: `${key.charAt(0).toUpperCase() + key.slice(1)} score updated`,
+ owner: 'Council Studio',
+ outcome: `新值:${value}%`,
+ timestamp: timestampLabel()
+ });
+ if (state.decisions.length > 8) state.decisions.length = 8;
+ renderInsights();
+ renderDecisions();
+ });
+ });
+}
+
+function attachExportHandler() {
+ exportButton?.addEventListener('click', () => {
+ const snapshot = {
+ generatedAt: new Date().toISOString(),
+ agenda: state.agenda,
+ metrics: state.metrics,
+ decisions: state.decisions
+ };
+ const json = JSON.stringify(snapshot, null, 2);
+ exportPreview.textContent = json.slice(0, 260) + (json.length > 260 ? '\n…' : '');
+
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `council-studio-${Date.now()}.json`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ });
+}
+
+renderAgenda();
+renderDecisions();
+renderInsights();
+attachFormHandlers();
+attachMetricHandlers();
+attachExportHandler();
diff --git a/public/lab/helium-simulator/index.html b/public/lab/helium-simulator/index.html
new file mode 100644
index 0000000..9875815
--- /dev/null
+++ b/public/lab/helium-simulator/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+ Helium Simulator · Experience Systems Atlas
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Asset |
+ Altitude |
+ Velocity |
+ Status |
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/lab/helium-simulator/main.js b/public/lab/helium-simulator/main.js
new file mode 100644
index 0000000..c0bdfba
--- /dev/null
+++ b/public/lab/helium-simulator/main.js
@@ -0,0 +1,244 @@
+const canvas = document.getElementById('simCanvas');
+const ctx = canvas?.getContext('2d');
+const altitudeInput = document.getElementById('altitude');
+const smoothingInput = document.getElementById('smoothing');
+const copilotToggle = document.getElementById('copilotToggle');
+const anomalyToggle = document.getElementById('anomalyToggle');
+const altitudeValue = document.getElementById('altitudeValue');
+const smoothingValue = document.getElementById('smoothingValue');
+const telemetryTable = document.getElementById('telemetryTable');
+const eventFeed = document.getElementById('eventFeed');
+const copilotInstruction = document.getElementById('copilotInstruction');
+const copilotTags = document.getElementById('copilotTags');
+const telemetryTicker = document.getElementById('telemetryTicker');
+
+const assets = [
+ {
+ id: 'helium-01',
+ label: 'Helium-01',
+ color: '#91a9ff',
+ orbitRadius: 0.32,
+ phase: 0.12,
+ health: 0.96
+ },
+ {
+ id: 'helium-02',
+ label: 'Helium-02',
+ color: '#4ce0b3',
+ orbitRadius: 0.45,
+ phase: 0.55,
+ health: 0.9
+ },
+ {
+ id: 'relay-arc',
+ label: 'Relay-ARC',
+ color: '#ffd46f',
+ orbitRadius: 0.56,
+ phase: 0.28,
+ health: 0.82
+ }
+];
+
+let lastTimestamp = 0;
+let smoothingGain = parseFloat(smoothingInput?.value ?? '0.35');
+let targetAltitude = parseInt(altitudeInput?.value ?? '280', 10);
+let tickAccumulator = 0;
+let copilotEnabled = copilotToggle?.checked ?? true;
+let anomalyWatch = anomalyToggle?.checked ?? false;
+
+const events = [];
+const copilotScripts = {
+ idle: {
+ text: '维持同步轨道,准备重建遥测投影。',
+ tags: ['Attitude control', 'Telemetry refresh']
+ },
+ adjust: {
+ text: '执行逆推校正,确保航迹与地面站窗口重叠。',
+ tags: ['Thruster burn', 'Window alignment']
+ },
+ anomaly: {
+ text: '触发异常追踪,收敛姿态漂移并通知联盟节点。',
+ tags: ['Anomaly response', 'Council alert']
+ }
+};
+
+function resizeCanvas() {
+ if (!canvas) return;
+ const parent = canvas.parentElement;
+ if (!parent) return;
+ const rect = parent.getBoundingClientRect();
+ const ratio = window.devicePixelRatio || 1;
+ canvas.width = rect.width * ratio;
+ canvas.height = rect.height * ratio;
+ canvas.style.width = `${rect.width}px`;
+ canvas.style.height = `${rect.height}px`;
+ if (ctx) {
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
+ ctx.scale(ratio, ratio);
+ }
+}
+
+function updateTelemetry(dt) {
+ tickAccumulator += dt;
+ if (tickAccumulator < 300) return;
+ tickAccumulator = 0;
+ assets.forEach((asset, index) => {
+ const variance = (Math.random() - 0.5) * 0.8;
+ asset.altitude = targetAltitude + (index * 22 + variance) * (1 - smoothingGain);
+ asset.velocity = 7.3 + (Math.random() - 0.5) * 0.2;
+ asset.health += (Math.random() - 0.5) * 0.01;
+ asset.health = Math.min(1, Math.max(0.68, asset.health));
+ });
+ renderTelemetry();
+ telemetryTicker.textContent = `${(300 / 1000).toFixed(1)}s`;
+ emitEvent('Telemetry refresh', 'Updated orbit solutions and power draw.');
+}
+
+function renderTelemetry() {
+ if (!telemetryTable) return;
+ telemetryTable.innerHTML = assets
+ .map((asset) => {
+ const status = asset.health > 0.9 ? 'Nominal' : asset.health > 0.8 ? 'Guarded' : 'Watch';
+ const statusColor =
+ status === 'Nominal' ? 'var(--success)' : status === 'Guarded' ? 'var(--warning)' : 'var(--danger)';
+ return `
+
+ | ${asset.label} |
+ ${Math.round(asset.altitude ?? targetAltitude)} km |
+ ${(asset.velocity ?? 7.3).toFixed(2)} km/s |
+ ${status} |
+
+ `;
+ })
+ .join('');
+}
+
+function emitEvent(title, summary) {
+ events.unshift({ title, summary, time: new Date() });
+ if (events.length > 6) events.length = 6;
+ if (!eventFeed) return;
+ eventFeed.innerHTML = events
+ .map((event) => {
+ const time = event.time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
+ return `${event.title}${summaryWithTime(
+ event.summary,
+ time
+ )}
`;
+ })
+ .join('');
+}
+
+function summaryWithTime(summary, time) {
+ return `${time} · ${summary}`;
+}
+
+function draw(timestamp) {
+ if (!canvas || !ctx) return;
+ if (!lastTimestamp) lastTimestamp = timestamp;
+ const dt = timestamp - lastTimestamp;
+ lastTimestamp = timestamp;
+ updateTelemetry(dt);
+
+ const width = canvas.getBoundingClientRect().width;
+ const height = canvas.getBoundingClientRect().height;
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = 'rgba(5, 8, 14, 0.88)';
+ ctx.fillRect(0, 0, width, height);
+
+ const centerX = width / 2;
+ const centerY = height / 2;
+ const baseRadius = Math.min(width, height) * 0.38;
+
+ ctx.strokeStyle = 'rgba(145, 169, 255, 0.15)';
+ ctx.lineWidth = 1;
+ for (let i = 0; i < 3; i++) {
+ ctx.beginPath();
+ ctx.arc(centerX, centerY, baseRadius * (0.6 + i * 0.18), 0, Math.PI * 2);
+ ctx.stroke();
+ }
+
+ assets.forEach((asset, index) => {
+ const orbitRadius = baseRadius * (asset.orbitRadius + (targetAltitude - 280) / 600);
+ const speed = 0.0003 + index * 0.00012;
+ const angle = timestamp * speed + asset.phase * Math.PI * 2;
+ const x = centerX + Math.cos(angle) * orbitRadius;
+ const y = centerY + Math.sin(angle) * orbitRadius * 0.78;
+
+ ctx.beginPath();
+ ctx.strokeStyle = `${asset.color}33`;
+ ctx.lineWidth = 2;
+ ctx.ellipse(centerX, centerY, orbitRadius, orbitRadius * 0.78, 0, 0, Math.PI * 2);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.fillStyle = asset.color;
+ ctx.shadowBlur = 12;
+ ctx.shadowColor = `${asset.color}aa`;
+ ctx.arc(x, y, 6, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+
+ ctx.fillStyle = 'rgba(242, 245, 255, 0.9)';
+ ctx.font = '13px Inter, system-ui';
+ ctx.fillText(asset.label, x + 10, y - 10);
+ });
+
+ if (copilotEnabled) {
+ const pulse = (Math.sin(timestamp / 300) + 1) / 2;
+ ctx.beginPath();
+ ctx.strokeStyle = `rgba(77, 106, 255, ${0.2 + pulse * 0.3})`;
+ ctx.lineWidth = 2;
+ ctx.arc(centerX, centerY, baseRadius * 0.45 + pulse * 8, 0, Math.PI * 2);
+ ctx.stroke();
+ }
+
+ window.requestAnimationFrame(draw);
+}
+
+function setCopilotScript() {
+ if (!copilotInstruction || !copilotTags) return;
+ let script = copilotScripts.idle;
+ if (!copilotEnabled) {
+ script = {
+ text: '手动模式开启,等待操作员指令。',
+ tags: ['Manual control']
+ };
+ } else if (anomalyWatch) {
+ script = copilotScripts.anomaly;
+ } else if (Math.abs(targetAltitude - 280) > 40) {
+ script = copilotScripts.adjust;
+ }
+
+ copilotInstruction.textContent = script.text;
+ copilotTags.innerHTML = script.tags.map((tag) => `${tag}`).join('');
+}
+
+altitudeInput?.addEventListener('input', (event) => {
+ const value = parseInt(event.target.value, 10);
+ targetAltitude = value;
+ altitudeValue.textContent = `${value} km`;
+ setCopilotScript();
+});
+
+smoothingInput?.addEventListener('input', (event) => {
+ smoothingGain = parseFloat(event.target.value);
+ smoothingValue.textContent = smoothingGain.toFixed(2);
+});
+
+copilotToggle?.addEventListener('change', (event) => {
+ copilotEnabled = event.target.checked;
+ setCopilotScript();
+ emitEvent('Copilot toggled', copilotEnabled ? 'AI copilots synced to mission timeline.' : 'Manual override accepted.');
+});
+
+anomalyToggle?.addEventListener('change', (event) => {
+ anomalyWatch = event.target.checked;
+ setCopilotScript();
+ emitEvent(anomalyWatch ? 'Anomaly watch' : 'Nominal ops', anomalyWatch ? 'Scanning clusters for drift anomalies.' : 'Returning to nominal operations.');
+});
+
+resizeCanvas();
+renderTelemetry();
+setCopilotScript();
+window.addEventListener('resize', resizeCanvas);
+window.requestAnimationFrame(draw);
diff --git a/public/lab/styles.css b/public/lab/styles.css
new file mode 100644
index 0000000..1fbf25f
--- /dev/null
+++ b/public/lab/styles.css
@@ -0,0 +1,339 @@
+:root {
+ color-scheme: dark;
+ --bg: #090b11;
+ --bg-elevated: rgba(14, 18, 27, 0.92);
+ --panel-border: rgba(88, 116, 255, 0.18);
+ --accent: #91a9ff;
+ --accent-strong: #4d6aff;
+ --accent-soft: rgba(145, 169, 255, 0.2);
+ --text-primary: #f2f5ff;
+ --text-secondary: #b0b8d9;
+ --text-faint: rgba(176, 184, 217, 0.64);
+ --success: #4ce0b3;
+ --warning: #ffd46f;
+ --danger: #ff6f91;
+ font-size: 16px;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ font-family: 'Inter', 'SF Pro Display', 'Segoe UI', system-ui, -apple-system, sans-serif;
+ background: radial-gradient(circle at 20% 20%, rgba(77, 106, 255, 0.08), transparent 55%),
+ radial-gradient(circle at 80% 10%, rgba(76, 224, 179, 0.08), transparent 60%),
+ var(--bg);
+ color: var(--text-primary);
+ display: flex;
+ flex-direction: column;
+}
+
+.page-header {
+ padding: 3rem clamp(1.5rem, 4vw, 3rem) 2rem;
+ display: grid;
+ gap: 1rem;
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.page-header h1 {
+ font-size: clamp(2.4rem, 4vw, 3.2rem);
+ letter-spacing: -0.02em;
+ margin: 0;
+}
+
+.page-header p {
+ margin: 0;
+ font-size: clamp(1.05rem, 2vw, 1.2rem);
+ color: var(--text-secondary);
+ max-width: 70ch;
+}
+
+main.layout {
+ flex: 1;
+ width: min(1200px, 92vw);
+ margin: 0 auto 4rem;
+ display: grid;
+ gap: 1.5rem;
+}
+
+.atlas-grid {
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ align-items: start;
+}
+
+.simulator-grid {
+ grid-template-columns: minmax(260px, 320px) 1fr minmax(240px, 320px);
+ gap: 1.25rem;
+}
+
+.studio-grid {
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+}
+
+.panel {
+ background: var(--bg-elevated);
+ border: 1px solid var(--panel-border);
+ border-radius: 18px;
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ backdrop-filter: blur(12px);
+}
+
+.panel.compact {
+ gap: 0.75rem;
+ padding: 1.25rem;
+}
+
+.panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.panel-header h2 {
+ margin: 0;
+ font-size: 1.25rem;
+ letter-spacing: -0.01em;
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ border-radius: 999px;
+ padding: 0.3rem 0.75rem;
+ font-size: 0.85rem;
+ background: var(--accent-soft);
+ color: var(--accent);
+}
+
+.badge strong {
+ color: var(--text-primary);
+}
+
+.metric {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+}
+
+.metric strong {
+ font-size: 1.4rem;
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.list {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.list button,
+.list .list-item {
+ border: 1px solid rgba(145, 169, 255, 0.18);
+ background: rgba(10, 13, 23, 0.55);
+ color: var(--text-primary);
+ padding: 0.85rem 1rem;
+ border-radius: 14px;
+ text-align: left;
+ font-size: 0.95rem;
+ display: grid;
+ gap: 0.35rem;
+ cursor: pointer;
+ transition: border-color 120ms ease, transform 150ms ease;
+}
+
+.list button:hover,
+.list button:focus-visible {
+ border-color: var(--accent);
+ transform: translateY(-2px);
+}
+
+.list button[aria-pressed='true'] {
+ border-color: var(--accent-strong);
+ background: rgba(77, 106, 255, 0.18);
+}
+
+.list .tagline {
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+}
+
+.detail-card {
+ border-radius: 16px;
+ border: 1px solid rgba(77, 106, 255, 0.25);
+ background: rgba(14, 18, 27, 0.7);
+ padding: 1.2rem;
+ display: grid;
+ gap: 0.6rem;
+}
+
+.detail-card h3 {
+ margin: 0;
+ font-size: 1.1rem;
+}
+
+.token-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.token {
+ padding: 0.25rem 0.7rem;
+ border-radius: 999px;
+ background: rgba(145, 169, 255, 0.14);
+ color: var(--accent);
+ font-size: 0.8rem;
+}
+
+.canvas-shell {
+ position: relative;
+ border-radius: 16px;
+ overflow: hidden;
+ border: 1px solid rgba(77, 106, 255, 0.2);
+ background: radial-gradient(circle at 30% 20%, rgba(77, 106, 255, 0.1), transparent 65%),
+ rgba(5, 8, 14, 0.88);
+ min-height: 320px;
+}
+
+.canvas-shell canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+}
+
+.legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+
+.legend span {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.legend span::before {
+ content: '';
+ width: 10px;
+ height: 10px;
+ border-radius: 999px;
+ background: currentColor;
+ opacity: 0.7;
+}
+
+input[type='range'] {
+ width: 100%;
+ accent-color: var(--accent-strong);
+}
+
+fieldset {
+ border: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 0.6rem;
+}
+
+fieldset legend {
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+}
+
+.toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.9rem;
+}
+
+.toggle input {
+ width: 2.4rem;
+ height: 1.3rem;
+}
+
+.table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.85rem;
+}
+
+.table th,
+.table td {
+ text-align: left;
+ padding: 0.4rem 0;
+ border-bottom: 1px solid rgba(145, 169, 255, 0.12);
+}
+
+.chip-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+}
+
+.chip {
+ padding: 0.35rem 0.7rem;
+ border-radius: 10px;
+ background: rgba(145, 169, 255, 0.16);
+ color: var(--accent);
+ font-size: 0.8rem;
+}
+
+.timeline {
+ border-left: 2px solid rgba(145, 169, 255, 0.2);
+ padding-left: 1rem;
+ display: grid;
+ gap: 1rem;
+}
+
+.timeline-item {
+ display: grid;
+ gap: 0.25rem;
+ position: relative;
+}
+
+.timeline-item::before {
+ content: '';
+ width: 11px;
+ height: 11px;
+ border-radius: 999px;
+ background: var(--accent-strong);
+ position: absolute;
+ left: -1.55rem;
+ top: 0.2rem;
+}
+
+.timeline-item time {
+ font-size: 0.75rem;
+ color: var(--text-faint);
+}
+
+@media (max-width: 960px) {
+ .simulator-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 720px) {
+ .page-header {
+ padding: 2.5rem 1.25rem 1.75rem;
+ }
+ main.layout {
+ width: 92vw;
+ margin-bottom: 3rem;
+ }
+}