From 52a7596f96eae48e5fd713850ecf1bd7e0262f32 Mon Sep 17 00:00:00 2001 From: Amamiya Miu Date: Wed, 22 Oct 2025 13:42:36 +0800 Subject: [PATCH] Add interactive Atlas lab experiences --- assets/js/main.js | 12 +- public/lab/atlas-console/index.html | 102 ++++++++ public/lab/atlas-console/main.js | 279 ++++++++++++++++++++ public/lab/council-studio/index.html | 93 +++++++ public/lab/council-studio/main.js | 197 ++++++++++++++ public/lab/helium-simulator/index.html | 89 +++++++ public/lab/helium-simulator/main.js | 244 ++++++++++++++++++ public/lab/styles.css | 339 +++++++++++++++++++++++++ 8 files changed, 1349 insertions(+), 6 deletions(-) create mode 100644 public/lab/atlas-console/index.html create mode 100644 public/lab/atlas-console/main.js create mode 100644 public/lab/council-studio/index.html create mode 100644 public/lab/council-studio/main.js create mode 100644 public/lab/helium-simulator/index.html create mode 100644 public/lab/helium-simulator/main.js create mode 100644 public/lab/styles.css 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 + + + + + + + + +
+
+
+

Constellation graph

+ Atlas Core +
+ +
+ Research track + Operational ritual + Asset library +
+
+ +
+
+

Mission tracks

+ 3 active +
+
+ + + +
+
+ +
+
+

Track intelligence

+
+
+

Constellation design

+

+ 维护星图拓扑和语义层,支持跨学科研究索引与路径推演。 +

+
+ Systems research + Simulation + Narratives +
+
+ Active briefs + 6 +
+
+ Research ingest (24h) + 18 +
+
+
+ +
+
+

Ritual timeline

+
+
+
+ +
+
+

Asset inventory

+
+
+
+
+ + + + 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 + + + + + + + + +
+
+
+

Ritual builder

+ Interactive +
+
+ + + + +
+
+

Agenda timeline

+
+
+
+ +
+
+

Scorecard

+
+
+ Alignment confidence + 82% +
+ +
+ Incentive health + 76% +
+ +
+ Execution readiness + 64% +
+ +
+

Insights

+
+
+
+ +
+
+

Decision log

+ Auto generated +
+
+
+

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 + + + + + + + + +
+
+
+

Control stack

+ Live sandbox +
+
+ 轨道高度 + +
Target altitude280 km
+
+
+ 遥测平滑 + +
Kalman gain0.35
+
+ + +
+

Copilot 指令

+

维持同步轨道,准备重建遥测投影。

+
+
+
+ +
+
+

Orbital theatre

+ 3 assets +
+
+ +
+
+ +
+
+

Telemetry

+ Updated 0.3s +
+ + + + + + + + + + +
AssetAltitudeVelocityStatus
+
+

Event feed

+
+
+
+
+ + + + 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; + } +}