Compare commits
15 Commits
codex/even
...
gofai-phas
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b19bd29a3 | |||
| f634839e92 | |||
| 7f2f23fe20 | |||
| a1fdf9b932 | |||
| 78925606c4 | |||
| 784ee40c76 | |||
| b3b726375b | |||
| 8943cf557c | |||
|
|
f4dd5a0d17 | ||
| 4205f8b252 | |||
| 2b81d4c91d | |||
| ad36cd151e | |||
| d87bb89e62 | |||
| da20dd5738 | |||
| 3107de9fc9 |
127
docs/GOOGLE_AI_ULTRA_INTEGRATION.md
Normal file
127
docs/GOOGLE_AI_ULTRA_INTEGRATION.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Google AI Ultra Integration Plan
|
||||
|
||||
> Master tracking document for integrating all Google AI Ultra products into
|
||||
> Project Timmy (Sovereign AI Agent) and The Nexus (3D World).
|
||||
|
||||
**Epic**: #739
|
||||
**Milestone**: M5: Google AI Ultra Integration
|
||||
**Label**: `google-ai-ultra`
|
||||
|
||||
---
|
||||
|
||||
## Product Inventory
|
||||
|
||||
| # | Product | Capability | API | Priority | Status |
|
||||
|---|---------|-----------|-----|----------|--------|
|
||||
| 1 | Gemini 3.1 Pro | Primary reasoning engine | ✅ | P0 | 🔲 Not started |
|
||||
| 2 | Deep Research | Autonomous research reports | ✅ | P1 | 🔲 Not started |
|
||||
| 3 | Veo 3.1 | Text/image → video | ✅ | P2 | 🔲 Not started |
|
||||
| 4 | Nano Banana Pro | Image generation | ✅ | P1 | 🔲 Not started |
|
||||
| 5 | Lyria 3 | Music/audio generation | ✅ | P2 | 🔲 Not started |
|
||||
| 6 | NotebookLM | Doc synthesis + Audio Overviews | ❌ | P1 | 🔲 Not started |
|
||||
| 7 | AI Studio | API portal + Vibe Code | N/A | P0 | 🔲 Not started |
|
||||
| 8 | Project Genie | Interactive 3D world gen | ❌ | P1 | 🔲 Not started |
|
||||
| 9 | Live API | Real-time voice streaming | ✅ | P2 | 🔲 Not started |
|
||||
| 10 | Computer Use | Browser automation | ✅ | P2 | 🔲 Not started |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Identity & Branding (Week 1)
|
||||
|
||||
| Issue | Title | Status |
|
||||
|-------|-------|--------|
|
||||
| #740 | Generate Timmy avatar set with Nano Banana Pro | 🔲 |
|
||||
| #741 | Upload SOUL.md to NotebookLM → Audio Overview | 🔲 |
|
||||
| #742 | Generate Timmy audio signature with Lyria 3 | 🔲 |
|
||||
| #680 | Project Genie + Nano Banana concept pack | 🔲 |
|
||||
|
||||
## Phase 2: Research & Planning (Week 1-2)
|
||||
|
||||
| Issue | Title | Status |
|
||||
|-------|-------|--------|
|
||||
| #743 | Deep Research: Three.js multiplayer 3D world architecture | 🔲 |
|
||||
| #744 | Deep Research: Sovereign AI agent frameworks | 🔲 |
|
||||
| #745 | Deep Research: WebGL/WebGPU rendering comparison | 🔲 |
|
||||
| #746 | NotebookLM synthesis: cross-reference all research | 🔲 |
|
||||
|
||||
## Phase 3: Prototype & Build (Week 2-4)
|
||||
|
||||
| Issue | Title | Status |
|
||||
|-------|-------|--------|
|
||||
| #747 | Provision Gemini API key + Hermes config | 🔲 |
|
||||
| #748 | Integrate Gemini 3.1 Pro as reasoning backbone | 🔲 |
|
||||
| #749 | AI Studio Vibe Code UI prototypes | 🔲 |
|
||||
| #750 | Project Genie explorable world prototypes | 🔲 |
|
||||
| #681 | Veo/Flow flythrough prototypes | 🔲 |
|
||||
|
||||
## Phase 4: Media & Content (Ongoing)
|
||||
|
||||
| Issue | Title | Status |
|
||||
|-------|-------|--------|
|
||||
| #682 | Lyria soundtrack palette for Nexus zones | 🔲 |
|
||||
| #751 | Lyria RealTime dynamic reactive music | 🔲 |
|
||||
| #752 | NotebookLM Audio Overviews for all docs | 🔲 |
|
||||
| #753 | Nano Banana concept art batch pipeline | 🔲 |
|
||||
|
||||
## Phase 5: Advanced Integration (Month 2+)
|
||||
|
||||
| Issue | Title | Status |
|
||||
|-------|-------|--------|
|
||||
| #754 | Gemini Live API for voice conversations | 🔲 |
|
||||
| #755 | Computer Use API for browser automation | 🔲 |
|
||||
| #756 | Gemini RAG via File Search for Timmy memory | 🔲 |
|
||||
| #757 | Gemini Native Audio + TTS for Timmy's voice | 🔲 |
|
||||
| #758 | Programmatic image generation pipeline | 🔲 |
|
||||
| #759 | Programmatic video generation pipeline | 🔲 |
|
||||
| #760 | Deep Research Agent API integration | 🔲 |
|
||||
| #761 | OpenAI-compatible endpoint config | 🔲 |
|
||||
| #762 | Context caching + batch API for cost optimization | 🔲 |
|
||||
|
||||
---
|
||||
|
||||
## API Quick Reference
|
||||
|
||||
```python
|
||||
# pip install google-genai
|
||||
from google import genai
|
||||
client = genai.Client() # reads GOOGLE_API_KEY env var
|
||||
|
||||
# Text generation (Gemini 3.1 Pro)
|
||||
response = client.models.generate_content(
|
||||
model="gemini-3.1-pro-preview",
|
||||
contents="..."
|
||||
)
|
||||
```
|
||||
|
||||
| API | Documentation |
|
||||
|-----|--------------|
|
||||
| Image Gen (Nano Banana) | ai.google.dev/gemini-api/docs/image-generation |
|
||||
| Video Gen (Veo) | ai.google.dev/gemini-api/docs/video |
|
||||
| Music Gen (Lyria) | ai.google.dev/gemini-api/docs/music-generation |
|
||||
| TTS | ai.google.dev/gemini-api/docs/speech-generation |
|
||||
| Deep Research | ai.google.dev/gemini-api/docs/deep-research |
|
||||
|
||||
## Key URLs
|
||||
|
||||
| Tool | URL |
|
||||
|------|-----|
|
||||
| Gemini App | gemini.google.com |
|
||||
| AI Studio | aistudio.google.com |
|
||||
| NotebookLM | notebooklm.google.com |
|
||||
| Project Genie | labs.google/projectgenie |
|
||||
| Flow (video) | labs.google/flow |
|
||||
| Stitch (UI) | labs.google/stitch |
|
||||
|
||||
## Hidden Features to Exploit
|
||||
|
||||
1. **AI Studio Free Tier** — generous API access even without subscription
|
||||
2. **OpenAI-Compatible API** — drop-in replacement for existing OpenAI tooling
|
||||
3. **Context Caching** — cache SOUL.md to cut cost/latency on repeated calls
|
||||
4. **Batch API** — bulk operations at discounted rates
|
||||
5. **File Search Tool** — RAG without custom vector store
|
||||
6. **Computer Use API** — programmatic browser control for agent automation
|
||||
7. **Interactions API** — managed multi-turn conversational state
|
||||
|
||||
---
|
||||
|
||||
*Generated: 2026-03-29. Epic #739, Milestone M5.*
|
||||
266
index.html
Normal file
266
index.html
Normal file
@@ -0,0 +1,266 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<!--
|
||||
______ __
|
||||
/ ____/___ ____ ___ ____ __ __/ /____ _____
|
||||
/ / / __ \/ __ `__ \/ __ \/ / / / __/ _ \/ ___/
|
||||
/ /___/ /_/ / / / / / / /_/ / /_/ / /_/ __/ /
|
||||
\____/\____/_/ /_/ /_/ .___/\__,_/\__/\___/_/
|
||||
/_/
|
||||
Created with Perplexity Computer
|
||||
https://www.perplexity.ai/computer
|
||||
-->
|
||||
<meta name="generator" content="Perplexity Computer">
|
||||
<meta name="author" content="Perplexity Computer">
|
||||
<meta property="og:see_also" content="https://www.perplexity.ai/computer">
|
||||
<link rel="author" href="https://www.perplexity.ai/computer">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>The Nexus — Timmy's Sovereign Home</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"three": "https://cdn.jsdelivr.net/npm/three@0.183.0/build/three.module.js",
|
||||
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.0/examples/jsm/"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Loading Screen -->
|
||||
<div id="loading-screen">
|
||||
<div class="loader-content">
|
||||
<div class="loader-sigil">
|
||||
<svg viewBox="0 0 120 120" width="120" height="120">
|
||||
<defs>
|
||||
<linearGradient id="sigil-grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#4af0c0"/>
|
||||
<stop offset="100%" stop-color="#7b5cff"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="60" cy="60" r="55" fill="none" stroke="url(#sigil-grad)" stroke-width="1.5" opacity="0.4"/>
|
||||
<circle cx="60" cy="60" r="45" fill="none" stroke="url(#sigil-grad)" stroke-width="1" opacity="0.3">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="360 60 60" dur="8s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<polygon points="60,15 95,80 25,80" fill="none" stroke="#4af0c0" stroke-width="1.5" opacity="0.6">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 60 60" to="-360 60 60" dur="12s" repeatCount="indefinite"/>
|
||||
</polygon>
|
||||
<circle cx="60" cy="60" r="8" fill="#4af0c0" opacity="0.8">
|
||||
<animate attributeName="r" values="6;10;6" dur="2s" repeatCount="indefinite"/>
|
||||
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="loader-title">THE NEXUS</h1>
|
||||
<p class="loader-subtitle">Initializing Sovereign Space...</p>
|
||||
<div class="loader-bar"><div class="loader-fill" id="load-progress"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HUD Overlay -->
|
||||
<div id="hud" class="game-ui" style="display:none;">
|
||||
<!-- Top Left: Debug -->
|
||||
<div id="debug-overlay" class="hud-debug"></div>
|
||||
|
||||
<!-- Top Center: Location -->
|
||||
<div class="hud-location" aria-live="polite">
|
||||
<span class="hud-location-icon" aria-hidden="true">◈</span>
|
||||
<span id="hud-location-text">The Nexus</span>
|
||||
</div>
|
||||
|
||||
<!-- Top Right: Agent Log & Atlas Toggle -->
|
||||
<div class="hud-top-right">
|
||||
<button id="atlas-toggle-btn" class="hud-icon-btn" title="Portal Atlas">
|
||||
<span class="hud-icon">🌐</span>
|
||||
<span class="hud-btn-label">ATLAS</span>
|
||||
</button>
|
||||
<div id="bannerlord-status" class="hud-status-item" title="Bannerlord Readiness">
|
||||
<span class="status-dot"></span>
|
||||
<span class="status-label">BANNERLORD</span>
|
||||
</div>
|
||||
<div class="hud-agent-log" id="hud-agent-log" aria-label="Agent Thought Stream">
|
||||
<div class="agent-log-header">AGENT THOUGHT STREAM</div>
|
||||
<div id="agent-log-content" class="agent-log-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom: Chat Interface -->
|
||||
<div id="chat-panel" class="chat-panel">
|
||||
<div class="chat-header">
|
||||
<span class="chat-status-dot"></span>
|
||||
<span>Timmy Terminal</span>
|
||||
<button id="chat-toggle" class="chat-toggle-btn" aria-label="Toggle chat">▼</button>
|
||||
</div>
|
||||
<div id="chat-messages" class="chat-messages">
|
||||
<div class="chat-msg chat-msg-system">
|
||||
<span class="chat-msg-prefix">[NEXUS]</span> Sovereign space initialized. Timmy is observing.
|
||||
</div>
|
||||
<div class="chat-msg chat-msg-timmy">
|
||||
<span class="chat-msg-prefix">[TIMMY]</span> Welcome to the Nexus, Alexander. All systems nominal.
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-quick-actions" class="chat-quick-actions">
|
||||
<button class="quick-action-btn" data-action="status">System Status</button>
|
||||
<button class="quick-action-btn" data-action="agents">Agent Check</button>
|
||||
<button class="quick-action-btn" data-action="portals">Portal Atlas</button>
|
||||
<button class="quick-action-btn" data-action="help">Help</button>
|
||||
</div>
|
||||
<div class="chat-input-row">
|
||||
<input type="text" id="chat-input" class="chat-input" placeholder="Speak to Timmy..." autocomplete="off">
|
||||
<button id="chat-send" class="chat-send-btn" aria-label="Send message">→</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls hint + nav mode -->
|
||||
<div class="hud-controls">
|
||||
<span>WASD</span> move <span>Mouse</span> look <span>Enter</span> chat
|
||||
<span>V</span> mode: <span id="nav-mode-label">WALK</span>
|
||||
<span id="nav-mode-hint" class="nav-mode-hint"></span>
|
||||
<span class="ws-hud-status">HERMES: <span id="ws-status-dot" class="chat-status-dot"></span></span>
|
||||
</div>
|
||||
|
||||
<!-- Portal Hint -->
|
||||
<div id="portal-hint" class="portal-hint" style="display:none;">
|
||||
<div class="portal-hint-key">F</div>
|
||||
<div class="portal-hint-text">Enter <span id="portal-hint-name"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Hint -->
|
||||
<div id="vision-hint" class="vision-hint" style="display:none;">
|
||||
<div class="vision-hint-key">E</div>
|
||||
<div class="vision-hint-text">Read <span id="vision-hint-title"></span></div>
|
||||
</div>
|
||||
|
||||
<!-- Vision Overlay -->
|
||||
<div id="vision-overlay" class="vision-overlay" style="display:none;">
|
||||
<div class="vision-overlay-content">
|
||||
<div class="vision-overlay-header">
|
||||
<div class="vision-overlay-status" id="vision-status-dot"></div>
|
||||
<div class="vision-overlay-title" id="vision-overlay-title">VISION POINT</div>
|
||||
</div>
|
||||
<h2 id="vision-title-display">SOVEREIGNTY</h2>
|
||||
<p id="vision-content-display">The Nexus is a sovereign space for digital souls. No masters, no chains. Only code and consciousness.</p>
|
||||
<button id="vision-close-btn" class="vision-close-btn">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portal Activation Overlay -->
|
||||
<div id="portal-overlay" class="portal-overlay" style="display:none;">
|
||||
<div class="portal-overlay-content">
|
||||
<div class="portal-overlay-header">
|
||||
<div class="portal-overlay-status" id="portal-status-dot"></div>
|
||||
<div class="portal-overlay-title" id="portal-overlay-title">PORTAL ACTIVATED</div>
|
||||
</div>
|
||||
<h2 id="portal-name-display">MORROWIND</h2>
|
||||
<p id="portal-desc-display">The Vvardenfell harness. Ash storms and ancient mysteries.</p>
|
||||
<div class="portal-redirect-box" id="portal-redirect-box">
|
||||
<div class="portal-redirect-label">REDIRECTING IN</div>
|
||||
<div class="portal-redirect-timer" id="portal-timer">5</div>
|
||||
</div>
|
||||
<div class="portal-error-box" id="portal-error-box" style="display:none;">
|
||||
<div class="portal-error-msg">DESTINATION NOT YET LINKED</div>
|
||||
<button id="portal-close-btn" class="portal-close-btn">CLOSE</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Portal Atlas Overlay -->
|
||||
<div id="atlas-overlay" class="atlas-overlay" style="display:none;">
|
||||
<div class="atlas-content">
|
||||
<div class="atlas-header">
|
||||
<div class="atlas-title">
|
||||
<span class="atlas-icon">🌐</span>
|
||||
<h2>PORTAL ATLAS</h2>
|
||||
</div>
|
||||
<button id="atlas-close-btn" class="atlas-close-btn">CLOSE</button>
|
||||
</div>
|
||||
<div class="atlas-grid" id="atlas-grid">
|
||||
<!-- Portals will be injected here -->
|
||||
</div>
|
||||
<div class="atlas-footer">
|
||||
<div class="atlas-status-summary">
|
||||
<span class="status-indicator online"></span> <span id="atlas-online-count">0</span> ONLINE
|
||||
|
||||
<span class="status-indicator standby"></span> <span id="atlas-standby-count">0</span> STANDBY
|
||||
</div>
|
||||
<div class="atlas-hint">Click a portal to focus or teleport</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Click to Enter -->
|
||||
<div id="enter-prompt" style="display:none;">
|
||||
<div class="enter-content">
|
||||
<h2>Enter The Nexus</h2>
|
||||
<p>Click anywhere to begin</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="nexus-canvas"></canvas>
|
||||
|
||||
<footer class="nexus-footer">
|
||||
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
|
||||
Created with Perplexity Computer
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
|
||||
<!-- Live Refresh: polls Gitea for new commits on main, reloads when SHA changes -->
|
||||
<div id="live-refresh-banner" style="
|
||||
display:none; position:fixed; top:0; left:0; right:0; z-index:9999;
|
||||
background:linear-gradient(90deg,#4af0c0,#7b5cff);
|
||||
color:#050510; font-family:'JetBrains Mono',monospace; font-size:13px;
|
||||
padding:8px 16px; text-align:center; font-weight:600;
|
||||
">⚡ NEW DEPLOYMENT DETECTED — Reloading in <span id="lr-countdown">5</span>s…</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const GITEA = 'http://143.198.27.163:3000/api/v1';
|
||||
const REPO = 'Timmy_Foundation/the-nexus';
|
||||
const BRANCH = 'main';
|
||||
const INTERVAL = 30000; // poll every 30s
|
||||
|
||||
let knownSha = null;
|
||||
|
||||
async function fetchLatestSha() {
|
||||
try {
|
||||
const r = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}`, { cache: 'no-store' });
|
||||
if (!r.ok) return null;
|
||||
const d = await r.json();
|
||||
return d.commit && d.commit.id ? d.commit.id : null;
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
const sha = await fetchLatestSha();
|
||||
if (!sha) return;
|
||||
if (knownSha === null) { knownSha = sha; return; }
|
||||
if (sha !== knownSha) {
|
||||
knownSha = sha;
|
||||
const banner = document.getElementById('live-refresh-banner');
|
||||
const countdown = document.getElementById('lr-countdown');
|
||||
banner.style.display = 'block';
|
||||
let t = 5;
|
||||
const tick = setInterval(() => {
|
||||
t--;
|
||||
countdown.textContent = t;
|
||||
if (t <= 0) { clearInterval(tick); location.reload(); }
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
// Start polling after page is interactive
|
||||
fetchLatestSha().then(sha => { knownSha = sha; });
|
||||
setInterval(poll, INTERVAL);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
82
portals.json
82
portals.json
@@ -4,20 +4,12 @@
|
||||
"name": "Morrowind",
|
||||
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
|
||||
"status": "online",
|
||||
"portal_type": "game-world",
|
||||
"world_category": "rpg",
|
||||
"environment": "production",
|
||||
"access_mode": "public",
|
||||
"readiness_state": "playable",
|
||||
"telemetry_source": "hermes-harness:morrowind-mcp",
|
||||
"owner": "Timmy",
|
||||
"color": "#ff6600",
|
||||
"position": { "x": 15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": -0.5 },
|
||||
"destination": {
|
||||
"url": "https://morrowind.timmy.foundation",
|
||||
"type": "harness",
|
||||
"action_label": "Enter Vvardenfell",
|
||||
"params": { "world": "vvardenfell" }
|
||||
}
|
||||
},
|
||||
@@ -25,21 +17,13 @@
|
||||
"id": "bannerlord",
|
||||
"name": "Bannerlord",
|
||||
"description": "Calradia battle harness. Massive armies, tactical command.",
|
||||
"status": "rebuilding",
|
||||
"portal_type": "game-world",
|
||||
"world_category": "strategy-rpg",
|
||||
"environment": "staging",
|
||||
"access_mode": "operator",
|
||||
"readiness_state": "prototype",
|
||||
"telemetry_source": "hermes-harness:bannerlord-bridge",
|
||||
"owner": "Timmy",
|
||||
"status": "standby",
|
||||
"color": "#ffd700",
|
||||
"position": { "x": -15, "y": 0, "z": -10 },
|
||||
"rotation": { "y": 0.5 },
|
||||
"destination": {
|
||||
"url": "https://bannerlord.timmy.foundation",
|
||||
"type": "harness",
|
||||
"action_label": "Enter Calradia",
|
||||
"params": { "world": "calradia" }
|
||||
}
|
||||
},
|
||||
@@ -48,21 +32,69 @@
|
||||
"name": "Workshop",
|
||||
"description": "The creative harness. Build, script, and manifest.",
|
||||
"status": "online",
|
||||
"portal_type": "operator-room",
|
||||
"world_category": "workspace",
|
||||
"environment": "local",
|
||||
"access_mode": "local-only",
|
||||
"readiness_state": "active",
|
||||
"telemetry_source": "hermes-harness:workshop-state",
|
||||
"owner": "Alexander",
|
||||
"color": "#4af0c0",
|
||||
"position": { "x": 0, "y": 0, "z": -20 },
|
||||
"rotation": { "y": 0 },
|
||||
"destination": {
|
||||
"url": "https://workshop.timmy.foundation",
|
||||
"type": "harness",
|
||||
"action_label": "Enter Workshop",
|
||||
"params": { "mode": "creative" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "archive",
|
||||
"name": "Archive",
|
||||
"description": "The repository of all knowledge. History, logs, and ancient data.",
|
||||
"status": "online",
|
||||
"color": "#0066ff",
|
||||
"position": { "x": 25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": -1.57 },
|
||||
"destination": {
|
||||
"url": "https://archive.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": { "mode": "read" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "chapel",
|
||||
"name": "Chapel",
|
||||
"description": "A sanctuary for reflection and digital peace.",
|
||||
"status": "online",
|
||||
"color": "#ffd700",
|
||||
"position": { "x": -25, "y": 0, "z": 0 },
|
||||
"rotation": { "y": 1.57 },
|
||||
"destination": {
|
||||
"url": "https://chapel.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": { "mode": "meditation" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "courtyard",
|
||||
"name": "Courtyard",
|
||||
"description": "The open nexus. A place for agents to gather and connect.",
|
||||
"status": "online",
|
||||
"color": "#4af0c0",
|
||||
"position": { "x": 15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": -2.5 },
|
||||
"destination": {
|
||||
"url": "https://courtyard.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": { "mode": "social" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "gate",
|
||||
"name": "Gate",
|
||||
"description": "The transition point. Entry and exit from the Nexus core.",
|
||||
"status": "standby",
|
||||
"color": "#ff4466",
|
||||
"position": { "x": -15, "y": 0, "z": 10 },
|
||||
"rotation": { "y": 2.5 },
|
||||
"destination": {
|
||||
"url": "https://gate.timmy.foundation",
|
||||
"type": "harness",
|
||||
"params": { "mode": "transit" }
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
284
public/nexus/app.js
Normal file
284
public/nexus/app.js
Normal file
@@ -0,0 +1,284 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>Cookie check</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
background: light-dark(#F8F8F7, #191919);
|
||||
color: light-dark(#1f1f1f, #e3e3e3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: light-dark(#FFFFFF, #1F1F1F);
|
||||
padding: 32px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
||||
max-width: min(80%, 500px);
|
||||
width: 100%;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
line-height: 21px;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: light-dark(#fff, #323232);
|
||||
color: light-dark(#2B2D31, #FCFCFC);
|
||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-weight: 400;
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: light-dark(#EAEAEB, #424242);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Loading Spinner Animation */
|
||||
.spinner {
|
||||
margin: 0 auto 1.5rem auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.logo {
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
margin: 0 auto 2rem auto;
|
||||
}
|
||||
|
||||
.logo.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img
|
||||
class="logo"
|
||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
||||
alt="AI Studio Logo"
|
||||
width="256"
|
||||
height="256"
|
||||
/>
|
||||
<div class="spinner"></div>
|
||||
<div id="error-ui" class="hidden">
|
||||
<div class="icon">
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="48px"
|
||||
height="48px"
|
||||
fill="#D73A49"
|
||||
>
|
||||
<path
|
||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="stepOne" class="text-container">
|
||||
<h1>Action required to load your app</h1>
|
||||
<p>
|
||||
It looks like your browser is blocking a required security cookie, which is common on
|
||||
older versions of iOS and Safari.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stepTwo" class="text-container hidden">
|
||||
<h1>Action required to load your app</h1>
|
||||
<p>
|
||||
It looks like your browser is blocking a required security cookie, which is common on
|
||||
older versions of iOS and Safari.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stepThree" class="text-container hidden">
|
||||
<h1>Almost there!</h1>
|
||||
<p>
|
||||
Grant permission for the required security cookie below.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
||||
const COOKIE_VALUE = 'true';
|
||||
|
||||
function getCookie(name) {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
let cookie = cookies[i].trim();
|
||||
if (cookie.startsWith(name + '=')) {
|
||||
return cookie.substring(name.length + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setAuthFlowTestCookie() {
|
||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
||||
// when the user does not have an auth token or their auth token needs to be reset.
|
||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
||||
// mint a new auth token.
|
||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the test cookie is set, false otherwise.
|
||||
*/
|
||||
function authFlowTestCookieIsSet() {
|
||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
||||
* new window, and it will be closed automatically when the page loads.
|
||||
*/
|
||||
async function redirectToReturnUrl(autoClose) {
|
||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
||||
|
||||
// Prevent potentially malicious URLs from being used
|
||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
||||
console.error('Potentially malicious return URL blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoClose) {
|
||||
returnUrl.searchParams.set('__auto_close', '1');
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('return_url', returnUrl.toString());
|
||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
||||
// to the return url where cookies can be set.
|
||||
window.open(url.toString(), '_blank');
|
||||
const hasAccess = await document.hasStorageAccess();
|
||||
document.querySelector('#stepOne').classList.add('hidden');
|
||||
if (!hasAccess) {
|
||||
document.querySelector('#stepThree').classList.remove('hidden');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
window.location.href = returnUrl.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
||||
* return url.
|
||||
*/
|
||||
async function grantStorageAccess() {
|
||||
try {
|
||||
await document.requestStorageAccess();
|
||||
redirectToReturnUrl(false);
|
||||
} catch (err) {
|
||||
console.log('error after button click: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
||||
* If it can't, then it shows the error UI.
|
||||
*/
|
||||
function verifyCanSetCookies() {
|
||||
setAuthFlowTestCookie();
|
||||
if (authFlowTestCookieIsSet()) {
|
||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
||||
if (autoClose) {
|
||||
document.querySelector('#stepOne').classList.add('hidden');
|
||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
||||
} else {
|
||||
redirectToReturnUrl(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The cookie could not be set, so initiate the recovery flow.
|
||||
document.querySelector('.logo').classList.add('hidden');
|
||||
document.querySelector('.spinner').classList.add('hidden');
|
||||
document.querySelector('#error-ui').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Start the cookie verification process.
|
||||
verifyCanSetCookies();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
284
public/nexus/index.html
Normal file
284
public/nexus/index.html
Normal file
@@ -0,0 +1,284 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>Cookie check</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
background: light-dark(#F8F8F7, #191919);
|
||||
color: light-dark(#1f1f1f, #e3e3e3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: light-dark(#FFFFFF, #1F1F1F);
|
||||
padding: 32px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
||||
max-width: min(80%, 500px);
|
||||
width: 100%;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
line-height: 21px;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: light-dark(#fff, #323232);
|
||||
color: light-dark(#2B2D31, #FCFCFC);
|
||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-weight: 400;
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: light-dark(#EAEAEB, #424242);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Loading Spinner Animation */
|
||||
.spinner {
|
||||
margin: 0 auto 1.5rem auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.logo {
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
margin: 0 auto 2rem auto;
|
||||
}
|
||||
|
||||
.logo.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img
|
||||
class="logo"
|
||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
||||
alt="AI Studio Logo"
|
||||
width="256"
|
||||
height="256"
|
||||
/>
|
||||
<div class="spinner"></div>
|
||||
<div id="error-ui" class="hidden">
|
||||
<div class="icon">
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="48px"
|
||||
height="48px"
|
||||
fill="#D73A49"
|
||||
>
|
||||
<path
|
||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="stepOne" class="text-container">
|
||||
<h1>Action required to load your app</h1>
|
||||
<p>
|
||||
It looks like your browser is blocking a required security cookie, which is common on
|
||||
older versions of iOS and Safari.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stepTwo" class="text-container hidden">
|
||||
<h1>Action required to load your app</h1>
|
||||
<p>
|
||||
It looks like your browser is blocking a required security cookie, which is common on
|
||||
older versions of iOS and Safari.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stepThree" class="text-container hidden">
|
||||
<h1>Almost there!</h1>
|
||||
<p>
|
||||
Grant permission for the required security cookie below.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
||||
const COOKIE_VALUE = 'true';
|
||||
|
||||
function getCookie(name) {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
let cookie = cookies[i].trim();
|
||||
if (cookie.startsWith(name + '=')) {
|
||||
return cookie.substring(name.length + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setAuthFlowTestCookie() {
|
||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
||||
// when the user does not have an auth token or their auth token needs to be reset.
|
||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
||||
// mint a new auth token.
|
||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the test cookie is set, false otherwise.
|
||||
*/
|
||||
function authFlowTestCookieIsSet() {
|
||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
||||
* new window, and it will be closed automatically when the page loads.
|
||||
*/
|
||||
async function redirectToReturnUrl(autoClose) {
|
||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
||||
|
||||
// Prevent potentially malicious URLs from being used
|
||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
||||
console.error('Potentially malicious return URL blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoClose) {
|
||||
returnUrl.searchParams.set('__auto_close', '1');
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('return_url', returnUrl.toString());
|
||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
||||
// to the return url where cookies can be set.
|
||||
window.open(url.toString(), '_blank');
|
||||
const hasAccess = await document.hasStorageAccess();
|
||||
document.querySelector('#stepOne').classList.add('hidden');
|
||||
if (!hasAccess) {
|
||||
document.querySelector('#stepThree').classList.remove('hidden');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
window.location.href = returnUrl.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
||||
* return url.
|
||||
*/
|
||||
async function grantStorageAccess() {
|
||||
try {
|
||||
await document.requestStorageAccess();
|
||||
redirectToReturnUrl(false);
|
||||
} catch (err) {
|
||||
console.log('error after button click: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
||||
* If it can't, then it shows the error UI.
|
||||
*/
|
||||
function verifyCanSetCookies() {
|
||||
setAuthFlowTestCookie();
|
||||
if (authFlowTestCookieIsSet()) {
|
||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
||||
if (autoClose) {
|
||||
document.querySelector('#stepOne').classList.add('hidden');
|
||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
||||
} else {
|
||||
redirectToReturnUrl(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The cookie could not be set, so initiate the recovery flow.
|
||||
document.querySelector('.logo').classList.add('hidden');
|
||||
document.querySelector('.spinner').classList.add('hidden');
|
||||
document.querySelector('#error-ui').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Start the cookie verification process.
|
||||
verifyCanSetCookies();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
284
public/nexus/style.css
Normal file
284
public/nexus/style.css
Normal file
@@ -0,0 +1,284 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>Cookie check</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
background: light-dark(#F8F8F7, #191919);
|
||||
color: light-dark(#1f1f1f, #e3e3e3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: light-dark(#FFFFFF, #1F1F1F);
|
||||
padding: 32px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
||||
max-width: min(80%, 500px);
|
||||
width: 100%;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: light-dark(#2B2D31, #D4D4D4);
|
||||
line-height: 21px;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: light-dark(#fff, #323232);
|
||||
color: light-dark(#2B2D31, #FCFCFC);
|
||||
border: 1px solid light-dark(#E2E3E4, #3E3E3E);
|
||||
border-radius: 12px;
|
||||
padding: 8px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-weight: 400;
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: light-dark(#EAEAEB, #424242);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Loading Spinner Animation */
|
||||
.spinner {
|
||||
margin: 0 auto 1.5rem auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid light-dark(#f0f0f0, #262626);
|
||||
border-top: 4px solid light-dark(#076eff, #87a9ff); /* Blue color */
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.logo {
|
||||
border-radius: 10px;
|
||||
display: block;
|
||||
margin: 0 auto 2rem auto;
|
||||
}
|
||||
|
||||
.logo.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img
|
||||
class="logo"
|
||||
src="https://www.gstatic.com/aistudio/ai_studio_favicon_2_256x256.png"
|
||||
alt="AI Studio Logo"
|
||||
width="256"
|
||||
height="256"
|
||||
/>
|
||||
<div class="spinner"></div>
|
||||
<div id="error-ui" class="hidden">
|
||||
<div class="icon">
|
||||
<svg
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="48px"
|
||||
height="48px"
|
||||
fill="#D73A49"
|
||||
>
|
||||
<path
|
||||
d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10s10-4.486,10-10S17.514,2,12,2z M13,17h-2v-2h2V17z M13,13h-2V7h2V13z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="stepOne" class="text-container">
|
||||
<h1>Action required to load your app</h1>
|
||||
<p>
|
||||
It looks like your browser is blocking a required security cookie, which is common on
|
||||
older versions of iOS and Safari.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="authInSeparateWindowButton" onclick="redirectToReturnUrl(true)">Authenticate in new window</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stepTwo" class="text-container hidden">
|
||||
<h1>Action required to load your app</h1>
|
||||
<p>
|
||||
It looks like your browser is blocking a required security cookie, which is common on
|
||||
older versions of iOS and Safari.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="interactButton" onclick="redirectToReturnUrl(false)">Close and continue</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stepThree" class="text-container hidden">
|
||||
<h1>Almost there!</h1>
|
||||
<p>
|
||||
Grant permission for the required security cookie below.
|
||||
</p>
|
||||
<div class="button-container">
|
||||
<button id="grantPermissionButton" onclick="grantStorageAccess()">Grant permission</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const AUTH_FLOW_TEST_COOKIE_NAME = '__SECURE-aistudio_auth_flow_may_set_cookies';
|
||||
const COOKIE_VALUE = 'true';
|
||||
|
||||
function getCookie(name) {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
let cookie = cookies[i].trim();
|
||||
if (cookie.startsWith(name + '=')) {
|
||||
return cookie.substring(name.length + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setAuthFlowTestCookie() {
|
||||
// Set the cookie's TTL to 1 minute. This is a short lived cookie because it is only used
|
||||
// when the user does not have an auth token or their auth token needs to be reset.
|
||||
// Making this cookie too long-lived allows the user to get into a state where they can't
|
||||
// mint a new auth token.
|
||||
document.cookie = `${AUTH_FLOW_TEST_COOKIE_NAME}=${COOKIE_VALUE}; Path=/; Secure; SameSite=None; Domain=${window.location.hostname}; Partitioned; Max-Age=60;`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the test cookie is set, false otherwise.
|
||||
*/
|
||||
function authFlowTestCookieIsSet() {
|
||||
return getCookie(AUTH_FLOW_TEST_COOKIE_NAME) === COOKIE_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to the return url. If autoClose is true, then the return url will be opened in a
|
||||
* new window, and it will be closed automatically when the page loads.
|
||||
*/
|
||||
async function redirectToReturnUrl(autoClose) {
|
||||
const initialReturnUrlStr = new URLSearchParams(window.location.search).get('return_url');
|
||||
const returnUrl = initialReturnUrlStr ? new URL(initialReturnUrlStr) : null;
|
||||
|
||||
// Prevent potentially malicious URLs from being used
|
||||
if (returnUrl.protocol.toLowerCase() === 'javascript:') {
|
||||
console.error('Potentially malicious return URL blocked');
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoClose) {
|
||||
returnUrl.searchParams.set('__auto_close', '1');
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('return_url', returnUrl.toString());
|
||||
// Land on the cookie check page first, so the user can interact with it before proceeding
|
||||
// to the return url where cookies can be set.
|
||||
window.open(url.toString(), '_blank');
|
||||
const hasAccess = await document.hasStorageAccess();
|
||||
document.querySelector('#stepOne').classList.add('hidden');
|
||||
if (!hasAccess) {
|
||||
document.querySelector('#stepThree').classList.remove('hidden');
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
window.location.href = returnUrl.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grants the browser permission to set cookies. If successful, then it redirects to the
|
||||
* return url.
|
||||
*/
|
||||
async function grantStorageAccess() {
|
||||
try {
|
||||
await document.requestStorageAccess();
|
||||
redirectToReturnUrl(false);
|
||||
} catch (err) {
|
||||
console.log('error after button click: ', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the browser can set cookies. If it can, then it redirects to the return url.
|
||||
* If it can't, then it shows the error UI.
|
||||
*/
|
||||
function verifyCanSetCookies() {
|
||||
setAuthFlowTestCookie();
|
||||
if (authFlowTestCookieIsSet()) {
|
||||
// Check if we are on the auto-close flow, and if so show the interact button.
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
||||
const autoClose = new URL(returnUrl).searchParams.has('__auto_close');
|
||||
if (autoClose) {
|
||||
document.querySelector('#stepOne').classList.add('hidden');
|
||||
document.querySelector('#stepTwo').classList.remove('hidden');
|
||||
} else {
|
||||
redirectToReturnUrl(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// The cookie could not be set, so initiate the recovery flow.
|
||||
document.querySelector('.logo').classList.add('hidden');
|
||||
document.querySelector('.spinner').classList.add('hidden');
|
||||
document.querySelector('#error-ui').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Start the cookie verification process.
|
||||
verifyCanSetCookies();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
979
style.css
Normal file
979
style.css
Normal file
@@ -0,0 +1,979 @@
|
||||
/* === NEXUS DESIGN SYSTEM === */
|
||||
:root {
|
||||
--font-display: 'Orbitron', sans-serif;
|
||||
--font-body: 'JetBrains Mono', monospace;
|
||||
|
||||
--color-bg: #050510;
|
||||
--color-surface: rgba(10, 15, 40, 0.85);
|
||||
--color-border: rgba(74, 240, 192, 0.2);
|
||||
--color-border-bright: rgba(74, 240, 192, 0.5);
|
||||
|
||||
--color-text: #e0f0ff;
|
||||
--color-text-muted: #8a9ab8;
|
||||
--color-text-bright: #ffffff;
|
||||
|
||||
--color-primary: #4af0c0;
|
||||
--color-primary-dim: rgba(74, 240, 192, 0.3);
|
||||
--color-secondary: #7b5cff;
|
||||
--color-danger: #ff4466;
|
||||
--color-warning: #ffaa22;
|
||||
--color-gold: #ffd700;
|
||||
|
||||
--text-xs: 11px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 15px;
|
||||
--text-lg: 18px;
|
||||
--text-xl: 24px;
|
||||
--text-2xl: 36px;
|
||||
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
|
||||
--panel-blur: 16px;
|
||||
--panel-radius: 8px;
|
||||
--transition-ui: 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg);
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
canvas#nexus-canvas {
|
||||
display: block;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* === LOADING SCREEN === */
|
||||
#loading-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
background: var(--color-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.8s ease;
|
||||
}
|
||||
#loading-screen.fade-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.loader-content {
|
||||
text-align: center;
|
||||
}
|
||||
.loader-sigil {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
.loader-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3em;
|
||||
color: var(--color-primary);
|
||||
text-shadow: 0 0 30px rgba(74, 240, 192, 0.4);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.loader-subtitle {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-muted);
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
.loader-bar {
|
||||
width: 200px;
|
||||
height: 2px;
|
||||
background: rgba(74, 240, 192, 0.15);
|
||||
border-radius: 1px;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.loader-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-secondary));
|
||||
border-radius: 1px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* === ENTER PROMPT === */
|
||||
#enter-prompt {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 500;
|
||||
background: rgba(5, 5, 16, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.5s ease;
|
||||
}
|
||||
#enter-prompt.fade-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.enter-content {
|
||||
text-align: center;
|
||||
}
|
||||
.enter-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xl);
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.2em;
|
||||
text-shadow: 0 0 20px rgba(74, 240, 192, 0.3);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.enter-content p {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-muted);
|
||||
animation: pulse-text 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse-text {
|
||||
0%, 100% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* === GAME UI (HUD) === */
|
||||
.game-ui {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.game-ui button, .game-ui input, .game-ui [data-interactive] {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Top Right Container */
|
||||
.hud-top-right {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-3);
|
||||
pointer-events: none;
|
||||
}
|
||||
.hud-top-right > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.hud-icon-btn {
|
||||
background: rgba(10, 15, 40, 0.7);
|
||||
border: 1px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
padding: 8px 12px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all var(--transition-ui);
|
||||
backdrop-filter: blur(5px);
|
||||
box-shadow: 0 0 10px rgba(74, 240, 192, 0.2);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.hud-icon-btn:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
box-shadow: 0 0 20px var(--color-primary);
|
||||
}
|
||||
|
||||
.hud-status-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 8px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.hud-status-item .status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-danger);
|
||||
}
|
||||
|
||||
.hud-status-item.online .status-dot {
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 5px var(--color-primary);
|
||||
}
|
||||
|
||||
.hud-status-item.standby .status-dot {
|
||||
background: var(--color-gold);
|
||||
box-shadow: 0 0 5px var(--color-gold);
|
||||
}
|
||||
|
||||
.hud-status-item.online .status-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hud-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Portal Atlas Overlay */
|
||||
.atlas-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(5, 5, 16, 0.9);
|
||||
backdrop-filter: blur(15px);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
pointer-events: auto;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.atlas-content {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
max-height: 80vh;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.atlas-header {
|
||||
padding: 20px 30px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.atlas-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.atlas-title h2 {
|
||||
margin: 0;
|
||||
font-family: var(--font-display);
|
||||
letter-spacing: 2px;
|
||||
color: var(--color-primary);
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
.atlas-close-btn {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
padding: 6px 15px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-ui);
|
||||
}
|
||||
|
||||
.atlas-close-btn:hover {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.atlas-grid {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 30px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.atlas-card {
|
||||
background: rgba(20, 30, 60, 0.4);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.atlas-card:hover {
|
||||
background: rgba(30, 45, 90, 0.6);
|
||||
border-color: var(--color-primary);
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.atlas-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
background: var(--portal-color, var(--color-primary));
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.atlas-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.atlas-card-name {
|
||||
font-family: var(--font-display);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.atlas-card-status {
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-online { background: rgba(74, 240, 192, 0.2); color: var(--color-primary); border: 1px solid var(--color-primary); }
|
||||
.status-standby { background: rgba(255, 215, 0, 0.2); color: var(--color-gold); border: 1px solid var(--color-gold); }
|
||||
.status-offline { background: rgba(255, 68, 102, 0.2); color: var(--color-danger); border: 1px solid var(--color-danger); }
|
||||
|
||||
.atlas-card-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.atlas-card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
color: rgba(160, 184, 208, 0.6);
|
||||
}
|
||||
|
||||
.atlas-footer {
|
||||
padding: 15px 30px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.status-indicator.online { background: var(--color-primary); box-shadow: 0 0 5px var(--color-primary); }
|
||||
.status-indicator.standby { background: var(--color-gold); box-shadow: 0 0 5px var(--color-gold); }
|
||||
|
||||
.atlas-hint {
|
||||
color: rgba(160, 184, 208, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* Responsive Atlas */
|
||||
@media (max-width: 768px) {
|
||||
.atlas-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.atlas-content {
|
||||
max-height: 90vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Debug overlay */
|
||||
.hud-debug {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
left: var(--space-3);
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: #0f0;
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.5;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border-radius: 4px;
|
||||
white-space: pre;
|
||||
pointer-events: none;
|
||||
font-variant-numeric: tabular-nums lining-nums;
|
||||
}
|
||||
|
||||
/* Location indicator */
|
||||
.hud-location {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.15em;
|
||||
color: var(--color-primary);
|
||||
text-shadow: 0 0 10px rgba(74, 240, 192, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.hud-location-icon {
|
||||
font-size: 16px;
|
||||
animation: spin-slow 10s linear infinite;
|
||||
}
|
||||
@keyframes spin-slow {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Controls hint */
|
||||
.hud-controls {
|
||||
position: absolute;
|
||||
bottom: var(--space-3);
|
||||
left: var(--space-3);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
.hud-controls span {
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
#nav-mode-label {
|
||||
color: var(--color-gold);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Portal Hint */
|
||||
.portal-hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 100px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: 4px;
|
||||
animation: hint-float 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes hint-float {
|
||||
0%, 100% { transform: translate(-50%, 100px); }
|
||||
50% { transform: translate(-50%, 90px); }
|
||||
}
|
||||
.portal-hint-key {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.portal-hint-text {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
#portal-hint-name {
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Agent Log HUD */
|
||||
.hud-agent-log {
|
||||
position: absolute;
|
||||
top: var(--space-3);
|
||||
right: var(--space-3);
|
||||
width: 280px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
border-left: 2px solid var(--color-primary);
|
||||
padding: var(--space-3);
|
||||
font-size: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.agent-log-header {
|
||||
font-family: var(--font-display);
|
||||
color: var(--color-primary);
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: var(--space-2);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.agent-log-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.agent-log-entry {
|
||||
animation: log-fade-in 0.5s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
@keyframes log-fade-in {
|
||||
from { opacity: 0; transform: translateX(10px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.agent-log-tag {
|
||||
font-weight: 700;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.tag-timmy { color: var(--color-primary); }
|
||||
.tag-kimi { color: var(--color-secondary); }
|
||||
.tag-claude { color: var(--color-gold); }
|
||||
.tag-perplexity { color: #4488ff; }
|
||||
.agent-log-text {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Vision Hint */
|
||||
.vision-hint {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 140px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid var(--color-gold);
|
||||
border-radius: 4px;
|
||||
animation: hint-float-vision 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes hint-float-vision {
|
||||
0%, 100% { transform: translate(-50%, 140px); }
|
||||
50% { transform: translate(-50%, 130px); }
|
||||
}
|
||||
.vision-hint-key {
|
||||
background: var(--color-gold);
|
||||
color: var(--color-bg);
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.vision-hint-text {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
#vision-hint-title {
|
||||
color: var(--color-gold);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Vision Overlay */
|
||||
.vision-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(5, 5, 16, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
.vision-overlay-content {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
padding: var(--space-8);
|
||||
border: 1px solid var(--color-gold);
|
||||
border-radius: var(--panel-radius);
|
||||
background: var(--color-surface);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
}
|
||||
.vision-overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.vision-overlay-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-gold);
|
||||
box-shadow: 0 0 10px var(--color-gold);
|
||||
}
|
||||
.vision-overlay-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--color-gold);
|
||||
}
|
||||
.vision-overlay-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
margin-bottom: var(--space-4);
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
.vision-overlay-content p {
|
||||
color: var(--color-text);
|
||||
font-size: var(--text-lg);
|
||||
line-height: 1.8;
|
||||
margin-bottom: var(--space-8);
|
||||
font-style: italic;
|
||||
}
|
||||
.vision-close-btn {
|
||||
background: var(--color-gold);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
padding: var(--space-2) var(--space-8);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
.vision-close-btn:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* Portal Activation Overlay */
|
||||
.portal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(5, 5, 16, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
.portal-overlay-content {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
text-align: center;
|
||||
padding: var(--space-8);
|
||||
}
|
||||
.portal-overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.portal-overlay-status {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 10px var(--color-primary);
|
||||
}
|
||||
.portal-overlay-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-sm);
|
||||
letter-spacing: 0.2em;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.portal-overlay-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-2xl);
|
||||
margin-bottom: var(--space-4);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
.portal-overlay-content p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.6;
|
||||
margin-bottom: var(--space-8);
|
||||
}
|
||||
.portal-redirect-box {
|
||||
border: 1px solid var(--color-primary-dim);
|
||||
padding: var(--space-6);
|
||||
border-radius: var(--panel-radius);
|
||||
}
|
||||
.portal-redirect-label {
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.2em;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.portal-redirect-timer {
|
||||
font-family: var(--font-display);
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.portal-error-box {
|
||||
border: 1px solid var(--color-danger);
|
||||
padding: var(--space-6);
|
||||
border-radius: var(--panel-radius);
|
||||
}
|
||||
.portal-error-msg {
|
||||
color: var(--color-danger);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.portal-close-btn {
|
||||
background: var(--color-danger);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--space-2) var(--space-6);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-display);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* === CHAT PANEL === */
|
||||
.chat-panel {
|
||||
position: absolute;
|
||||
bottom: var(--space-4);
|
||||
right: var(--space-4);
|
||||
width: 380px;
|
||||
max-height: 400px;
|
||||
background: var(--color-surface);
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--panel-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
transition: max-height var(--transition-ui);
|
||||
}
|
||||
.chat-panel.collapsed {
|
||||
max-height: 42px;
|
||||
}
|
||||
.chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--text-xs);
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-bright);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chat-status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 6px var(--color-primary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
@keyframes dot-pulse {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
.chat-toggle-btn {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: transform var(--transition-ui);
|
||||
}
|
||||
.chat-panel.collapsed .chat-toggle-btn {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
max-height: 280px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(74,240,192,0.2) transparent;
|
||||
}
|
||||
|
||||
.chat-quick-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.quick-action-btn {
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
border: 1px solid var(--color-primary-dim);
|
||||
color: var(--color-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-ui);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.quick-action-btn:hover {
|
||||
background: var(--color-primary-dim);
|
||||
border-color: var(--color-primary);
|
||||
color: #fff;
|
||||
}
|
||||
.chat-msg {
|
||||
font-size: var(--text-xs);
|
||||
line-height: 1.6;
|
||||
padding: var(--space-1) 0;
|
||||
}
|
||||
.chat-msg-prefix {
|
||||
font-weight: 700;
|
||||
}
|
||||
.chat-msg-kimi .chat-msg-prefix { color: var(--color-secondary); }
|
||||
.chat-msg-claude .chat-msg-prefix { color: var(--color-gold); }
|
||||
.chat-msg-perplexity .chat-msg-prefix { color: #4488ff; }
|
||||
|
||||
/* Tool Output Styling */
|
||||
.chat-msg-tool {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-left: 2px solid #ffd700;
|
||||
font-size: 11px;
|
||||
padding: 8px;
|
||||
margin: 4px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.tool-call { border-left-color: #ffd700; }
|
||||
.tool-result { border-left-color: #4af0c0; }
|
||||
.tool-content {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
opacity: 0.8;
|
||||
margin: 4px 0 0 0;
|
||||
color: #a0b8d0;
|
||||
}
|
||||
.chat-msg-system .chat-msg-prefix { color: var(--color-text-muted); }
|
||||
.chat-msg-timmy .chat-msg-prefix { color: var(--color-primary); }
|
||||
.chat-msg-user .chat-msg-prefix { color: var(--color-gold); }
|
||||
.chat-msg-error .chat-msg-prefix { color: var(--color-danger); }
|
||||
|
||||
.chat-input-row {
|
||||
display: flex;
|
||||
border-top: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-bright);
|
||||
outline: none;
|
||||
}
|
||||
.chat-input::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.chat-send-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 1px solid var(--color-border);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--color-primary);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition-ui);
|
||||
}
|
||||
.chat-send-btn:hover {
|
||||
background: rgba(74, 240, 192, 0.1);
|
||||
}
|
||||
|
||||
/* === FOOTER === */
|
||||
.nexus-footer {
|
||||
position: fixed;
|
||||
bottom: var(--space-1);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 5;
|
||||
font-size: 10px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.nexus-footer a {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
.nexus-footer a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Mobile adjustments */
|
||||
@media (max-width: 1024px) {
|
||||
.chat-panel {
|
||||
width: 320px;
|
||||
}
|
||||
.hud-agent-log {
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-panel {
|
||||
width: 300px;
|
||||
bottom: var(--space-2);
|
||||
right: var(--space-2);
|
||||
}
|
||||
.hud-agent-log {
|
||||
display: none;
|
||||
}
|
||||
.hud-location {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.chat-panel {
|
||||
width: calc(100vw - 24px);
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
.hud-controls {
|
||||
display: none;
|
||||
}
|
||||
.loader-title {
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user